v45-0001-refactor-ATExecMergePartitions-transformPartitionCmdF.no-cfbot

application/octet-stream

Filename: v45-0001-refactor-ATExecMergePartitions-transformPartitionCmdF.no-cfbot
Type: application/octet-stream
Part: 0
Message: Re: Add SPLIT PARTITION/MERGE PARTITIONS commands
From 97f4372b6c1f4eb282b7a3e09e860abe9a27cae1 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Sun, 15 Jun 2025 22:24:26 +0800
Subject: [PATCH v45 1/2] refactor ATExecMergePartitions,
 transformPartitionCmdForMerge
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

For the command:
ALTER TABLE pk MERGE PARTITIONS (pk_1, pk_2) INTO pk_1;

I believe we should acquire AccessExclusiveLock on the partitions to be merged
(pk_1, pk_2) during transformPartitionCmdForMerge. Here’s why:

* The merged partitions (pk_1, pk_2) will be dropped in the end, so acquiring
AccessExclusiveLock is unavoidable for ALTER TABLE MERGE PARTITIONS.
* Taking an AccessShareLock first, and then later acquiring AccessExclusiveLock in
ATExecMergePartitions unnecessarily waste resources.
* Acquiring AccessExclusiveLock first helps avoid potential anomalies caused by concurrent operations.

+ partOid = RangeVarGetRelid(name, AccessShareLock, false);
if current user don't owne the table (name), then he can not lock the table.
thus the above code in transformPartitionCmdForMerge need refactor
---
 src/backend/commands/tablecmds.c   | 74 +++++++++++++++++-------------
 src/backend/parser/parse_utilcmd.c | 18 ++++++--
 2 files changed, 56 insertions(+), 36 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7e7703c7997..e3bbfd9283b 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -22543,13 +22543,13 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 }
 
 /*
- * moveMergedTablesRows: scan partitions to be merged (mergingPartitionsList)
+ * moveMergedTablesRows: scan partitions to be merged (mergingPartitions)
  * of the partitioned table (rel) and move rows into the new partition
  * (newPartRel). We also reevaulate check constraints against these rows.
  */
 static void
 moveMergedTablesRows(List **wqueue, Relation rel,
-					 List *mergingPartitionsList, Relation newPartRel)
+					 List *mergingPartitions, Relation newPartRel)
 {
 	CommandId	mycid;
 	EState	   *estate;
@@ -22580,12 +22580,15 @@ moveMergedTablesRows(List **wqueue, Relation rel,
 	/* Create necessary tuple slot. */
 	dstslot = table_slot_create(newPartRel, NULL);
 
-	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	foreach_oid(merging_oid, mergingPartitions)
 	{
 		TupleTableSlot *srcslot;
 		TupleConversionMap *tuple_map;
 		TableScanDesc scan;
 		Snapshot	snapshot;
+		Relation 	mergingPartition;
+
+		mergingPartition = table_open(merging_oid, NoLock);
 
 		/* Create tuple slot for new partition. */
 		srcslot = table_slot_create(mergingPartition, NULL);
@@ -22659,6 +22662,7 @@ moveMergedTablesRows(List **wqueue, Relation rel,
 			free_conversion_map(tuple_map);
 
 		ExecDropSingleTupleTableSlot(srcslot);
+		table_close(mergingPartition, NoLock);
 	}
 
 	FreeExecutorState(estate);
@@ -22709,7 +22713,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 					  PartitionCmd *cmd, AlterTableUtilityContext *context)
 {
 	Relation	newPartRel;
-	List	   *mergingPartitionsList = NIL;
+	List	   *mergingPartitions = NIL;
 	Oid			defaultPartOid;
 	Oid			existingRelid;
 	Oid			ownerId = InvalidOid;
@@ -22718,18 +22722,21 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	int			save_nestlevel;
 
 	/*
-	 * Lock all merged partitions, check them and create list with partitions
-	 * contexts.
-	 */
+	 * 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: need to use
-		 * exclusive lock for preventing DML-queries to the partition.
+		 * We are going to detach and remove this partition.  We already took
+		 * AccessExclusiveLock lock on transformPartitionCmdForMerge, so here,
+		 * NoLock is fine.
 		 */
-		mergingPartition = table_openrv(name, AccessExclusiveLock);
+		mergingPartition = table_openrv_extended(name, NoLock, false);
+		Assert(CheckRelationLockedByMe(mergingPartition, AccessExclusiveLock, false));
 
 		if (OidIsValid(ownerId))
 		{
@@ -22743,8 +22750,10 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			ownerId = mergingPartition->rd_rel->relowner;
 
 		/* Store a next merging partition into the list. */
-		mergingPartitionsList = lappend(mergingPartitionsList,
-										mergingPartition);
+		mergingPartitions = lappend_oid(mergingPartitions,
+										RelationGetRelid(mergingPartition));
+
+		table_close(mergingPartition, NoLock);
 	}
 
 	/*
@@ -22764,18 +22773,18 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 */
 	if (OidIsValid(existingRelid))
 	{
-		Relation	sameNamePartition = NULL;
+		Oid		new_partition = InvalidOid;
 
-		foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+		foreach_oid(mergingPartition, mergingPartitions)
 		{
-			if (RelationGetRelid(mergingPartition) == existingRelid)
+			if (mergingPartition == existingRelid)
 			{
-				sameNamePartition = mergingPartition;
+				new_partition = mergingPartition;
 				break;
 			}
 		}
 
-		if (sameNamePartition)
+		if (OidIsValid(new_partition))
 		{
 			/*
 			 * The new partition has the same name as one of merging
@@ -22792,8 +22801,7 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 * in the future because we're going to eventually drop the
 			 * existing partition anyway.
 			 */
-			RenameRelationInternal(RelationGetRelid(sameNamePartition),
-								   tmpRelName, true, false);
+			RenameRelationInternal(new_partition, tmpRelName, true, false);
 
 			/*
 			 * We must bump the command counter to make the new partition
@@ -22822,14 +22830,23 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	 * merge process (moveMergedTablesRows) can be time-consuming, performing an
 	 * early check on the drop eligibility of old partitions is preferable.
 	 */
-	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	foreach_oid(mergingPartition, mergingPartitions)
+	{
+		Relation	child_rel;
+
+		child_rel = table_open(mergingPartition, NoLock);
+
+		detachPartitionTable(rel, child_rel, defaultPartOid);
+
+		table_close(child_rel, NoLock);
+	}
+
+	foreach_oid(mergingPartition, mergingPartitions)
 	{
 		ObjectAddress object;
 
-		detachPartitionTable(rel, mergingPartition, defaultPartOid);
-
 		/* Get oid of the later to be dropped relation */
-		object.objectId = RelationGetRelid(mergingPartition);
+		object.objectId = mergingPartition;
 		object.classId = RelationRelationId;
 		object.objectSubId = 0;
 
@@ -22854,24 +22871,19 @@ ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	RestrictSearchPath();
 
 	/* Copy data from merged partitions to new partition. */
-	moveMergedTablesRows(wqueue, rel, mergingPartitionsList, newPartRel);
+	moveMergedTablesRows(wqueue, rel, mergingPartitions, newPartRel);
 
 	/* Drop the current partitions before attaching the new one. */
-	foreach_ptr(RelationData, mergingPartition, mergingPartitionsList)
+	foreach_oid(mergingPartition, mergingPartitions)
 	{
 		ObjectAddress object;
 
-		/* Get relation id before table_close() call. */
-		object.objectId = RelationGetRelid(mergingPartition);
+		object.objectId = mergingPartition;
 		object.classId = RelationRelationId;
 		object.objectSubId = 0;
 
-		/* Keep the lock until commit. */
-		table_close(mergingPartition, NoLock);
-
 		performDeletion(&object, DROP_RESTRICT, 0);
 	}
-	list_free(mergingPartitionsList);
 
 	/*
 	 * Attach a new partition to the partitioned table. wqueue = NULL:
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index da291986446..eb8a937e1e8 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -3500,7 +3500,7 @@ checkPartition(Relation rel, Oid partRelOid)
 {
 	Relation	partRel;
 
-	partRel = relation_open(partRelOid, AccessShareLock);
+	partRel = table_open(partRelOid, NoLock);
 
 	if (partRel->rd_rel->relkind != RELKIND_RELATION)
 		ereport(ERROR,
@@ -3526,7 +3526,7 @@ checkPartition(Relation rel, Oid partRelOid)
 		aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(partRel->rd_rel->relkind),
 					   RelationGetRelationName(partRel));
 
-	relation_close(partRel, AccessShareLock);
+	table_close(partRel, NoLock);
 }
 
 /*
@@ -3577,10 +3577,18 @@ transformPartitionCmdForMerge(CreateStmtContext *cxt, PartitionCmd *partcmd)
 		}
 
 		/*
-		 * Search DEFAULT partition in the list. Lock partitions before
-		 * calculating the boundary for resulting partition.
+		 * Search DEFAULT partition in the list. Open and lock partitions before
+		 * calculating the boundary for resulting partition, we also check for
+		 * ownership along the way.  We need to use AccessExclusiveLock here,
+		 * because these merged partitions will be detached then dropped in
+		 * ATExecMergePartitions.
 		 */
-		partOid = RangeVarGetRelid(name, AccessShareLock, false);
+		partOid = RangeVarGetRelidExtended(name,
+										   AccessExclusiveLock,
+										   false,
+										   RangeVarCallbackOwnsRelation,
+										   NULL);
+
 		if (partOid == defaultPartOid)
 			isDefaultPart = true;
 
-- 
2.34.1