From 772b6edf4ddffdedf4a163a25a5b28ee4dac6675 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Thu, 29 Jan 2026 16:09:46 -0600 Subject: [PATCH v3 1/1] move partitioning code out of tablecmds.c --- src/backend/commands/Makefile | 1 + src/backend/commands/meson.build | 1 + src/backend/commands/tablecmds.c | 4787 +------------------- src/backend/commands/tablecmds_partition.c | 4627 +++++++++++++++++++ src/backend/partitioning/partbounds.c | 1 + src/include/commands/tablecmds.h | 2 - src/include/commands/tablecmds_internal.h | 194 + src/include/commands/tablecmds_partition.h | 66 + 8 files changed, 4903 insertions(+), 4776 deletions(-) create mode 100644 src/backend/commands/tablecmds_partition.c create mode 100644 src/include/commands/tablecmds_internal.h create mode 100644 src/include/commands/tablecmds_partition.h diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 64cb6278409..764f75f6b52 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -57,6 +57,7 @@ OBJS = \ statscmds.o \ subscriptioncmds.o \ tablecmds.o \ + tablecmds_partition.o \ tablespace.o \ trigger.o \ tsearchcmds.o \ diff --git a/src/backend/commands/meson.build b/src/backend/commands/meson.build index ca3f53c6213..86b9d8780fd 100644 --- a/src/backend/commands/meson.build +++ b/src/backend/commands/meson.build @@ -45,6 +45,7 @@ backend_sources += files( 'statscmds.c', 'subscriptioncmds.c', 'tablecmds.c', + 'tablecmds_partition.c', 'tablespace.c', 'trigger.c', 'tsearchcmds.c', diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index f976c0e5c7e..4abba13a5b8 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -62,6 +62,8 @@ #include "commands/event_trigger.h" #include "commands/sequence.h" #include "commands/tablecmds.h" +#include "commands/tablecmds_internal.h" +#include "commands/tablecmds_partition.h" #include "commands/tablespace.h" #include "commands/trigger.h" #include "commands/typecmds.h" @@ -131,115 +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 -{ - char *name; /* Constraint name, or NULL if none */ - ConstrType contype; /* CHECK or FOREIGN */ - Oid refrelid; /* PK rel, if FOREIGN */ - Oid refindid; /* OID of PK's index, if FOREIGN */ - bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */ - Oid conid; /* OID of pg_constraint entry, if FOREIGN */ - Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */ - ExprState *qualstate; /* Execution state for CHECK expr */ -} NewConstraint; - -/* - * Struct describing one new column value that needs to be computed during - * Phase 3 copy (this could be either a new column with a non-null default, or - * a column that we're changing the type of). Columns without such an entry - * are just copied from the old table during ATRewriteTable. Note that the - * expr is an expression over *old* table values, except when is_generated - * is true; then it is an expression over columns of the *new* tuple. - */ -typedef struct NewColumnValue -{ - AttrNumber attnum; /* which column */ - Expr *expr; /* expression to compute */ - ExprState *exprstate; /* execution state */ - bool is_generated; /* is it a GENERATED expression? */ -} NewColumnValue; - /* * Error-reporting support for RemoveRelations */ @@ -325,17 +218,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 +232,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 +305,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); @@ -455,7 +327,6 @@ static CoercionPathType findFkeyCast(Oid targetTypeId, Oid sourceTypeId, static void validateForeignKeyConstraint(char *conname, Relation rel, Relation pkrel, Oid pkindOid, Oid constraintOid, bool hasperiod); -static void CheckAlterTableIsSafe(Relation rel); static void ATController(AlterTableStmt *parsetree, Relation rel, List *cmds, bool recurse, LOCKMODE lockmode, AlterTableUtilityContext *context); @@ -476,12 +347,9 @@ 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); -static void ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode); static void ATTypedTableRecursion(List **wqueue, Relation rel, AlterTableCmd *cmd, LOCKMODE lockmode, AlterTableUtilityContext *context); @@ -508,8 +376,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 +384,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 +429,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 +439,6 @@ 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,45 +515,9 @@ 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); -static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, - PartitionCmd *cmd, AlterTableUtilityContext *context); -static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, - Relation rel, PartitionCmd *cmd, - AlterTableUtilityContext *context); - /* ---------------------------------------------------------------- * DefineRelation * Creates a new relation. @@ -4443,7 +4215,7 @@ CheckTableNotInUse(Relation rel, const char *stmt) * that, notably DROP TABLE. (We must allow DROP or we couldn't clean out * an orphaned temp schema.) Compare truncate_check_activity(). */ -static void +void CheckAlterTableIsSafe(Relation rel) { /* @@ -6583,7 +6355,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); @@ -6764,7 +6536,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; @@ -6879,36 +6651,6 @@ ATSimpleRecursion(List **wqueue, Relation rel, } } -/* - * Obtain list of partitions of the given table, locking them all at the given - * lockmode and ensuring that they all pass CheckAlterTableIsSafe. - * - * This function is a no-op if the given relation is not a partitioned table; - * in particular, nothing is done if it's a legacy inheritance parent. - */ -static void -ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode) -{ - if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) - { - List *inh; - ListCell *cell; - - inh = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); - /* first element is the parent rel; must ignore it */ - for_each_from(cell, inh, 1) - { - Relation childrel; - - /* find_all_inheritors already got lock */ - childrel = table_open(lfirst_oid(cell), NoLock); - CheckAlterTableIsSafe(childrel); - table_close(childrel, NoLock); - } - list_free(inh); - } -} - /* * ATTypedTableRecursion * @@ -8513,7 +8255,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) { @@ -10748,7 +10490,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, @@ -10926,7 +10668,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, @@ -11064,7 +10806,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, @@ -11230,795 +10972,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 * @@ -12029,7 +10982,7 @@ RemoveInheritedConstraint(Relation conrel, Relation trigrel, Oid conoid, * trigger check will be skipped, and the trigger will be considered for * removal. */ -static void +void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, Oid confrelid, Oid conrelid) { @@ -12092,128 +11045,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 * @@ -13024,7 +11855,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) { @@ -17398,7 +16229,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; @@ -17884,78 +16715,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 * @@ -17974,7 +16733,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; @@ -19746,2290 +18505,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 */ - } - } -} - -/* - * attachPartitionTable: attach a new partition to the partitioned table - * - * wqueue: the ALTER TABLE work queue; can be NULL when not running as part - * of an ALTER TABLE sequence. - * rel: partitioned relation; - * attachrel: relation of attached partition; - * bound: bounds of attached relation. - */ -static void -attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound) -{ - /* - * Create an inheritance; the relevant checks are performed inside the - * function. - */ - CreateInheritance(attachrel, rel, true); - - /* Update the pg_class entry. */ - StorePartitionBound(attachrel, rel, 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); -} - -/* - * 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); - - attachPartitionTable(wqueue, rel, attachrel, cmd->bound); - - /* - * Generate a 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_array(Relation, list_length(attachRelIdxs)); - attachInfos = palloc_array(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(NULL, - 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. */ @@ -22104,1239 +18579,3 @@ GetAttributeStorage(Oid atttypid, const char *storagemode) return cstorage; } - -/* - * buildExpressionExecutionStates: build the needed expression execution states - * for new partition (newPartRel) checks and initialize expressions for - * generated columns. All expressions should be created in "tab" - * (AlteredTableInfo structure). - */ -static void -buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate) -{ - /* - * Build the needed expression execution states. Here, we expect only NOT - * NULL and CHECK constraint. - */ - foreach_ptr(NewConstraint, con, tab->constraints) - { - switch (con->contype) - { - case CONSTR_CHECK: - - /* - * We already expanded virtual expression in - * createTableConstraints. - */ - con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate); - break; - case CONSTR_NOTNULL: - /* Nothing to do here. */ - break; - default: - elog(ERROR, "unrecognized constraint type: %d", - (int) con->contype); - } - } - - /* Expression already planned in createTableConstraints */ - foreach_ptr(NewColumnValue, ex, tab->newvals) - ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL); -} - -/* - * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated - * expressions for "tab" (AlteredTableInfo structure) whose inputs come from - * the new tuple (insertslot) of the new partition (newPartRel). - */ -static void -evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab, - Relation newPartRel, - TupleTableSlot *insertslot, - ExprContext *econtext) -{ - econtext->ecxt_scantuple = insertslot; - - foreach_ptr(NewColumnValue, ex, tab->newvals) - { - if (!ex->is_generated) - continue; - - insertslot->tts_values[ex->attnum - 1] - = ExecEvalExpr(ex->exprstate, - econtext, - &insertslot->tts_isnull[ex->attnum - 1]); - } - - foreach_ptr(NewConstraint, con, tab->constraints) - { - switch (con->contype) - { - case CONSTR_CHECK: - if (!ExecCheck(con->qualstate, econtext)) - ereport(ERROR, - errcode(ERRCODE_CHECK_VIOLATION), - errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row", - con->name, RelationGetRelationName(newPartRel)), - errtableconstraint(newPartRel, con->name)); - break; - case CONSTR_NOTNULL: - case CONSTR_FOREIGN: - /* Nothing to do here */ - break; - default: - elog(ERROR, "unrecognized constraint type: %d", - (int) con->contype); - } - } -} - -/* - * getAttributesList: build a list of columns (ColumnDef) based on parent_rel - */ -static List * -getAttributesList(Relation parent_rel) -{ - AttrNumber parent_attno; - TupleDesc modelDesc; - List *colList = NIL; - - modelDesc = RelationGetDescr(parent_rel); - - for (parent_attno = 1; parent_attno <= modelDesc->natts; - parent_attno++) - { - Form_pg_attribute attribute = TupleDescAttr(modelDesc, - parent_attno - 1); - ColumnDef *def; - - /* Ignore dropped columns in the parent. */ - if (attribute->attisdropped) - continue; - - def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, - attribute->atttypmod, attribute->attcollation); - - def->is_not_null = attribute->attnotnull; - - /* Copy identity. */ - def->identity = attribute->attidentity; - - /* Copy attgenerated. */ - def->generated = attribute->attgenerated; - - def->storage = attribute->attstorage; - - /* Likewise, copy compression. */ - if (CompressionMethodIsValid(attribute->attcompression)) - def->compression = - pstrdup(GetCompressionMethodName(attribute->attcompression)); - else - def->compression = NULL; - - /* Add to column list. */ - colList = lappend(colList, def); - } - - return colList; -} - -/* - * createTableConstraints: - * create check constraints, default values, and generated values for newRel - * based on parent_rel. tab is pending-work queue for newRel, we may need it in - * MergePartitionsMoveRows. - */ -static void -createTableConstraints(List **wqueue, AlteredTableInfo *tab, - Relation parent_rel, Relation newRel) -{ - TupleDesc tupleDesc; - TupleConstr *constr; - AttrMap *attmap; - AttrNumber parent_attno; - int ccnum; - List *constraints = NIL; - List *cookedConstraints = NIL; - - tupleDesc = RelationGetDescr(parent_rel); - constr = tupleDesc->constr; - - if (!constr) - return; - - /* - * Construct a map from the parent relation's attnos to the child rel's. - * This re-checks type match, etc, although it shouldn't be possible to - * have a failure since both tables are locked. - */ - attmap = build_attrmap_by_name(RelationGetDescr(newRel), - tupleDesc, - false); - - /* Cycle for default values. */ - for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) - { - Form_pg_attribute attribute = TupleDescAttr(tupleDesc, - parent_attno - 1); - - /* Ignore dropped columns in the parent. */ - if (attribute->attisdropped) - continue; - - /* Copy the default, if present, and it should be copied. */ - if (attribute->atthasdef) - { - Node *this_default = NULL; - bool found_whole_row; - AttrNumber num; - Node *def; - NewColumnValue *newval; - - if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) - this_default = build_generation_expression(parent_rel, attribute->attnum); - else - { - this_default = TupleDescGetDefault(tupleDesc, attribute->attnum); - if (this_default == NULL) - elog(ERROR, "default expression not found for attribute %d of relation \"%s\"", - attribute->attnum, RelationGetRelationName(parent_rel)); - } - - num = attmap->attnums[parent_attno - 1]; - def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row); - - if (found_whole_row && attribute->attgenerated != '\0') - elog(ERROR, "cannot convert whole-row table reference"); - - /* Add a pre-cooked default expression. */ - StoreAttrDefault(newRel, num, def, true); - - /* - * Stored generated column expressions in parent_rel might - * reference the tableoid. newRel, parent_rel tableoid clear is - * not the same. If so, these stored generated columns require - * recomputation for newRel within MergePartitionsMoveRows. - */ - if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED) - { - newval = palloc0_object(NewColumnValue); - newval->attnum = num; - newval->expr = expression_planner((Expr *) def); - newval->is_generated = (attribute->attgenerated != '\0'); - tab->newvals = lappend(tab->newvals, newval); - } - } - } - - /* Cycle for CHECK constraints. */ - for (ccnum = 0; ccnum < constr->num_check; ccnum++) - { - char *ccname = constr->check[ccnum].ccname; - char *ccbin = constr->check[ccnum].ccbin; - bool ccenforced = constr->check[ccnum].ccenforced; - bool ccnoinherit = constr->check[ccnum].ccnoinherit; - bool ccvalid = constr->check[ccnum].ccvalid; - Node *ccbin_node; - bool found_whole_row; - Constraint *constr; - - /* - * The partitioned table can not have a NO INHERIT check constraint - * (see StoreRelCheck function for details). - */ - Assert(!ccnoinherit); - - ccbin_node = map_variable_attnos(stringToNode(ccbin), - 1, 0, - attmap, - InvalidOid, &found_whole_row); - - /* - * For the moment we have to reject whole-row variables (as for CREATE - * TABLE LIKE and inheritances). - */ - if (found_whole_row) - elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".", - ccname, - RelationGetRelationName(parent_rel)); - - constr = makeNode(Constraint); - constr->contype = CONSTR_CHECK; - constr->conname = pstrdup(ccname); - constr->deferrable = false; - constr->initdeferred = false; - constr->is_enforced = ccenforced; - constr->skip_validation = !ccvalid; - constr->initially_valid = ccvalid; - constr->is_no_inherit = ccnoinherit; - constr->raw_expr = NULL; - constr->cooked_expr = nodeToString(ccbin_node); - constr->location = -1; - constraints = lappend(constraints, constr); - } - - /* Install all CHECK constraints. */ - cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, - false, true, true, NULL); - - /* Make the additional catalog changes visible. */ - CommandCounterIncrement(); - - /* - * parent_rel check constraint expression may reference tableoid, so later - * in MergePartitionsMoveRows, we need to evaluate the check constraint - * again for the newRel. We can check whether the check constraint - * contains a tableoid reference via pull_varattnos. - */ - foreach_ptr(CookedConstraint, ccon, cookedConstraints) - { - if (!ccon->skip_validation) - { - Node *qual; - Bitmapset *attnums = NULL; - - Assert(ccon->contype == CONSTR_CHECK); - qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1); - pull_varattnos(qual, 1, &attnums); - - /* - * Add a check only if it contains a tableoid - * (TableOidAttributeNumber). - */ - if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, - attnums)) - { - NewConstraint *newcon; - - newcon = palloc0_object(NewConstraint); - newcon->name = ccon->name; - newcon->contype = CONSTR_CHECK; - newcon->qual = qual; - - tab->constraints = lappend(tab->constraints, newcon); - } - } - } - - /* Don't need the cookedConstraints anymore. */ - list_free_deep(cookedConstraints); - - /* Reproduce not-null constraints. */ - if (constr->has_not_null) - { - List *nnconstraints; - - /* - * The "include_noinh" argument is false because a partitioned table - * can't have NO INHERIT constraint. - */ - nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), - false, false); - - Assert(list_length(nnconstraints) > 0); - - /* - * We already set pg_attribute.attnotnull in createPartitionTable. No - * need call set_attnotnull again. - */ - AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL); - } -} - -/* - * createPartitionTable: - * - * Create a new partition (newPartName) for the partitioned table (parent_rel). - * ownerId is determined by the partition on which the operation is performed, - * so it is passed separately. The new partition will inherit the access method - * and persistence type from the parent table. - * - * Returns the created relation (locked in AccessExclusiveLock mode). - */ -static Relation -createPartitionTable(List **wqueue, RangeVar *newPartName, - Relation parent_rel, Oid ownerId) -{ - Relation newRel; - Oid newRelId; - Oid existingRelid; - TupleDesc descriptor; - List *colList = NIL; - Oid relamId; - Oid namespaceId; - AlteredTableInfo *new_partrel_tab; - Form_pg_class parent_relform = parent_rel->rd_rel; - - /* If the existing rel is temp, it must belong to this session. */ - if (RELATION_IS_OTHER_TEMP(parent_rel)) - ereport(ERROR, - errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot create as partition of temporary relation of another session")); - - /* Look up inheritance ancestors and generate the relation schema. */ - colList = getAttributesList(parent_rel); - - /* Create a tuple descriptor from the relation schema. */ - descriptor = BuildDescForRelation(colList); - - /* Look up the access method for the new relation. */ - relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID; - - /* Look up the namespace in which we are supposed to create the relation. */ - namespaceId = - RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid); - if (OidIsValid(existingRelid)) - ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", newPartName->relname)); - - /* - * We intended to create the partition with the same persistence as the - * parent table, but we still need to recheck because that might be - * affected by the search_path. If the parent is permanent, so must be - * all of its partitions. - */ - if (parent_relform->relpersistence != RELPERSISTENCE_TEMP && - newPartName->relpersistence == RELPERSISTENCE_TEMP) - ereport(ERROR, - errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"", - RelationGetRelationName(parent_rel))); - - /* Permanent rels cannot be partitions belonging to a temporary parent. */ - if (newPartName->relpersistence != RELPERSISTENCE_TEMP && - parent_relform->relpersistence == RELPERSISTENCE_TEMP) - ereport(ERROR, - errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"", - RelationGetRelationName(parent_rel))); - - /* Create the relation. */ - newRelId = heap_create_with_catalog(newPartName->relname, - namespaceId, - parent_relform->reltablespace, - InvalidOid, - InvalidOid, - InvalidOid, - ownerId, - relamId, - descriptor, - NIL, - RELKIND_RELATION, - newPartName->relpersistence, - false, - false, - ONCOMMIT_NOOP, - (Datum) 0, - true, - allowSystemTableMods, - true, - InvalidOid, - NULL); - - /* - * We must bump the command counter to make the newly-created relation - * tuple visible for opening. - */ - CommandCounterIncrement(); - - /* - * Open the new partition with no lock, because we already have an - * AccessExclusiveLock placed there after creation. - */ - newRel = table_open(newRelId, NoLock); - - /* Find or create a work queue entry for the newly created table. */ - new_partrel_tab = ATGetQueueEntry(wqueue, newRel); - - /* Create constraints, default values, and generated values. */ - createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel); - - /* - * Need to call CommandCounterIncrement, so a fresh relcache entry has - * newly installed constraint info. - */ - CommandCounterIncrement(); - - return newRel; -} - -/* - * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) - * of the partitioned table and move rows into the new partition - * (newPartRel). We also verify check constraints against these rows. - */ -static void -MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel) -{ - CommandId mycid; - EState *estate; - AlteredTableInfo *tab; - ListCell *ltab; - - /* The FSM is empty, so don't bother using it. */ - int ti_options = TABLE_INSERT_SKIP_FSM; - BulkInsertState bistate; /* state of bulk inserts for partition */ - TupleTableSlot *dstslot; - - /* Find the work queue entry for the new partition table: newPartRel. */ - tab = ATGetQueueEntry(wqueue, newPartRel); - - /* Generate the constraint and default execution states. */ - estate = CreateExecutorState(); - - buildExpressionExecutionStates(tab, newPartRel, estate); - - mycid = GetCurrentCommandId(true); - - /* Prepare a BulkInsertState for table_tuple_insert. */ - bistate = GetBulkInsertState(); - - /* Create the necessary tuple slot. */ - dstslot = table_slot_create(newPartRel, NULL); - - foreach_oid(merging_oid, mergingPartitions) - { - ExprContext *econtext; - TupleTableSlot *srcslot; - TupleConversionMap *tuple_map; - TableScanDesc scan; - MemoryContext oldCxt; - Snapshot snapshot; - Relation mergingPartition; - - econtext = GetPerTupleExprContext(estate); - - /* - * Partition is already locked in the transformPartitionCmdForMerge - * function. - */ - mergingPartition = table_open(merging_oid, NoLock); - - /* Create a source tuple slot for the partition being merged. */ - srcslot = table_slot_create(mergingPartition, NULL); - - /* - * Map computing for moving attributes of the merged partition to the - * new partition. - */ - tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition), - RelationGetDescr(newPartRel)); - - /* Scan through the rows. */ - snapshot = RegisterSnapshot(GetLatestSnapshot()); - scan = table_beginscan(mergingPartition, snapshot, 0, NULL); - - /* - * Switch to per-tuple memory context and reset it for each tuple - * produced, so we don't leak memory. - */ - oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); - - while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) - { - TupleTableSlot *insertslot; - - CHECK_FOR_INTERRUPTS(); - - if (tuple_map) - { - /* Need to use a map to copy attributes. */ - insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot); - } - else - { - slot_getallattrs(srcslot); - - /* Copy attributes directly. */ - insertslot = dstslot; - - ExecClearTuple(insertslot); - - memcpy(insertslot->tts_values, srcslot->tts_values, - sizeof(Datum) * srcslot->tts_nvalid); - memcpy(insertslot->tts_isnull, srcslot->tts_isnull, - sizeof(bool) * srcslot->tts_nvalid); - - ExecStoreVirtualTuple(insertslot); - } - - /* - * Constraints and GENERATED expressions might reference the - * tableoid column, so fill tts_tableOid with the desired value. - * (We must do this each time, because it gets overwritten with - * newrel's OID during storing.) - */ - insertslot->tts_tableOid = RelationGetRelid(newPartRel); - - /* - * Now, evaluate any generated expressions whose inputs come from - * the new tuple. We assume these columns won't reference each - * other, so that there's no ordering dependency. - */ - evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel, - insertslot, econtext); - - /* Write the tuple out to the new relation. */ - table_tuple_insert(newPartRel, insertslot, mycid, - ti_options, bistate); - - ResetExprContext(econtext); - } - - MemoryContextSwitchTo(oldCxt); - table_endscan(scan); - UnregisterSnapshot(snapshot); - - if (tuple_map) - free_conversion_map(tuple_map); - - ExecDropSingleTupleTableSlot(srcslot); - table_close(mergingPartition, NoLock); - } - - FreeExecutorState(estate); - ExecDropSingleTupleTableSlot(dstslot); - FreeBulkInsertState(bistate); - - table_finish_bulk_insert(newPartRel, ti_options); - - /* - * We don't need to process this newPartRel since we already processed it - * here, so delete the ALTER TABLE queue for it. - */ - foreach(ltab, *wqueue) - { - tab = (AlteredTableInfo *) lfirst(ltab); - if (tab->relid == RelationGetRelid(newPartRel)) - { - *wqueue = list_delete_cell(*wqueue, ltab); - break; - } - } -} - -/* - * detachPartitionTable: detach partition "child_rel" from partitioned table - * "parent_rel" with default partition identifier "defaultPartOid" - */ -static void -detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid) -{ - /* Remove the pg_inherits row first. */ - RemoveInheritance(child_rel, parent_rel, false); - - /* - * Detaching the partition might involve TOAST table access, so ensure we - * have a valid snapshot. - */ - PushActiveSnapshot(GetTransactionSnapshot()); - - /* Do the final part of detaching. */ - DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid); - - PopActiveSnapshot(); -} - -/* - * ALTER TABLE MERGE PARTITIONS INTO - */ -static void -ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, - PartitionCmd *cmd, AlterTableUtilityContext *context) -{ - Relation newPartRel; - List *mergingPartitions = NIL; - Oid defaultPartOid; - Oid existingRelid; - Oid ownerId = InvalidOid; - Oid save_userid; - int save_sec_context; - int save_nestlevel; - - /* - * Check ownership of merged partitions - partitions with different owners - * cannot be merged. Also, collect the OIDs of these partitions during the - * check. - */ - foreach_node(RangeVar, name, cmd->partlist) - { - Relation mergingPartition; - - /* - * We are going to detach and remove this partition. We already took - * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here, - * NoLock is fine. - */ - mergingPartition = table_openrv_extended(name, NoLock, false); - Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false)); - - if (OidIsValid(ownerId)) - { - /* Do the partitions being merged have different owners? */ - if (ownerId != mergingPartition->rd_rel->relowner) - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("partitions being merged have different owners")); - } - else - ownerId = mergingPartition->rd_rel->relowner; - - /* Store the next merging partition into the list. */ - mergingPartitions = lappend_oid(mergingPartitions, - RelationGetRelid(mergingPartition)); - - table_close(mergingPartition, NoLock); - } - - /* Look up the existing relation by the new partition name. */ - RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid); - - /* - * Check if this name is already taken. This helps us to detect the - * situation when one of the merging partitions has the same name as the - * new partition. Otherwise, this would fail later on anyway, but - * catching this here allows us to emit a nicer error message. - */ - if (OidIsValid(existingRelid)) - { - if (list_member_oid(mergingPartitions, existingRelid)) - { - /* - * The new partition has the same name as one of the merging - * partitions. - */ - char tmpRelName[NAMEDATALEN]; - - /* Generate a temporary name. */ - sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); - - /* - * Rename the existing partition with a temporary name, leaving it - * free for the new partition. We don't need to care about this - * in the future because we're going to eventually drop the - * existing partition anyway. - */ - RenameRelationInternal(existingRelid, tmpRelName, true, false); - - /* - * We must bump the command counter to make the new partition - * tuple visible for rename. - */ - CommandCounterIncrement(); - } - else - { - ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", cmd->name->relname)); - } - } - - defaultPartOid = - get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - - /* Detach all merging partitions. */ - foreach_oid(mergingPartitionOid, mergingPartitions) - { - Relation child_rel; - - child_rel = table_open(mergingPartitionOid, NoLock); - - detachPartitionTable(rel, child_rel, defaultPartOid); - - table_close(child_rel, NoLock); - } - - /* - * Perform a preliminary check to determine whether it's safe to drop all - * merging partitions before we actually do so later. After merging rows - * into the new partitions via MergePartitionsMoveRows, all old partitions - * need to be dropped. However, since the drop behavior is DROP_RESTRICT - * and the merge process (MergePartitionsMoveRows) can be time-consuming, - * performing an early check on the drop eligibility of old partitions is - * preferable. - */ - foreach_oid(mergingPartitionOid, mergingPartitions) - { - ObjectAddress object; - - /* Get oid of the later to be dropped relation. */ - object.objectId = mergingPartitionOid; - object.classId = RelationRelationId; - object.objectSubId = 0; - - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); - } - - /* - * Create a table for the new partition, using the partitioned table as a - * model. - */ - Assert(OidIsValid(ownerId)); - newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); - - /* - * Switch to the table owner's userid, so that any index functions are run - * as that user. Also, lockdown security-restricted operations and - * arrange to make GUC variable changes local to this command. - * - * Need to do it after determining the namespace in the - * createPartitionTable() call. - */ - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(ownerId, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - RestrictSearchPath(); - - /* Copy data from merged partitions to the new partition. */ - MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel); - - /* Drop the current partitions before attaching the new one. */ - foreach_oid(mergingPartitionOid, mergingPartitions) - { - ObjectAddress object; - - object.objectId = mergingPartitionOid; - object.classId = RelationRelationId; - object.objectSubId = 0; - - performDeletion(&object, DROP_RESTRICT, 0); - } - - list_free(mergingPartitions); - - /* - * Attach a new partition to the partitioned table. wqueue = NULL: - * verification for each cloned constraint is not needed. - */ - attachPartitionTable(NULL, rel, newPartRel, cmd->bound); - - /* Keep the lock until commit. */ - table_close(newPartRel, NoLock); - - /* Roll back any GUC changes executed by index functions. */ - AtEOXact_GUC(false, save_nestlevel); - - /* Restore the userid and security context. */ - SetUserIdAndSecContext(save_userid, save_sec_context); -} - -/* - * Struct with the context of the new partition for inserting rows from the - * split partition. - */ -typedef struct SplitPartitionContext -{ - ExprState *partqualstate; /* expression for checking a slot for a - * partition (NULL for DEFAULT partition) */ - BulkInsertState bistate; /* state of bulk inserts for partition */ - TupleTableSlot *dstslot; /* slot for inserting row into partition */ - AlteredTableInfo *tab; /* structure with generated column expressions - * and check constraint expressions. */ - Relation partRel; /* relation for partition */ -} SplitPartitionContext; - -/* - * createSplitPartitionContext: create context for partition and fill it - */ -static SplitPartitionContext * -createSplitPartitionContext(Relation partRel) -{ - SplitPartitionContext *pc; - - pc = palloc0_object(SplitPartitionContext); - pc->partRel = partRel; - - /* - * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so - * don't bother using it. - */ - pc->bistate = GetBulkInsertState(); - - /* Create a destination tuple slot for the new partition. */ - pc->dstslot = table_slot_create(pc->partRel, NULL); - - return pc; -} - -/* - * deleteSplitPartitionContext: delete context for partition - */ -static void -deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options) -{ - ListCell *ltab; - - ExecDropSingleTupleTableSlot(pc->dstslot); - FreeBulkInsertState(pc->bistate); - - table_finish_bulk_insert(pc->partRel, ti_options); - - /* - * We don't need to process this pc->partRel so delete the ALTER TABLE - * queue of it. - */ - foreach(ltab, *wqueue) - { - AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); - - if (tab->relid == RelationGetRelid(pc->partRel)) - { - *wqueue = list_delete_cell(*wqueue, ltab); - break; - } - } - - pfree(pc); -} - -/* - * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table - * (rel) and move rows into new partitions. - * - * New partitions description: - * partlist: list of pointers to SinglePartitionSpec structures. It contains - * the partition specification details for all new partitions. - * newPartRels: list of Relations, new partitions created in - * ATExecSplitPartition. - */ -static void -SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel, - List *partlist, List *newPartRels) -{ - /* The FSM is empty, so don't bother using it. */ - int ti_options = TABLE_INSERT_SKIP_FSM; - CommandId mycid; - EState *estate; - ListCell *listptr, - *listptr2; - TupleTableSlot *srcslot; - ExprContext *econtext; - TableScanDesc scan; - Snapshot snapshot; - MemoryContext oldCxt; - List *partContexts = NIL; - TupleConversionMap *tuple_map; - SplitPartitionContext *defaultPartCtx = NULL, - *pc; - - mycid = GetCurrentCommandId(true); - - estate = CreateExecutorState(); - - forboth(listptr, partlist, listptr2, newPartRels) - { - SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); - - pc = createSplitPartitionContext((Relation) lfirst(listptr2)); - - /* Find the work queue entry for the new partition table: newPartRel. */ - pc->tab = ATGetQueueEntry(wqueue, pc->partRel); - - buildExpressionExecutionStates(pc->tab, pc->partRel, estate); - - if (sps->bound->is_default) - { - /* - * We should not create a structure to check the partition - * constraint for the new DEFAULT partition. - */ - defaultPartCtx = pc; - } - else - { - List *partConstraint; - - /* Build expression execution states for partition check quals. */ - partConstraint = get_qual_from_partbound(rel, sps->bound); - partConstraint = - (List *) eval_const_expressions(NULL, - (Node *) partConstraint); - /* Make a boolean expression for ExecCheck(). */ - partConstraint = list_make1(make_ands_explicit(partConstraint)); - - /* - * Map the vars in the constraint expression from rel's attnos to - * splitRel's. - */ - partConstraint = map_partition_varattnos(partConstraint, - 1, splitRel, rel); - - pc->partqualstate = - ExecPrepareExpr((Expr *) linitial(partConstraint), estate); - Assert(pc->partqualstate != NULL); - } - - /* Store partition context into a list. */ - partContexts = lappend(partContexts, pc); - } - - econtext = GetPerTupleExprContext(estate); - - /* Create the necessary tuple slot. */ - srcslot = table_slot_create(splitRel, NULL); - - /* - * Map computing for moving attributes of the split partition to the new - * partition (for the first new partition, but other new partitions can - * use the same map). - */ - pc = (SplitPartitionContext *) lfirst(list_head(partContexts)); - tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel), - RelationGetDescr(pc->partRel)); - - /* Scan through the rows. */ - snapshot = RegisterSnapshot(GetLatestSnapshot()); - scan = table_beginscan(splitRel, snapshot, 0, NULL); - - /* - * Switch to per-tuple memory context and reset it for each tuple - * produced, so we don't leak memory. - */ - oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); - - while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) - { - bool found = false; - TupleTableSlot *insertslot; - - CHECK_FOR_INTERRUPTS(); - - econtext->ecxt_scantuple = srcslot; - - /* Search partition for the current slot, srcslot. */ - foreach(listptr, partContexts) - { - pc = (SplitPartitionContext *) lfirst(listptr); - - /* skip DEFAULT partition */ - if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext)) - { - found = true; - break; - } - } - if (!found) - { - /* Use the DEFAULT partition if it exists. */ - if (defaultPartCtx) - pc = defaultPartCtx; - else - ereport(ERROR, - errcode(ERRCODE_CHECK_VIOLATION), - errmsg("can not find partition for split partition row"), - errtable(splitRel)); - } - - if (tuple_map) - { - /* Need to use a map to copy attributes. */ - insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot); - } - else - { - /* Extract data from the old tuple. */ - slot_getallattrs(srcslot); - - /* Copy attributes directly. */ - insertslot = pc->dstslot; - - ExecClearTuple(insertslot); - - memcpy(insertslot->tts_values, srcslot->tts_values, - sizeof(Datum) * srcslot->tts_nvalid); - memcpy(insertslot->tts_isnull, srcslot->tts_isnull, - sizeof(bool) * srcslot->tts_nvalid); - - ExecStoreVirtualTuple(insertslot); - } - - /* - * Constraints and GENERATED expressions might reference the tableoid - * column, so fill tts_tableOid with the desired value. (We must do - * this each time, because it gets overwritten with newrel's OID - * during storing.) - */ - insertslot->tts_tableOid = RelationGetRelid(pc->partRel); - - /* - * Now, evaluate any generated expressions whose inputs come from the - * new tuple. We assume these columns won't reference each other, so - * that there's no ordering dependency. - */ - evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel, - insertslot, econtext); - - /* Write the tuple out to the new relation. */ - table_tuple_insert(pc->partRel, insertslot, mycid, - ti_options, pc->bistate); - - ResetExprContext(econtext); - } - - MemoryContextSwitchTo(oldCxt); - - table_endscan(scan); - UnregisterSnapshot(snapshot); - - if (tuple_map) - free_conversion_map(tuple_map); - - ExecDropSingleTupleTableSlot(srcslot); - - FreeExecutorState(estate); - - foreach_ptr(SplitPartitionContext, spc, partContexts) - deleteSplitPartitionContext(spc, wqueue, ti_options); -} - -/* - * ALTER TABLE SPLIT PARTITION INTO - */ -static void -ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, - PartitionCmd *cmd, AlterTableUtilityContext *context) -{ - Relation splitRel; - Oid splitRelOid; - ListCell *listptr, - *listptr2; - bool isSameName = false; - char tmpRelName[NAMEDATALEN]; - List *newPartRels = NIL; - ObjectAddress object; - Oid defaultPartOid; - Oid save_userid; - int save_sec_context; - int save_nestlevel; - - defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); - - /* - * Partition is already locked in the transformPartitionCmdForSplit - * function. - */ - splitRel = table_openrv(cmd->name, NoLock); - - splitRelOid = RelationGetRelid(splitRel); - - /* Check descriptions of new partitions. */ - foreach_node(SinglePartitionSpec, sps, cmd->partlist) - { - Oid existingRelid; - - /* Look up the existing relation by the new partition name. */ - RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid); - - /* - * This would fail later on anyway if the relation already exists. But - * by catching it here, we can emit a nicer error message. - */ - if (existingRelid == splitRelOid && !isSameName) - /* One new partition can have the same name as a split partition. */ - isSameName = true; - else if (OidIsValid(existingRelid)) - ereport(ERROR, - errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("relation \"%s\" already exists", sps->name->relname)); - } - - /* Detach the split partition. */ - detachPartitionTable(rel, splitRel, defaultPartOid); - - /* - * Perform a preliminary check to determine whether it's safe to drop the - * split partition before we actually do so later. After merging rows into - * the new partitions via SplitPartitionMoveRows, all old partitions need - * to be dropped. However, since the drop behavior is DROP_RESTRICT and - * the merge process (SplitPartitionMoveRows) can be time-consuming, - * performing an early check on the drop eligibility of old partitions is - * preferable. - */ - object.objectId = splitRelOid; - object.classId = RelationRelationId; - object.objectSubId = 0; - performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); - - /* - * If a new partition has the same name as the split partition, then we - * should rename the split partition to reuse its name. - */ - if (isSameName) - { - /* - * We must bump the command counter to make the split partition tuple - * visible for renaming. - */ - CommandCounterIncrement(); - /* Rename partition. */ - sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); - RenameRelationInternal(splitRelOid, tmpRelName, true, false); - - /* - * We must bump the command counter to make the split partition tuple - * visible after renaming. - */ - CommandCounterIncrement(); - } - - /* Create new partitions (like a split partition), without indexes. */ - foreach_node(SinglePartitionSpec, sps, cmd->partlist) - { - Relation newPartRel; - - newPartRel = createPartitionTable(wqueue, sps->name, rel, - splitRel->rd_rel->relowner); - newPartRels = lappend(newPartRels, newPartRel); - } - - /* - * Switch to the table owner's userid, so that any index functions are run - * as that user. Also, lockdown security-restricted operations and - * arrange to make GUC variable changes local to this command. - * - * Need to do it after determining the namespace in the - * createPartitionTable() call. - */ - GetUserIdAndSecContext(&save_userid, &save_sec_context); - SetUserIdAndSecContext(splitRel->rd_rel->relowner, - save_sec_context | SECURITY_RESTRICTED_OPERATION); - save_nestlevel = NewGUCNestLevel(); - RestrictSearchPath(); - - /* Copy data from the split partition to the new partitions. */ - SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels); - /* Keep the lock until commit. */ - table_close(splitRel, NoLock); - - /* Attach new partitions to the partitioned table. */ - forboth(listptr, cmd->partlist, listptr2, newPartRels) - { - SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); - Relation newPartRel = (Relation) lfirst(listptr2); - - /* - * wqueue = NULL: verification for each cloned constraint is not - * needed. - */ - attachPartitionTable(NULL, rel, newPartRel, sps->bound); - /* Keep the lock until commit. */ - table_close(newPartRel, NoLock); - } - - /* Drop the split partition. */ - object.classId = RelationRelationId; - object.objectId = splitRelOid; - object.objectSubId = 0; - /* Probably DROP_CASCADE is not needed. */ - performDeletion(&object, DROP_RESTRICT, 0); - - /* Roll back any GUC changes executed by index functions. */ - AtEOXact_GUC(false, save_nestlevel); - - /* Restore the userid and security context. */ - SetUserIdAndSecContext(save_userid, save_sec_context); -} diff --git a/src/backend/commands/tablecmds_partition.c b/src/backend/commands/tablecmds_partition.c new file mode 100644 index 00000000000..1e8e7f5b669 --- /dev/null +++ b/src/backend/commands/tablecmds_partition.c @@ -0,0 +1,4627 @@ +/*------------------------------------------------------------------------- + * + * tablecmds_partition.c + * TODO + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/commands/tablecmds_partition.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "access/toast_compression.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.h" +#include "catalog/pg_attrdef.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_inherits.h" +#include "catalog/pg_trigger.h" +#include "commands/defrem.h" +#include "commands/tablecmds.h" +#include "commands/tablecmds_internal.h" +#include "commands/tablecmds_partition.h" +#include "commands/trigger.h" +#include "executor/executor.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 "rewrite/rewriteHandler.h" +#include "rewrite/rewriteManip.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" + +static void CloneFkReferenced(Relation parentRel, Relation partitionRel); +static void CloneFkReferencing(List **wqueue, Relation parentRel, + Relation partRel); +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 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 AttachPartitionEnsureIndexes(List **wqueue, Relation rel, Relation attachrel); +static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel, + List *partConstraint, + bool validate_default); +static void DropClonedTriggersFromPartition(Oid partitionId); +static void DetachPartitionFinalize(Relation rel, Relation partRel, + bool concurrent, Oid defaultPartOid); +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); + +/* + * Obtain list of partitions of the given table, locking them all at the given + * lockmode and ensuring that they all pass CheckAlterTableIsSafe. + * + * This function is a no-op if the given relation is not a partitioned table; + * in particular, nothing is done if it's a legacy inheritance parent. + */ +void +ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode) +{ + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + List *inh; + ListCell *cell; + + inh = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL); + /* first element is the parent rel; must ignore it */ + for_each_from(cell, inh, 1) + { + Relation childrel; + + /* find_all_inheritors already got lock */ + childrel = table_open(lfirst_oid(cell), NoLock); + CheckAlterTableIsSafe(childrel); + table_close(childrel, NoLock); + } + list_free(inh); + } +} + +/* + * 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); +} + +/* + * 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. + */ +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); +} + +/* + * 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); +} + +/* + * 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 */ + } + } +} + +/* + * attachPartitionTable: attach a new partition to the partitioned table + * + * wqueue: the ALTER TABLE work queue; can be NULL when not running as part + * of an ALTER TABLE sequence. + * rel: partitioned relation; + * attachrel: relation of attached partition; + * bound: bounds of attached relation. + */ +static void +attachPartitionTable(List **wqueue, Relation rel, Relation attachrel, PartitionBoundSpec *bound) +{ + /* + * Create an inheritance; the relevant checks are performed inside the + * function. + */ + CreateInheritance(attachrel, rel, true); + + /* Update the pg_class entry. */ + StorePartitionBound(attachrel, rel, 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); +} + +/* + * 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); + + attachPartitionTable(wqueue, rel, attachrel, cmd->bound); + + /* + * Generate a 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_array(Relation, list_length(attachRelIdxs)); + attachInfos = palloc_array(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(NULL, + 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 + */ +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). + */ +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. + */ +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 + */ +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); + } +} + +/* + * buildExpressionExecutionStates: build the needed expression execution states + * for new partition (newPartRel) checks and initialize expressions for + * generated columns. All expressions should be created in "tab" + * (AlteredTableInfo structure). + */ +static void +buildExpressionExecutionStates(AlteredTableInfo *tab, Relation newPartRel, EState *estate) +{ + /* + * Build the needed expression execution states. Here, we expect only NOT + * NULL and CHECK constraint. + */ + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + + /* + * We already expanded virtual expression in + * createTableConstraints. + */ + con->qualstate = ExecPrepareExpr((Expr *) con->qual, estate); + break; + case CONSTR_NOTNULL: + /* Nothing to do here. */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } + + /* Expression already planned in createTableConstraints */ + foreach_ptr(NewColumnValue, ex, tab->newvals) + ex->exprstate = ExecInitExpr((Expr *) ex->expr, NULL); +} + +/* + * evaluateGeneratedExpressionsAndCheckConstraints: evaluate any generated + * expressions for "tab" (AlteredTableInfo structure) whose inputs come from + * the new tuple (insertslot) of the new partition (newPartRel). + */ +static void +evaluateGeneratedExpressionsAndCheckConstraints(AlteredTableInfo *tab, + Relation newPartRel, + TupleTableSlot *insertslot, + ExprContext *econtext) +{ + econtext->ecxt_scantuple = insertslot; + + foreach_ptr(NewColumnValue, ex, tab->newvals) + { + if (!ex->is_generated) + continue; + + insertslot->tts_values[ex->attnum - 1] + = ExecEvalExpr(ex->exprstate, + econtext, + &insertslot->tts_isnull[ex->attnum - 1]); + } + + foreach_ptr(NewConstraint, con, tab->constraints) + { + switch (con->contype) + { + case CONSTR_CHECK: + if (!ExecCheck(con->qualstate, econtext)) + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("check constraint \"%s\" of relation \"%s\" is violated by some row", + con->name, RelationGetRelationName(newPartRel)), + errtableconstraint(newPartRel, con->name)); + break; + case CONSTR_NOTNULL: + case CONSTR_FOREIGN: + /* Nothing to do here */ + break; + default: + elog(ERROR, "unrecognized constraint type: %d", + (int) con->contype); + } + } +} + +/* + * getAttributesList: build a list of columns (ColumnDef) based on parent_rel + */ +static List * +getAttributesList(Relation parent_rel) +{ + AttrNumber parent_attno; + TupleDesc modelDesc; + List *colList = NIL; + + modelDesc = RelationGetDescr(parent_rel); + + for (parent_attno = 1; parent_attno <= modelDesc->natts; + parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(modelDesc, + parent_attno - 1); + ColumnDef *def; + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + def = makeColumnDef(NameStr(attribute->attname), attribute->atttypid, + attribute->atttypmod, attribute->attcollation); + + def->is_not_null = attribute->attnotnull; + + /* Copy identity. */ + def->identity = attribute->attidentity; + + /* Copy attgenerated. */ + def->generated = attribute->attgenerated; + + def->storage = attribute->attstorage; + + /* Likewise, copy compression. */ + if (CompressionMethodIsValid(attribute->attcompression)) + def->compression = + pstrdup(GetCompressionMethodName(attribute->attcompression)); + else + def->compression = NULL; + + /* Add to column list. */ + colList = lappend(colList, def); + } + + return colList; +} + +/* + * createTableConstraints: + * create check constraints, default values, and generated values for newRel + * based on parent_rel. tab is pending-work queue for newRel, we may need it in + * MergePartitionsMoveRows. + */ +static void +createTableConstraints(List **wqueue, AlteredTableInfo *tab, + Relation parent_rel, Relation newRel) +{ + TupleDesc tupleDesc; + TupleConstr *constr; + AttrMap *attmap; + AttrNumber parent_attno; + int ccnum; + List *constraints = NIL; + List *cookedConstraints = NIL; + + tupleDesc = RelationGetDescr(parent_rel); + constr = tupleDesc->constr; + + if (!constr) + return; + + /* + * Construct a map from the parent relation's attnos to the child rel's. + * This re-checks type match, etc, although it shouldn't be possible to + * have a failure since both tables are locked. + */ + attmap = build_attrmap_by_name(RelationGetDescr(newRel), + tupleDesc, + false); + + /* Cycle for default values. */ + for (parent_attno = 1; parent_attno <= tupleDesc->natts; parent_attno++) + { + Form_pg_attribute attribute = TupleDescAttr(tupleDesc, + parent_attno - 1); + + /* Ignore dropped columns in the parent. */ + if (attribute->attisdropped) + continue; + + /* Copy the default, if present, and it should be copied. */ + if (attribute->atthasdef) + { + Node *this_default = NULL; + bool found_whole_row; + AttrNumber num; + Node *def; + NewColumnValue *newval; + + if (attribute->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL) + this_default = build_generation_expression(parent_rel, attribute->attnum); + else + { + this_default = TupleDescGetDefault(tupleDesc, attribute->attnum); + if (this_default == NULL) + elog(ERROR, "default expression not found for attribute %d of relation \"%s\"", + attribute->attnum, RelationGetRelationName(parent_rel)); + } + + num = attmap->attnums[parent_attno - 1]; + def = map_variable_attnos(this_default, 1, 0, attmap, InvalidOid, &found_whole_row); + + if (found_whole_row && attribute->attgenerated != '\0') + elog(ERROR, "cannot convert whole-row table reference"); + + /* Add a pre-cooked default expression. */ + StoreAttrDefault(newRel, num, def, true); + + /* + * Stored generated column expressions in parent_rel might + * reference the tableoid. newRel, parent_rel tableoid clear is + * not the same. If so, these stored generated columns require + * recomputation for newRel within MergePartitionsMoveRows. + */ + if (attribute->attgenerated == ATTRIBUTE_GENERATED_STORED) + { + newval = palloc0_object(NewColumnValue); + newval->attnum = num; + newval->expr = expression_planner((Expr *) def); + newval->is_generated = (attribute->attgenerated != '\0'); + tab->newvals = lappend(tab->newvals, newval); + } + } + } + + /* Cycle for CHECK constraints. */ + for (ccnum = 0; ccnum < constr->num_check; ccnum++) + { + char *ccname = constr->check[ccnum].ccname; + char *ccbin = constr->check[ccnum].ccbin; + bool ccenforced = constr->check[ccnum].ccenforced; + bool ccnoinherit = constr->check[ccnum].ccnoinherit; + bool ccvalid = constr->check[ccnum].ccvalid; + Node *ccbin_node; + bool found_whole_row; + Constraint *constr; + + /* + * The partitioned table can not have a NO INHERIT check constraint + * (see StoreRelCheck function for details). + */ + Assert(!ccnoinherit); + + ccbin_node = map_variable_attnos(stringToNode(ccbin), + 1, 0, + attmap, + InvalidOid, &found_whole_row); + + /* + * For the moment we have to reject whole-row variables (as for CREATE + * TABLE LIKE and inheritances). + */ + if (found_whole_row) + elog(ERROR, "Constraint \"%s\" contains a whole-row reference to table \"%s\".", + ccname, + RelationGetRelationName(parent_rel)); + + constr = makeNode(Constraint); + constr->contype = CONSTR_CHECK; + constr->conname = pstrdup(ccname); + constr->deferrable = false; + constr->initdeferred = false; + constr->is_enforced = ccenforced; + constr->skip_validation = !ccvalid; + constr->initially_valid = ccvalid; + constr->is_no_inherit = ccnoinherit; + constr->raw_expr = NULL; + constr->cooked_expr = nodeToString(ccbin_node); + constr->location = -1; + constraints = lappend(constraints, constr); + } + + /* Install all CHECK constraints. */ + cookedConstraints = AddRelationNewConstraints(newRel, NIL, constraints, + false, true, true, NULL); + + /* Make the additional catalog changes visible. */ + CommandCounterIncrement(); + + /* + * parent_rel check constraint expression may reference tableoid, so later + * in MergePartitionsMoveRows, we need to evaluate the check constraint + * again for the newRel. We can check whether the check constraint + * contains a tableoid reference via pull_varattnos. + */ + foreach_ptr(CookedConstraint, ccon, cookedConstraints) + { + if (!ccon->skip_validation) + { + Node *qual; + Bitmapset *attnums = NULL; + + Assert(ccon->contype == CONSTR_CHECK); + qual = expand_generated_columns_in_expr(ccon->expr, newRel, 1); + pull_varattnos(qual, 1, &attnums); + + /* + * Add a check only if it contains a tableoid + * (TableOidAttributeNumber). + */ + if (bms_is_member(TableOidAttributeNumber - FirstLowInvalidHeapAttributeNumber, + attnums)) + { + NewConstraint *newcon; + + newcon = palloc0_object(NewConstraint); + newcon->name = ccon->name; + newcon->contype = CONSTR_CHECK; + newcon->qual = qual; + + tab->constraints = lappend(tab->constraints, newcon); + } + } + } + + /* Don't need the cookedConstraints anymore. */ + list_free_deep(cookedConstraints); + + /* Reproduce not-null constraints. */ + if (constr->has_not_null) + { + List *nnconstraints; + + /* + * The "include_noinh" argument is false because a partitioned table + * can't have NO INHERIT constraint. + */ + nnconstraints = RelationGetNotNullConstraints(RelationGetRelid(parent_rel), + false, false); + + Assert(list_length(nnconstraints) > 0); + + /* + * We already set pg_attribute.attnotnull in createPartitionTable. No + * need call set_attnotnull again. + */ + AddRelationNewConstraints(newRel, NIL, nnconstraints, false, true, true, NULL); + } +} + +/* + * createPartitionTable: + * + * Create a new partition (newPartName) for the partitioned table (parent_rel). + * ownerId is determined by the partition on which the operation is performed, + * so it is passed separately. The new partition will inherit the access method + * and persistence type from the parent table. + * + * Returns the created relation (locked in AccessExclusiveLock mode). + */ +static Relation +createPartitionTable(List **wqueue, RangeVar *newPartName, + Relation parent_rel, Oid ownerId) +{ + Relation newRel; + Oid newRelId; + Oid existingRelid; + TupleDesc descriptor; + List *colList = NIL; + Oid relamId; + Oid namespaceId; + AlteredTableInfo *new_partrel_tab; + Form_pg_class parent_relform = parent_rel->rd_rel; + + /* If the existing rel is temp, it must belong to this session. */ + if (RELATION_IS_OTHER_TEMP(parent_rel)) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create as partition of temporary relation of another session")); + + /* Look up inheritance ancestors and generate the relation schema. */ + colList = getAttributesList(parent_rel); + + /* Create a tuple descriptor from the relation schema. */ + descriptor = BuildDescForRelation(colList); + + /* Look up the access method for the new relation. */ + relamId = (parent_relform->relam != InvalidOid) ? parent_relform->relam : HEAP_TABLE_AM_OID; + + /* Look up the namespace in which we are supposed to create the relation. */ + namespaceId = + RangeVarGetAndCheckCreationNamespace(newPartName, NoLock, &existingRelid); + if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", newPartName->relname)); + + /* + * We intended to create the partition with the same persistence as the + * parent table, but we still need to recheck because that might be + * affected by the search_path. If the parent is permanent, so must be + * all of its partitions. + */ + if (parent_relform->relpersistence != RELPERSISTENCE_TEMP && + newPartName->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a temporary relation as partition of permanent relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* Permanent rels cannot be partitions belonging to a temporary parent. */ + if (newPartName->relpersistence != RELPERSISTENCE_TEMP && + parent_relform->relpersistence == RELPERSISTENCE_TEMP) + ereport(ERROR, + errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("cannot create a permanent relation as partition of temporary relation \"%s\"", + RelationGetRelationName(parent_rel))); + + /* Create the relation. */ + newRelId = heap_create_with_catalog(newPartName->relname, + namespaceId, + parent_relform->reltablespace, + InvalidOid, + InvalidOid, + InvalidOid, + ownerId, + relamId, + descriptor, + NIL, + RELKIND_RELATION, + newPartName->relpersistence, + false, + false, + ONCOMMIT_NOOP, + (Datum) 0, + true, + allowSystemTableMods, + true, + InvalidOid, + NULL); + + /* + * We must bump the command counter to make the newly-created relation + * tuple visible for opening. + */ + CommandCounterIncrement(); + + /* + * Open the new partition with no lock, because we already have an + * AccessExclusiveLock placed there after creation. + */ + newRel = table_open(newRelId, NoLock); + + /* Find or create a work queue entry for the newly created table. */ + new_partrel_tab = ATGetQueueEntry(wqueue, newRel); + + /* Create constraints, default values, and generated values. */ + createTableConstraints(wqueue, new_partrel_tab, parent_rel, newRel); + + /* + * Need to call CommandCounterIncrement, so a fresh relcache entry has + * newly installed constraint info. + */ + CommandCounterIncrement(); + + return newRel; +} + +/* + * MergePartitionsMoveRows: scan partitions to be merged (mergingPartitions) + * of the partitioned table and move rows into the new partition + * (newPartRel). We also verify check constraints against these rows. + */ +static void +MergePartitionsMoveRows(List **wqueue, List *mergingPartitions, Relation newPartRel) +{ + CommandId mycid; + EState *estate; + AlteredTableInfo *tab; + ListCell *ltab; + + /* The FSM is empty, so don't bother using it. */ + int ti_options = TABLE_INSERT_SKIP_FSM; + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; + + /* Find the work queue entry for the new partition table: newPartRel. */ + tab = ATGetQueueEntry(wqueue, newPartRel); + + /* Generate the constraint and default execution states. */ + estate = CreateExecutorState(); + + buildExpressionExecutionStates(tab, newPartRel, estate); + + mycid = GetCurrentCommandId(true); + + /* Prepare a BulkInsertState for table_tuple_insert. */ + bistate = GetBulkInsertState(); + + /* Create the necessary tuple slot. */ + dstslot = table_slot_create(newPartRel, NULL); + + foreach_oid(merging_oid, mergingPartitions) + { + ExprContext *econtext; + TupleTableSlot *srcslot; + TupleConversionMap *tuple_map; + TableScanDesc scan; + MemoryContext oldCxt; + Snapshot snapshot; + Relation mergingPartition; + + econtext = GetPerTupleExprContext(estate); + + /* + * Partition is already locked in the transformPartitionCmdForMerge + * function. + */ + mergingPartition = table_open(merging_oid, NoLock); + + /* Create a source tuple slot for the partition being merged. */ + srcslot = table_slot_create(mergingPartition, NULL); + + /* + * Map computing for moving attributes of the merged partition to the + * new partition. + */ + tuple_map = convert_tuples_by_name(RelationGetDescr(mergingPartition), + RelationGetDescr(newPartRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(mergingPartition, snapshot, 0, NULL); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, dstslot); + } + else + { + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the + * tableoid column, so fill tts_tableOid with the desired value. + * (We must do this each time, because it gets overwritten with + * newrel's OID during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(newPartRel); + + /* + * Now, evaluate any generated expressions whose inputs come from + * the new tuple. We assume these columns won't reference each + * other, so that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(tab, newPartRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(newPartRel, insertslot, mycid, + ti_options, bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + table_close(mergingPartition, NoLock); + } + + FreeExecutorState(estate); + ExecDropSingleTupleTableSlot(dstslot); + FreeBulkInsertState(bistate); + + table_finish_bulk_insert(newPartRel, ti_options); + + /* + * We don't need to process this newPartRel since we already processed it + * here, so delete the ALTER TABLE queue for it. + */ + foreach(ltab, *wqueue) + { + tab = (AlteredTableInfo *) lfirst(ltab); + if (tab->relid == RelationGetRelid(newPartRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } +} + +/* + * detachPartitionTable: detach partition "child_rel" from partitioned table + * "parent_rel" with default partition identifier "defaultPartOid" + */ +static void +detachPartitionTable(Relation parent_rel, Relation child_rel, Oid defaultPartOid) +{ + /* Remove the pg_inherits row first. */ + RemoveInheritance(child_rel, parent_rel, false); + + /* + * Detaching the partition might involve TOAST table access, so ensure we + * have a valid snapshot. + */ + PushActiveSnapshot(GetTransactionSnapshot()); + + /* Do the final part of detaching. */ + DetachPartitionFinalize(parent_rel, child_rel, false, defaultPartOid); + + PopActiveSnapshot(); +} + +/* + * ALTER TABLE MERGE PARTITIONS INTO + */ +void +ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) +{ + Relation newPartRel; + List *mergingPartitions = NIL; + Oid defaultPartOid; + Oid existingRelid; + Oid ownerId = InvalidOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + /* + * Check ownership of merged partitions - partitions with different owners + * cannot be merged. Also, collect the OIDs of these partitions during the + * check. + */ + foreach_node(RangeVar, name, cmd->partlist) + { + Relation mergingPartition; + + /* + * We are going to detach and remove this partition. We already took + * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here, + * NoLock is fine. + */ + mergingPartition = table_openrv_extended(name, NoLock, false); + Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false)); + + if (OidIsValid(ownerId)) + { + /* Do the partitions being merged have different owners? */ + if (ownerId != mergingPartition->rd_rel->relowner) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("partitions being merged have different owners")); + } + else + ownerId = mergingPartition->rd_rel->relowner; + + /* Store the next merging partition into the list. */ + mergingPartitions = lappend_oid(mergingPartitions, + RelationGetRelid(mergingPartition)); + + table_close(mergingPartition, NoLock); + } + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(cmd->name, NoLock, &existingRelid); + + /* + * Check if this name is already taken. This helps us to detect the + * situation when one of the merging partitions has the same name as the + * new partition. Otherwise, this would fail later on anyway, but + * catching this here allows us to emit a nicer error message. + */ + if (OidIsValid(existingRelid)) + { + if (list_member_oid(mergingPartitions, existingRelid)) + { + /* + * The new partition has the same name as one of the merging + * partitions. + */ + char tmpRelName[NAMEDATALEN]; + + /* Generate a temporary name. */ + sprintf(tmpRelName, "merge-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + + /* + * Rename the existing partition with a temporary name, leaving it + * free for the new partition. We don't need to care about this + * in the future because we're going to eventually drop the + * existing partition anyway. + */ + RenameRelationInternal(existingRelid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the new partition + * tuple visible for rename. + */ + CommandCounterIncrement(); + } + else + { + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", cmd->name->relname)); + } + } + + defaultPartOid = + get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* Detach all merging partitions. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + Relation child_rel; + + child_rel = table_open(mergingPartitionOid, NoLock); + + detachPartitionTable(rel, child_rel, defaultPartOid); + + table_close(child_rel, NoLock); + } + + /* + * Perform a preliminary check to determine whether it's safe to drop all + * merging partitions before we actually do so later. After merging rows + * into the new partitions via MergePartitionsMoveRows, all old partitions + * need to be dropped. However, since the drop behavior is DROP_RESTRICT + * and the merge process (MergePartitionsMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + /* Get oid of the later to be dropped relation. */ + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + } + + /* + * Create a table for the new partition, using the partitioned table as a + * model. + */ + Assert(OidIsValid(ownerId)); + newPartRel = createPartitionTable(wqueue, cmd->name, rel, ownerId); + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(ownerId, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from merged partitions to the new partition. */ + MergePartitionsMoveRows(wqueue, mergingPartitions, newPartRel); + + /* Drop the current partitions before attaching the new one. */ + foreach_oid(mergingPartitionOid, mergingPartitions) + { + ObjectAddress object; + + object.objectId = mergingPartitionOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + + performDeletion(&object, DROP_RESTRICT, 0); + } + + list_free(mergingPartitions); + + /* + * Attach a new partition to the partitioned table. wqueue = NULL: + * verification for each cloned constraint is not needed. + */ + attachPartitionTable(NULL, rel, newPartRel, cmd->bound); + + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); +} + +/* + * Struct with the context of the new partition for inserting rows from the + * split partition. + */ +typedef struct SplitPartitionContext +{ + ExprState *partqualstate; /* expression for checking a slot for a + * partition (NULL for DEFAULT partition) */ + BulkInsertState bistate; /* state of bulk inserts for partition */ + TupleTableSlot *dstslot; /* slot for inserting row into partition */ + AlteredTableInfo *tab; /* structure with generated column expressions + * and check constraint expressions. */ + Relation partRel; /* relation for partition */ +} SplitPartitionContext; + +/* + * createSplitPartitionContext: create context for partition and fill it + */ +static SplitPartitionContext * +createSplitPartitionContext(Relation partRel) +{ + SplitPartitionContext *pc; + + pc = palloc0_object(SplitPartitionContext); + pc->partRel = partRel; + + /* + * Prepare a BulkInsertState for table_tuple_insert. The FSM is empty, so + * don't bother using it. + */ + pc->bistate = GetBulkInsertState(); + + /* Create a destination tuple slot for the new partition. */ + pc->dstslot = table_slot_create(pc->partRel, NULL); + + return pc; +} + +/* + * deleteSplitPartitionContext: delete context for partition + */ +static void +deleteSplitPartitionContext(SplitPartitionContext *pc, List **wqueue, int ti_options) +{ + ListCell *ltab; + + ExecDropSingleTupleTableSlot(pc->dstslot); + FreeBulkInsertState(pc->bistate); + + table_finish_bulk_insert(pc->partRel, ti_options); + + /* + * We don't need to process this pc->partRel so delete the ALTER TABLE + * queue of it. + */ + foreach(ltab, *wqueue) + { + AlteredTableInfo *tab = (AlteredTableInfo *) lfirst(ltab); + + if (tab->relid == RelationGetRelid(pc->partRel)) + { + *wqueue = list_delete_cell(*wqueue, ltab); + break; + } + } + + pfree(pc); +} + +/* + * SplitPartitionMoveRows: scan split partition (splitRel) of partitioned table + * (rel) and move rows into new partitions. + * + * New partitions description: + * partlist: list of pointers to SinglePartitionSpec structures. It contains + * the partition specification details for all new partitions. + * newPartRels: list of Relations, new partitions created in + * ATExecSplitPartition. + */ +static void +SplitPartitionMoveRows(List **wqueue, Relation rel, Relation splitRel, + List *partlist, List *newPartRels) +{ + /* The FSM is empty, so don't bother using it. */ + int ti_options = TABLE_INSERT_SKIP_FSM; + CommandId mycid; + EState *estate; + ListCell *listptr, + *listptr2; + TupleTableSlot *srcslot; + ExprContext *econtext; + TableScanDesc scan; + Snapshot snapshot; + MemoryContext oldCxt; + List *partContexts = NIL; + TupleConversionMap *tuple_map; + SplitPartitionContext *defaultPartCtx = NULL, + *pc; + + mycid = GetCurrentCommandId(true); + + estate = CreateExecutorState(); + + forboth(listptr, partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + + pc = createSplitPartitionContext((Relation) lfirst(listptr2)); + + /* Find the work queue entry for the new partition table: newPartRel. */ + pc->tab = ATGetQueueEntry(wqueue, pc->partRel); + + buildExpressionExecutionStates(pc->tab, pc->partRel, estate); + + if (sps->bound->is_default) + { + /* + * We should not create a structure to check the partition + * constraint for the new DEFAULT partition. + */ + defaultPartCtx = pc; + } + else + { + List *partConstraint; + + /* Build expression execution states for partition check quals. */ + partConstraint = get_qual_from_partbound(rel, sps->bound); + partConstraint = + (List *) eval_const_expressions(NULL, + (Node *) partConstraint); + /* Make a boolean expression for ExecCheck(). */ + partConstraint = list_make1(make_ands_explicit(partConstraint)); + + /* + * Map the vars in the constraint expression from rel's attnos to + * splitRel's. + */ + partConstraint = map_partition_varattnos(partConstraint, + 1, splitRel, rel); + + pc->partqualstate = + ExecPrepareExpr((Expr *) linitial(partConstraint), estate); + Assert(pc->partqualstate != NULL); + } + + /* Store partition context into a list. */ + partContexts = lappend(partContexts, pc); + } + + econtext = GetPerTupleExprContext(estate); + + /* Create the necessary tuple slot. */ + srcslot = table_slot_create(splitRel, NULL); + + /* + * Map computing for moving attributes of the split partition to the new + * partition (for the first new partition, but other new partitions can + * use the same map). + */ + pc = (SplitPartitionContext *) lfirst(list_head(partContexts)); + tuple_map = convert_tuples_by_name(RelationGetDescr(splitRel), + RelationGetDescr(pc->partRel)); + + /* Scan through the rows. */ + snapshot = RegisterSnapshot(GetLatestSnapshot()); + scan = table_beginscan(splitRel, snapshot, 0, NULL); + + /* + * Switch to per-tuple memory context and reset it for each tuple + * produced, so we don't leak memory. + */ + oldCxt = MemoryContextSwitchTo(GetPerTupleMemoryContext(estate)); + + while (table_scan_getnextslot(scan, ForwardScanDirection, srcslot)) + { + bool found = false; + TupleTableSlot *insertslot; + + CHECK_FOR_INTERRUPTS(); + + econtext->ecxt_scantuple = srcslot; + + /* Search partition for the current slot, srcslot. */ + foreach(listptr, partContexts) + { + pc = (SplitPartitionContext *) lfirst(listptr); + + /* skip DEFAULT partition */ + if (pc->partqualstate && ExecCheck(pc->partqualstate, econtext)) + { + found = true; + break; + } + } + if (!found) + { + /* Use the DEFAULT partition if it exists. */ + if (defaultPartCtx) + pc = defaultPartCtx; + else + ereport(ERROR, + errcode(ERRCODE_CHECK_VIOLATION), + errmsg("can not find partition for split partition row"), + errtable(splitRel)); + } + + if (tuple_map) + { + /* Need to use a map to copy attributes. */ + insertslot = execute_attr_map_slot(tuple_map->attrMap, srcslot, pc->dstslot); + } + else + { + /* Extract data from the old tuple. */ + slot_getallattrs(srcslot); + + /* Copy attributes directly. */ + insertslot = pc->dstslot; + + ExecClearTuple(insertslot); + + memcpy(insertslot->tts_values, srcslot->tts_values, + sizeof(Datum) * srcslot->tts_nvalid); + memcpy(insertslot->tts_isnull, srcslot->tts_isnull, + sizeof(bool) * srcslot->tts_nvalid); + + ExecStoreVirtualTuple(insertslot); + } + + /* + * Constraints and GENERATED expressions might reference the tableoid + * column, so fill tts_tableOid with the desired value. (We must do + * this each time, because it gets overwritten with newrel's OID + * during storing.) + */ + insertslot->tts_tableOid = RelationGetRelid(pc->partRel); + + /* + * Now, evaluate any generated expressions whose inputs come from the + * new tuple. We assume these columns won't reference each other, so + * that there's no ordering dependency. + */ + evaluateGeneratedExpressionsAndCheckConstraints(pc->tab, pc->partRel, + insertslot, econtext); + + /* Write the tuple out to the new relation. */ + table_tuple_insert(pc->partRel, insertslot, mycid, + ti_options, pc->bistate); + + ResetExprContext(econtext); + } + + MemoryContextSwitchTo(oldCxt); + + table_endscan(scan); + UnregisterSnapshot(snapshot); + + if (tuple_map) + free_conversion_map(tuple_map); + + ExecDropSingleTupleTableSlot(srcslot); + + FreeExecutorState(estate); + + foreach_ptr(SplitPartitionContext, spc, partContexts) + deleteSplitPartitionContext(spc, wqueue, ti_options); +} + +/* + * ALTER TABLE SPLIT PARTITION INTO + */ +void +ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context) +{ + Relation splitRel; + Oid splitRelOid; + ListCell *listptr, + *listptr2; + bool isSameName = false; + char tmpRelName[NAMEDATALEN]; + List *newPartRels = NIL; + ObjectAddress object; + Oid defaultPartOid; + Oid save_userid; + int save_sec_context; + int save_nestlevel; + + defaultPartOid = get_default_oid_from_partdesc(RelationGetPartitionDesc(rel, true)); + + /* + * Partition is already locked in the transformPartitionCmdForSplit + * function. + */ + splitRel = table_openrv(cmd->name, NoLock); + + splitRelOid = RelationGetRelid(splitRel); + + /* Check descriptions of new partitions. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Oid existingRelid; + + /* Look up the existing relation by the new partition name. */ + RangeVarGetAndCheckCreationNamespace(sps->name, NoLock, &existingRelid); + + /* + * This would fail later on anyway if the relation already exists. But + * by catching it here, we can emit a nicer error message. + */ + if (existingRelid == splitRelOid && !isSameName) + /* One new partition can have the same name as a split partition. */ + isSameName = true; + else if (OidIsValid(existingRelid)) + ereport(ERROR, + errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("relation \"%s\" already exists", sps->name->relname)); + } + + /* Detach the split partition. */ + detachPartitionTable(rel, splitRel, defaultPartOid); + + /* + * Perform a preliminary check to determine whether it's safe to drop the + * split partition before we actually do so later. After merging rows into + * the new partitions via SplitPartitionMoveRows, all old partitions need + * to be dropped. However, since the drop behavior is DROP_RESTRICT and + * the merge process (SplitPartitionMoveRows) can be time-consuming, + * performing an early check on the drop eligibility of old partitions is + * preferable. + */ + object.objectId = splitRelOid; + object.classId = RelationRelationId; + object.objectSubId = 0; + performDeletionCheck(&object, DROP_RESTRICT, PERFORM_DELETION_INTERNAL); + + /* + * If a new partition has the same name as the split partition, then we + * should rename the split partition to reuse its name. + */ + if (isSameName) + { + /* + * We must bump the command counter to make the split partition tuple + * visible for renaming. + */ + CommandCounterIncrement(); + /* Rename partition. */ + sprintf(tmpRelName, "split-%u-%X-tmp", RelationGetRelid(rel), MyProcPid); + RenameRelationInternal(splitRelOid, tmpRelName, true, false); + + /* + * We must bump the command counter to make the split partition tuple + * visible after renaming. + */ + CommandCounterIncrement(); + } + + /* Create new partitions (like a split partition), without indexes. */ + foreach_node(SinglePartitionSpec, sps, cmd->partlist) + { + Relation newPartRel; + + newPartRel = createPartitionTable(wqueue, sps->name, rel, + splitRel->rd_rel->relowner); + newPartRels = lappend(newPartRels, newPartRel); + } + + /* + * Switch to the table owner's userid, so that any index functions are run + * as that user. Also, lockdown security-restricted operations and + * arrange to make GUC variable changes local to this command. + * + * Need to do it after determining the namespace in the + * createPartitionTable() call. + */ + GetUserIdAndSecContext(&save_userid, &save_sec_context); + SetUserIdAndSecContext(splitRel->rd_rel->relowner, + save_sec_context | SECURITY_RESTRICTED_OPERATION); + save_nestlevel = NewGUCNestLevel(); + RestrictSearchPath(); + + /* Copy data from the split partition to the new partitions. */ + SplitPartitionMoveRows(wqueue, rel, splitRel, cmd->partlist, newPartRels); + /* Keep the lock until commit. */ + table_close(splitRel, NoLock); + + /* Attach new partitions to the partitioned table. */ + forboth(listptr, cmd->partlist, listptr2, newPartRels) + { + SinglePartitionSpec *sps = (SinglePartitionSpec *) lfirst(listptr); + Relation newPartRel = (Relation) lfirst(listptr2); + + /* + * wqueue = NULL: verification for each cloned constraint is not + * needed. + */ + attachPartitionTable(NULL, rel, newPartRel, sps->bound); + /* Keep the lock until commit. */ + table_close(newPartRel, NoLock); + } + + /* Drop the split partition. */ + object.classId = RelationRelationId; + object.objectId = splitRelOid; + object.objectSubId = 0; + /* Probably DROP_CASCADE is not needed. */ + performDeletion(&object, DROP_RESTRICT, 0); + + /* Roll back any GUC changes executed by index functions. */ + AtEOXact_GUC(false, save_nestlevel); + + /* Restore the userid and security context. */ + SetUserIdAndSecContext(save_userid, save_sec_context); +} diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c index 0ca312ac27d..195c9f30a36 100644 --- a/src/backend/partitioning/partbounds.c +++ b/src/backend/partitioning/partbounds.c @@ -22,6 +22,7 @@ #include "catalog/pg_inherits.h" #include "catalog/pg_type.h" #include "commands/tablecmds.h" +#include "commands/tablecmds_partition.h" #include "common/hashfn.h" #include "executor/executor.h" #include "miscadmin.h" diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index ed38b311ddf..eae0f123f54 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -105,7 +105,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 */ diff --git a/src/include/commands/tablecmds_internal.h b/src/include/commands/tablecmds_internal.h new file mode 100644 index 00000000000..818da023865 --- /dev/null +++ b/src/include/commands/tablecmds_internal.h @@ -0,0 +1,194 @@ +/*------------------------------------------------------------------------- + * + * tablecmds_internal.h + * TODO + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/tablecmds_internal.h + * + *------------------------------------------------------------------------- + */ +#ifndef TABLECMDS_INTERNAL_H +#define TABLECMDS_INTERNAL_H + +#include "access/htup.h" +#include "access/tupdesc.h" +#include "nodes/parsenodes.h" +#include "storage/lockdefs.h" + +/* to avoid including other headers */ +typedef struct RelationData *Relation; +typedef struct List List; +typedef struct ObjectAddress ObjectAddress; +typedef struct ExprState ExprState; + +/* + * 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 +{ + char *name; /* Constraint name, or NULL if none */ + ConstrType contype; /* CHECK or FOREIGN */ + Oid refrelid; /* PK rel, if FOREIGN */ + Oid refindid; /* OID of PK's index, if FOREIGN */ + bool conwithperiod; /* Whether the new FOREIGN KEY uses PERIOD */ + Oid conid; /* OID of pg_constraint entry, if FOREIGN */ + Node *qual; /* Check expr or CONSTR_FOREIGN Constraint */ + ExprState *qualstate; /* Execution state for CHECK expr */ +} NewConstraint; + +/* + * Struct describing one new column value that needs to be computed during + * Phase 3 copy (this could be either a new column with a non-null default, or + * a column that we're changing the type of). Columns without such an entry + * are just copied from the old table during ATRewriteTable. Note that the + * expr is an expression over *old* table values, except when is_generated + * is true; then it is an expression over columns of the *new* tuple. + */ +typedef struct NewColumnValue +{ + AttrNumber attnum; /* which column */ + Expr *expr; /* expression to compute */ + ExprState *exprstate; /* execution state */ + bool is_generated; /* is it a GENERATED expression? */ +} NewColumnValue; + +/* 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 void CheckAlterTableIsSafe(Relation rel); +extern void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation fkrel, + Oid pkrelid, HeapTuple contuple, LOCKMODE lockmode); +extern AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel); +extern void ATSimplePermissions(AlterTableType cmdtype, Relation rel, int allowed_targets); +extern ObjectAddress ATExecDropIdentity(Relation rel, const char *colName, bool missing_ok, LOCKMODE lockmode, + bool recurse, bool recursing); +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 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 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 void DropForeignKeyConstraintTriggers(Relation trigrel, Oid conoid, + Oid confrelid, Oid conrelid); +extern void CreateInheritance(Relation child_rel, Relation parent_rel, bool ispartition); +extern void RemoveInheritance(Relation child_rel, Relation parent_rel, + bool expect_detached); + +#endif /* TABLECMDS_INTERNAL_H */ diff --git a/src/include/commands/tablecmds_partition.h b/src/include/commands/tablecmds_partition.h new file mode 100644 index 00000000000..2da6d7edf7e --- /dev/null +++ b/src/include/commands/tablecmds_partition.h @@ -0,0 +1,66 @@ +/*------------------------------------------------------------------------- + * + * tablecmds_partition.h + * prototypes for tablecmds_partition.c. + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/tablecmds_partition.h + * + *------------------------------------------------------------------------- + */ +#ifndef TABLECMDS_PARTITION_H +#define TABLECMDS_PARTITION_H + +#include "access/attnum.h" +#include "nodes/parsenodes.h" +#include "storage/lockdefs.h" + +/* to avoid including other headers */ +typedef struct AlteredTableInfo AlteredTableInfo; +typedef struct ParseState ParseState; +typedef struct AlterTableUtilityContext AlterTableUtilityContext; +typedef struct ForeignKeyCacheInfo ForeignKeyCacheInfo; +typedef struct RelationData *Relation; +typedef struct List List; +typedef struct ObjectAddress ObjectAddress; + +extern void ATCheckPartitionsNotInUse(Relation rel, LOCKMODE lockmode); +extern bool ConstraintImpliedByRelConstraint(Relation scanrel, + List *testConstraint, List *provenConstraint); +extern void CloneForeignKeyConstraints(List **wqueue, Relation parentRel, + Relation partitionRel); +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 bool PartConstraintImpliedByRelConstraint(Relation scanrel, + List *partConstraint); +extern ObjectAddress ATExecAttachPartition(List **wqueue, Relation rel, + PartitionCmd *cmd, + AlterTableUtilityContext *context); +extern void CloneRowTriggersToPartition(Relation parent, Relation partition); +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); +extern void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel, + PartitionCmd *cmd, AlterTableUtilityContext *context); +extern void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, + Relation rel, PartitionCmd *cmd, + AlterTableUtilityContext *context); + +#endif /* TABLECMDS_PARTITION_H */ -- 2.50.1 (Apple Git-155)