From a318d3726c6f2e7a6c03dd9c2b70f5c5d3526f1e Mon Sep 17 00:00:00 2001
From: nkey <mihailnikalayeu@gmail.com>
Date: Thu, 13 Aug 2026 11:18:37 +0200
Subject: [PATCH v1 2/3] Fix crash inserting into a partition pending detach
 with an identity column

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.

getIdentitySequence() was not ready for it.  The sequence behind a partition's
identity column belongs to the root of its partition tree, so it asked for the
last ancestor, which is an assertion failure, and a NULL pointer dereference
without assertions.  Any INSERT that has to compute the default of such a
column gets there.

The root is genuinely wanted here: the partition keeps its identity columns,
and the sequences behind them keep belonging to the root until the detach is
finalized, so climb to it regardless of the mark.

Oversight in 509199587df7, which taught getIdentitySequence() to climb to the
topmost partitioned table with get_partition_ancestors(), which by then could
already report none.

Author: Mikhail Nikalayeu <mihailnikalayeu@gmail.com>
Reviewed-by: XXX
Discussion: XXX
Backpatch-through: 17
---
 src/backend/catalog/pg_depend.c               | 19 +++++++++++---
 .../detach-partition-concurrently-3.out       | 26 +++++++++++++++++++
 .../detach-partition-concurrently-3.spec      | 11 ++++++++
 3 files changed, 53 insertions(+), 3 deletions(-)

diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 9a7a401aced..b89cbc03bbe 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -1157,15 +1157,28 @@ getIdentitySequence(Relation rel, AttrNumber attnum, bool missing_ok)
 	 */
 	if (RelationGetForm(rel)->relispartition)
 	{
-		List	   *ancestors = get_partition_ancestors(relid);
 		const char *attname = get_attname(relid, attnum, false);
 
-		relid = llast_oid(ancestors);
+		/*
+		 * Climb to the root.  get_partition_ancestors() is unusable here: it
+		 * stops at a partition whose concurrent detach has been committed but
+		 * not finalized, and so reports no ancestors at all for one.  Such a
+		 * partition keeps its identity columns, and the sequences behind them
+		 * keep belonging to the root, until the detach is finalized.
+		 *
+		 * XXX A variant of get_partition_ancestors() taking even_if_detached
+		 * would climb in a single scan of pg_inherits, but adding one would
+		 * make this harder to back-patch.
+		 */
+		do
+		{
+			relid = get_partition_parent(relid, true);
+		} while (get_rel_relispartition(relid));
+
 		attnum = get_attnum(relid, attname);
 		if (attnum == InvalidAttrNumber)
 			elog(ERROR, "cache lookup failed for attribute \"%s\" of relation %u",
 				 attname, relid);
-		list_free(ancestors);
 	}
 
 	seqlist = getOwnedSequences_internal(relid, attnum, DEPENDENCY_INTERNAL);
diff --git a/src/test/isolation/expected/detach-partition-concurrently-3.out b/src/test/isolation/expected/detach-partition-concurrently-3.out
index f23f46ad89b..a4a78eb5bdf 100644
--- a/src/test/isolation/expected/detach-partition-concurrently-3.out
+++ b/src/test/isolation/expected/detach-partition-concurrently-3.out
@@ -106,6 +106,32 @@ t
 step s1c: COMMIT;
 step s1insertpart: INSERT INTO d3_listp1 VALUES (1);
 
+starting permutation: s2snitch s1b s1sid s2detachid s1cancel s1c s1insertidpart
+step s2snitch: INSERT INTO d3_pid SELECT pg_backend_pid();
+step s1b: BEGIN;
+step s1sid: SELECT * FROM d3_idp;
+a|b
+-+-
+(0 rows)
+
+step s2detachid: ALTER TABLE d3_idp DETACH PARTITION d3_idp1 CONCURRENTLY; <waiting ...>
+step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid; <waiting ...>
+step s2detachid: <... completed>
+ERROR:  canceling statement due to user request
+step s1cancel: <... completed>
+pg_cancel_backend
+-----------------
+t                
+(1 row)
+
+step s1c: COMMIT;
+step s1insertidpart: INSERT INTO d3_idp1 (a) VALUES (1) RETURNING b;
+b
+-
+1
+(1 row)
+
+
 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 31aa3080daf..29bf352e470 100644
--- a/src/test/isolation/specs/detach-partition-concurrently-3.spec
+++ b/src/test/isolation/specs/detach-partition-concurrently-3.spec
@@ -14,10 +14,14 @@ setup
   CREATE TABLE d3_listp2 PARTITION OF d3_listp FOR VALUES IN (2);
   CREATE TABLE d3_pid (pid int);
   INSERT INTO d3_listp VALUES (1);
+  CREATE TABLE d3_idp (a int, b bigint GENERATED BY DEFAULT AS IDENTITY)
+    PARTITION BY LIST(a);
+  CREATE TABLE d3_idp1 PARTITION OF d3_idp FOR VALUES IN (1);
 }
 
 teardown {
     DROP TABLE IF EXISTS d3_listp, d3_listp1, d3_listp2, d3_pid;
+    DROP TABLE IF EXISTS d3_idp, d3_idp1;
 }
 
 session s1
@@ -31,6 +35,8 @@ step s1c			{ COMMIT; }
 step s1alter		{ ALTER TABLE d3_listp1 ALTER a DROP NOT NULL; }
 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 s1drop			{ DROP TABLE d3_listp; }
 step s1droppart		{ DROP TABLE d3_listp1; }
 step s1trunc		{ TRUNCATE TABLE d3_listp; }
@@ -44,6 +50,7 @@ step s2begin		{ BEGIN; }
 step s2snitch		{ INSERT INTO d3_pid SELECT pg_backend_pid(); }
 step s2detach		{ ALTER TABLE d3_listp DETACH PARTITION d3_listp1 CONCURRENTLY; }
 step s2detach2		{ ALTER TABLE d3_listp DETACH PARTITION d3_listp2 CONCURRENTLY; }
+step s2detachid		{ ALTER TABLE d3_idp DETACH PARTITION d3_idp1 CONCURRENTLY; }
 step s2detachfinal	{ ALTER TABLE d3_listp DETACH PARTITION d3_listp1 FINALIZE; }
 step s2drop			{ DROP TABLE d3_listp1; }
 step s2commit		{ COMMIT; }
@@ -55,6 +62,10 @@ permutation s2snitch s1b s1s s2detach s1cancel(s2detach) s1insert s1c
 permutation s2snitch s1brr s1s s2detach s1cancel(s2detach) s1insert s1c s1spart
 permutation s2snitch s1b s1s s2detach s1cancel(s2detach) s1c s1insertpart
 
+# The sequence behind an identity column lives in the topmost ancestor, and a
+# partition pending detach has none.
+permutation s2snitch s1b s1sid s2detachid s1cancel(s2detachid) s1c s1insertidpart
+
 # 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

