From 4423be275ac65a7b0fc062ff319789d0205b2727 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Thu, 27 Aug 2026 22:41:00 +0000
Subject: [PATCH v1] Restrict concurrent repack on user catalog tables.

Concurrent repack (28d534e2ae0) rewrites the table without going
through the rewriteheap.c machinery that the non-concurrent path
uses. That machinery logs old-to-new tuple location mappings
under pg_logical/mappings, which logical decoding uses to resolve
the cmin/cmax of catalog tuples relocated by the rewrite.
check_concurrent_repack_requirements() didn't check whether the
table is used as a catalog table.

A table declared with user_catalog_table can be read by an output
plugin during logical decoding. Without those mappings, such a
consumer can no longer resolve the relocated tuples and is
silently broken.

Fix by erroring out for tables used as catalog tables, the same
way a heap rewrite via ALTER TABLE already does, and document the
limitation.

Backpatch to v19, where concurrent repack was introduced.

Reported-by: Nathan Bossart <nathandbossart@gmail.com>
Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Discussion: https://postgr.es/m/apBIFWzHYOaG0auN%40nathan
Backpatch-through: 19
---
 doc/src/sgml/ref/repack.sgml  | 8 ++++++++
 src/backend/commands/repack.c | 9 +++++++++
 2 files changed, 17 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..39751af24af 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -886,6 +886,15 @@ check_concurrent_repack_requirements(Relation rel, Oid *ident_idx_p)
 				errhint("%s is not supported for catalog relations.",
 						"REPACK (CONCURRENTLY)"));
 
+	/* The CONCURRENTLY path does not write logical rewrite mappings. */
+	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

