From 5f35590dac6fc517eda39fc976749210004c257f Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Fri, 30 Jan 2026 11:10:32 +0800
Subject: [PATCH v1] Prevent deadlock between ALTER TABLE recursion and ATTACH
 PARTITION

ALTER TABLE commands that recurse to partitions (e.g. ALTER COLUMN SET
DEFAULT) lock partitions in ascending OID order using find_all_inheritors().
In contrast, ALTER TABLE ATTACH PARTITION may lock ancestor tables in
reverse OID order while recursively collecting partition constraints.
When these two operations run concurrently, this lock order inversion
can lead to a deadlock.

To avoid this, pre-lock all ancestor partitioned tables in OID order
when processing ALTER TABLE ATTACH PARTITION. This ensures a consistent
lock ordering before any recursive traversal of the partition hierarchy
occurs.

The ancestor locks are taken early, via RangeVarCallbackForAlterRelation(),
so that they precede the standard ALTER TABLE locking of the target
relation.

This fixes a reproducible deadlock observed on the current master
branch with deep partition hierarchies.

Author: Chao Li <lic@highgo.com>
Reviewed-by:
Discussion: https://postgr.es/m/
---
 src/backend/catalog/partition.c  | 19 ++++++++++++++++++
 src/backend/commands/tablecmds.c | 33 ++++++++++++++++++++++++++++++++
 src/include/catalog/partition.h  |  2 ++
 3 files changed, 54 insertions(+)

diff --git a/src/backend/catalog/partition.c b/src/backend/catalog/partition.c
index 28f3cade6ff..d9449bca92c 100644
--- a/src/backend/catalog/partition.c
+++ b/src/backend/catalog/partition.c
@@ -26,6 +26,7 @@
 #include "nodes/makefuncs.h"
 #include "optimizer/optimizer.h"
 #include "rewrite/rewriteManip.h"
+#include "storage/lmgr.h"
 #include "utils/fmgroids.h"
 #include "utils/partcache.h"
 #include "utils/rel.h"
@@ -145,6 +146,24 @@ get_partition_ancestors(Oid relid)
 	return result;
 }
 
+/*
+ * lock_partition_ancestors
+ *		Lock all ancestors of given relation from root to parent.
+ */
+List *
+lock_partition_ancestors(Oid relid, LOCKMODE lockmode)
+{
+	List	   *ancestors = get_partition_ancestors(relid);
+
+	for (int i = list_length(ancestors) - 1; i >= 0; i--)
+	{
+		Oid			parentoid = list_nth_oid(ancestors, i);
+
+		LockRelationOid(parentoid, lockmode);
+	}
+	return ancestors;
+}
+
 /*
  * get_partition_ancestors_worker
  *		recursive worker for get_partition_ancestors
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f976c0e5c7e..fc27e2ba53e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19743,6 +19743,39 @@ RangeVarCallbackForAlterRelation(const RangeVar *rv, Oid relid, Oid oldrelid,
 					 errhint("Change the schema of the table instead.")));
 	}
 
+	/* 
+	 * For ALTER TABLE ATTACH PARTITION, pre-lock all ancestors. That's because
+	 * ATTACH PARTITION needs to find all ancestors' partition bounds, thus it
+	 * will recurse up the partition tree, and lock each ancestor it meets,
+	 * which may lead to deadlock. So we pre-lock all ancestors here to avoid
+	 * that.
+	 */
+	if (IsA(stmt, AlterTableStmt))
+	{
+		AlterTableStmt *ats = (AlterTableStmt *) stmt;
+		ListCell   *l;
+		bool found = false;
+
+		foreach (l, ats->cmds)
+		{
+			AlterTableCmd *atc = lfirst_node(AlterTableCmd, l);
+
+			if (atc->subtype == AT_AttachPartition)
+			{
+				found = true;
+				break;
+			}
+		}
+		if (found)
+		{
+			/* 
+			 * The target table must be a partitioned table, but if, we don't
+			 * need to fail here; The normal ALTER TABLE processing will do that.
+			 */
+			if (relkind == RELKIND_PARTITIONED_TABLE)
+				lock_partition_ancestors(relid, AccessShareLock);
+		}
+	}
 	ReleaseSysCache(tuple);
 }
 
diff --git a/src/include/catalog/partition.h b/src/include/catalog/partition.h
index a93cf081dd2..1882de56126 100644
--- a/src/include/catalog/partition.h
+++ b/src/include/catalog/partition.h
@@ -14,6 +14,7 @@
 #define PARTITION_H
 
 #include "partitioning/partdefs.h"
+#include "storage/lockdefs.h"
 #include "utils/relcache.h"
 
 /* Seed for the extended hash function */
@@ -21,6 +22,7 @@
 
 extern Oid	get_partition_parent(Oid relid, bool even_if_detached);
 extern List *get_partition_ancestors(Oid relid);
+extern List *lock_partition_ancestors(Oid relid, LOCKMODE lockmode);
 extern Oid	index_get_partition(Relation partition, Oid indexId);
 extern List *map_partition_varattnos(List *expr, int fromrel_varno,
 									 Relation to_rel, Relation from_rel);
-- 
2.50.1 (Apple Git-155)

