From 71301630532b8aa8bb0e031b3c0b90a5365b4c25 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Sat, 29 Aug 2026 15:15:47 +0000
Subject: [PATCH v2] Restrict REPACK (CONCURRENTLY) on user catalog tables.

Commit 28d534e2ae0 added the CONCURRENTLY option to REPACK.
Unlike the default path, which rewrites the table through
rewriteheap.c, the concurrent path is not MVCC-safe: it changes
the visibility information of the tuples it rewrites.

A table declared with the user_catalog_table storage parameter is
read by an output plugin during logical decoding, using
historical snapshots. Changing that visibility information can
make the table's contents invisible to the plugin at some point,
silently breaking decoding.

check_concurrent_repack_requirements() already rejects system
catalogs but did not consider tables used as catalog tables.
Reject those too, the same way a table rewrite via ALTER TABLE
already does, and document the limitation. Once REPACK
(CONCURRENTLY) is made MVCC-safe, this check should stay unless
the rewrite mappings are also written for user catalog tables.

Backpatch to v19, where REPACK (CONCURRENTLY) was introduced.

Reported-by: Nathan Bossart <nathandbossart@gmail.com>
Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Antonin Houska <ah@cybertec.at>
Discussion: https://postgr.es/m/apBIFWzHYOaG0auN%40nathan
Backpatch-through: 19
---
 doc/src/sgml/ref/repack.sgml  |  8 ++++++++
 src/backend/commands/repack.c | 15 +++++++++++++++
 2 files changed, 23 insertions(+)

diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 0cb72b6b289..e2d5a34ba8b 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -285,6 +285,14 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
         </para>
        </listitem>
 
+       <listitem>
+        <para>
+          The table is declared as a catalog table using the
+          <link linkend="reloption-user-catalog-table"><literal>user_catalog_table</literal></link>
+          storage parameter.
+        </para>
+       </listitem>
+
        <listitem>
         <para>
          <command>REPACK</command> is executed inside a transaction block.
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 477c86b2ba6..829f76ac1d5 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -886,6 +886,21 @@ check_concurrent_repack_requirements(Relation rel, Oid *ident_idx_p)
 				errhint("%s is not supported for catalog relations.",
 						"REPACK (CONCURRENTLY)"));
 
+	/*
+	 * REPACK (CONCURRENTLY) is not MVCC-safe, i.e. it changes visibility
+	 * information, which can make the contents invisible to the output
+	 * plugin. Once it is made MVCC-safe, the logical rewrite mappings must
+	 * also be written for user catalog tables before this check can be
+	 * removed.
+	 */
+	if (RelationIsUsedAsCatalogTable(rel))
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot execute %s on relation \"%s\"",
+					   "REPACK (CONCURRENTLY)", RelationGetRelationName(rel)),
+				errhint("%s is not supported for tables used as catalog tables.",
+						"REPACK (CONCURRENTLY)"));
+
 	/*
 	 * reorderbuffer.c does not seem to handle processing of TOAST relation
 	 * alone.
-- 
2.47.3

