From 18e747adaf3e19b30663cef939dc23b30cf385cc Mon Sep 17 00:00:00 2001 From: Nisha Moond Date: Thu, 10 Sep 2026 10:14:49 +0530 Subject: [PATCH v2_003] 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 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 19c4ff6e75e..d6067a068d5 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 f23f46ad89b..f1eebadec0f 100644 --- a/src/test/isolation/expected/detach-partition-concurrently-3.out +++ b/src/test/isolation/expected/detach-partition-concurrently-3.out @@ -106,6 +106,50 @@ t step s1c: COMMIT; step s1insertpart: INSERT INTO d3_listp1 VALUES (1); +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; +step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid; +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; +step s1cancel: SELECT pg_cancel_backend(pid) FROM d3_pid; +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 31aa3080daf..1a15da1cb5d 100644 --- a/src/test/isolation/specs/detach-partition-concurrently-3.spec +++ b/src/test/isolation/specs/detach-partition-concurrently-3.spec @@ -31,6 +31,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 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; } @@ -55,6 +57,11 @@ 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 +# 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.50.1 (Apple Git-155)