From 5fe70d4b3ca6d731bb9611500bb6975f9b8aa370 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Tue, 1 Sep 2026 03:20:50 +0000
Subject: [PATCH v3] 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
---
 contrib/test_decoding/expected/repack.out |  6 ++++++
 contrib/test_decoding/sql/repack.sql      |  5 +++++
 doc/src/sgml/ref/repack.sgml              |  8 ++++++++
 src/backend/commands/repack.c             | 15 +++++++++++++++
 4 files changed, 34 insertions(+)

diff --git a/contrib/test_decoding/expected/repack.out b/contrib/test_decoding/expected/repack.out
index 5ddc63238c5..6ed0af0f8ad 100644
--- a/contrib/test_decoding/expected/repack.out
+++ b/contrib/test_decoding/expected/repack.out
@@ -61,6 +61,12 @@ HINT:  Consider running the command on individual partitions.
 REPACK (CONCURRENTLY) pg_class;
 ERROR:  cannot execute REPACK (CONCURRENTLY) on relation "pg_class"
 HINT:  REPACK (CONCURRENTLY) is not supported for catalog relations.
+-- Doesn't support tables used as catalog tables
+CREATE TABLE repack_conc_user_catalog (i int) WITH (user_catalog_table = true);
+REPACK (CONCURRENTLY) repack_conc_user_catalog;
+ERROR:  cannot execute REPACK (CONCURRENTLY) on relation "repack_conc_user_catalog"
+HINT:  REPACK (CONCURRENTLY) is not supported for tables used as catalog tables.
+DROP TABLE repack_conc_user_catalog;
 -- Doesn't support TOAST tables directly
 CREATE TABLE repack_conc_toast (t text);
 SELECT reltoastrelid::regclass AS toast_rel
diff --git a/contrib/test_decoding/sql/repack.sql b/contrib/test_decoding/sql/repack.sql
index f461f5479f4..eb891009baf 100644
--- a/contrib/test_decoding/sql/repack.sql
+++ b/contrib/test_decoding/sql/repack.sql
@@ -42,6 +42,11 @@ REPACK (CONCURRENTLY) clstrpart;
 -- Disallowed in catalogs
 REPACK (CONCURRENTLY) pg_class;
 
+-- Doesn't support tables used as catalog tables
+CREATE TABLE repack_conc_user_catalog (i int) WITH (user_catalog_table = true);
+REPACK (CONCURRENTLY) repack_conc_user_catalog;
+DROP TABLE repack_conc_user_catalog;
+
 -- Doesn't support TOAST tables directly
 CREATE TABLE repack_conc_toast (t text);
 SELECT reltoastrelid::regclass AS toast_rel
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..3da61437580 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)"));
 
+	/*
+	 * Concurrent repack 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

