From 1648cf294cb4705a05d1c1ab7795be9bd5526503 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Thu, 29 Jan 2026 16:09:46 -0600
Subject: [PATCH v2 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  |  183 +
 src/include/commands/tablecmds_partition.h |   59 +
 8 files changed, 4885 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 <name> ATTACH PARTITION <partition-name> 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 <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
- */
-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 <name> SPLIT PARTITION <partition-name> INTO <partition-list>
- */
-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 <name> ATTACH PARTITION <partition-name> 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 <name> MERGE PARTITIONS <partition-list> INTO <partition-name>
+ */
+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 <name> SPLIT PARTITION <partition-name> INTO <partition-list>
+ */
+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..4e91fe3c20e
--- /dev/null
+++ b/src/include/commands/tablecmds_internal.h
@@ -0,0 +1,183 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+/*
+ * 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..27c3583abd8
--- /dev/null
+++ b/src/include/commands/tablecmds_partition.h
@@ -0,0 +1,59 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+/* to avoid including other headers */
+typedef struct AlteredTableInfo AlteredTableInfo;
+typedef struct ParseState ParseState;
+typedef struct AlterTableUtilityContext AlterTableUtilityContext;
+typedef struct ForeignKeyCacheInfo ForeignKeyCacheInfo;
+
+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)

