From 2c769c4c11a8306cc0dd1f79d156d032aaeb9fbe Mon Sep 17 00:00:00 2001
From: nkey <mihailnikalayeu@gmail.com>
Date: Thu, 13 Aug 2026 11:22:01 +0200
Subject: [PATCH v1 3/3] Fix crash on UPDATE or DELETE of a partition pending
 detach

ALTER TABLE ... DETACH PARTITION ... CONCURRENTLY, added by 71f4c8c6f74b,
leaves the partition marked as detaching when its wait is interrupted -- by
a lock timeout, a cancel, a disconnect.  Only DETACH PARTITION ... FINALIZE
clears that mark, and the partition cannot be attached back.  In that state
pg_class still says relispartition, while get_partition_ancestors() already
reports nothing.

RelationBuildPublicationDesc() was not ready for it.  It read relispartition
as meaning that the ancestor list is not empty, and asked for its last element
to evaluate the EXCEPT clause there, which is an assertion failure, and a NULL
pointer dereference without assertions.  CheckCmdReplicaIdentity() calls it for
every UPDATE and DELETE of a publishable relation, so no publication has to
exist for this: a plain UPDATE takes the cluster down.

Evaluate the exclusion on the relation itself when there are no ancestors, as
is_table_publishable_in_publication() already does for the same clause.

Oversight in fd366065e06a, which added the exclusion.

Author: Mikhail Nikalayeu <mihailnikalayeu@gmail.com>
Reviewed-by: XXX
Discussion: XXX
Backpatch-through: 19, where it was introduced
---
 src/backend/utils/cache/relcache.c            |  9 +++-
 .../detach-partition-concurrently-3.out       | 44 +++++++++++++++++++
 .../detach-partition-concurrently-3.spec      |  7 +++
 3 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9abbaeab4a9..d03e5e8b79f 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5855,7 +5855,14 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 
 		/* Add publications that the ancestors are in too. */
 		ancestors = get_partition_ancestors(relid);
-		last_ancestor_relid = llast_oid(ancestors);
+
+		/*
+		 * A partition whose concurrent detach has been committed but not
+		 * finalized reports no ancestors, even though relispartition is still
+		 * set.  Fall back to the partition itself, which is what it will be
+		 * once the detach completes.
+		 */
+		last_ancestor_relid = ancestors != NIL ? llast_oid(ancestors) : relid;
 
 		foreach(lc, ancestors)
 		{
diff --git a/src/test/isolation/expected/detach-partition-concurrently-3.out b/src/test/isolation/expected/detach-partition-concurrently-3.out
index a4a78eb5bdf..1f2be58b791 100644
--- a/src/test/isolation/expected/detach-partition-concurrently-3.out
+++ b/src/test/isolation/expected/detach-partition-concurrently-3.out
@@ -132,6 +132,50 @@ b
 (1 row)
 
 
+starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1updpart
+step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
+step s1b: BEGIN;
+step s1s: SELECT * FROM d3_listp;
+a
+-
+1
+(1 row)
+
+step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
+step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid; <waiting ...>
+step s2detach: <... completed>
+ERROR:  canceling statement due to user request
+step s1cancel: <... completed>
+pg_cancel_backend
+-----------------
+t                
+(1 row)
+
+step s1c: COMMIT;
+step s1updpart: UPDATE d3_listp1 SET a = 1;
+
+starting permutation: s2snitch s1b s1s s2detach s1cancel s1c s1delpart
+step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
+step s1b: BEGIN;
+step s1s: SELECT * FROM d3_listp;
+a
+-
+1
+(1 row)
+
+step s2detach: ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; <waiting ...>
+step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid; <waiting ...>
+step s2detach: <... completed>
+ERROR:  canceling statement due to user request
+step s1cancel: <... completed>
+pg_cancel_backend
+-----------------
+t                
+(1 row)
+
+step s1c: COMMIT;
+step s1delpart: DELETE FROM d3_listp1;
+
 starting permutation: s2snitch s1b s1s s2detach2 s1cancel s1c s1brr s1insert s1s s1insert s1c
 step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
 step s1b: BEGIN;
diff --git a/src/test/isolation/specs/detach-partition-concurrently-3.spec b/src/test/isolation/specs/detach-partition-concurrently-3.spec
index 29bf352e470..ab64bc81ce8 100644
--- a/src/test/isolation/specs/detach-partition-concurrently-3.spec
+++ b/src/test/isolation/specs/detach-partition-concurrently-3.spec
@@ -37,6 +37,8 @@ step s1insert		{ INSERT INTO d3_listp VALUES (1); }
 step s1insertpart	{ INSERT INTO d3_listp1 VALUES (1); }
 step s1sid			{ SELECT * FROM d3_idp; }
 step s1insertidpart	{ INSERT INTO d3_idp1 (a) VALUES (1) RETURNING b; }
+step s1updpart		{ UPDATE d3_listp1 SET a = 1; }
+step s1delpart		{ DELETE FROM d3_listp1; }
 step s1drop			{ DROP TABLE d3_listp; }
 step s1droppart		{ DROP TABLE d3_listp1; }
 step s1trunc		{ TRUNCATE TABLE d3_listp; }
@@ -66,6 +68,11 @@ permutation s2snitch s1b s1s s2detach s1cancel(s2detach) s1c s1insertpart
 # partition pending detach has none.
 permutation s2snitch s1b s1sid s2detachid s1cancel(s2detachid) s1c s1insertidpart
 
+# Deciding whether the relation is published made the same assumption, and
+# every UPDATE and DELETE of a publishable relation goes through it.
+permutation s2snitch s1b s1s s2detach s1cancel(s2detach) s1c s1updpart
+permutation s2snitch s1b s1s s2detach s1cancel(s2detach) s1c s1delpart
+
 # Test partition descriptor caching
 permutation s2snitch s1b s1s s2detach2 s1cancel(s2detach2) s1c s1brr s1insert s1s s1insert s1c
 permutation s2snitch s1b s1s s2detach2 s1cancel(s2detach2) s1c s1brr s1s s1insert s1s s1c
-- 
2.43.0

