From 98134b1edf9ac81868969c88ff3f76c443cd0ae5 Mon Sep 17 00:00:00 2001
From: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Date: Tue, 1 Sep 2026 03:29:27 +0000
Subject: [PATCH v3] Restrict concurrent repack to the heap access method.

Concurrent repack (28d534e2ae0) replays the data changes made
during the table rewrite by decoding them from WAL, which only
works if the table's access method supports logical decoding.
check_concurrent_repack_requirements() didn't check for that.

For an access method that doesn't support it, the concurrent
changes are never decoded and are silently lost from the
rewritten table.

Fix by erroring out for tables that use a non-heap access method,
and document the limitation. Recognizing logical-decoding support
properly would need a new table AM callback, which is more than
this open item warrants, so restrict it to heap for now.

Backpatch to v19, where concurrent repack was introduced.

Reported-by: Nathan Bossart <nathandbossart@gmail.com>
Author: Bharath Rupireddy <bharath.rupireddyforpostgres@gmail.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/apBkixO4xAGFoiT3%40nathan
Backpatch-through: 19
---
 contrib/test_decoding/expected/repack.out |  8 ++++++++
 contrib/test_decoding/sql/repack.sql      |  7 +++++++
 doc/src/sgml/ref/repack.sgml              |  6 ++++++
 src/backend/commands/repack.c             | 14 ++++++++++++++
 4 files changed, 35 insertions(+)

diff --git a/contrib/test_decoding/expected/repack.out b/contrib/test_decoding/expected/repack.out
index 5ddc63238c5..7872d567070 100644
--- a/contrib/test_decoding/expected/repack.out
+++ b/contrib/test_decoding/expected/repack.out
@@ -57,6 +57,14 @@ CREATE TABLE clstrpart (a int) PARTITION BY RANGE (a);
 REPACK (CONCURRENTLY) clstrpart;
 ERROR:  REPACK (CONCURRENTLY) is not supported for partitioned tables
 HINT:  Consider running the command on individual partitions.
+-- Only supported for the heap access method
+CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE repack_conc_heap2 (i int) USING heap2;
+REPACK (CONCURRENTLY) repack_conc_heap2;
+ERROR:  cannot execute REPACK (CONCURRENTLY) on relation "repack_conc_heap2"
+HINT:  REPACK (CONCURRENTLY) is only supported for the "heap" access method.
+DROP TABLE repack_conc_heap2;
+DROP ACCESS METHOD heap2;
 -- Disallowed in catalogs
 REPACK (CONCURRENTLY) pg_class;
 ERROR:  cannot execute REPACK (CONCURRENTLY) on relation "pg_class"
diff --git a/contrib/test_decoding/sql/repack.sql b/contrib/test_decoding/sql/repack.sql
index f461f5479f4..ce8ed6c3b26 100644
--- a/contrib/test_decoding/sql/repack.sql
+++ b/contrib/test_decoding/sql/repack.sql
@@ -39,6 +39,13 @@ DROP TABLE rpk_missing;
 CREATE TABLE clstrpart (a int) PARTITION BY RANGE (a);
 REPACK (CONCURRENTLY) clstrpart;
 
+-- Only supported for the heap access method
+CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler;
+CREATE TABLE repack_conc_heap2 (i int) USING heap2;
+REPACK (CONCURRENTLY) repack_conc_heap2;
+DROP TABLE repack_conc_heap2;
+DROP ACCESS METHOD heap2;
+
 -- Disallowed in catalogs
 REPACK (CONCURRENTLY) pg_class;
 
diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 0cb72b6b289..0496cd24af1 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -285,6 +285,12 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
         </para>
        </listitem>
 
+       <listitem>
+        <para>
+          The table uses an access method other than <literal>heap</literal>.
+        </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..05c0662a510 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -877,6 +877,20 @@ check_concurrent_repack_requirements(Relation rel, Oid *ident_idx_p)
 				errdetail("%s requires \"wal_level\" to be set to \"replica\" or higher.",
 						  "REPACK (CONCURRENTLY)"));
 
+	/*
+	 * A table AM that doesn't support logical decoding would cause concurrent
+	 * repack to silently lose the changes made during the rewrite. Detecting
+	 * such support cleanly would need a new table AM callback, so for now
+	 * just restrict the command to heap.
+	 */
+	if (rel->rd_rel->relam != HEAP_TABLE_AM_OID)
+		ereport(ERROR,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot execute %s on relation \"%s\"",
+					   "REPACK (CONCURRENTLY)", RelationGetRelationName(rel)),
+				errhint("%s is only supported for the \"heap\" access method.",
+						"REPACK (CONCURRENTLY)"));
+
 	/* Data changes in system relations are not logically decoded. */
 	if (IsCatalogRelation(rel))
 		ereport(ERROR,
-- 
2.47.3

