From 75430e78caa43feacf4a0260cc9c263edd76a681 Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@percona.com>
Date: Sat, 29 Aug 2026 17:10:34 +0000
Subject: [PATCH] Warn when REPACK or CLUSTER skips a foreign-table partition

Processing a partitioned table silently skipped partitions that are
foreign tables, both when expanding the table's partition tree and in
the USING INDEX case, where foreign tables cannot appear in the index
tree at all.  That is inconsistent with the surrounding behavior:
VACUUM FULL warns about such partitions, and a direct REPACK or
CLUSTER of a foreign table is an error.  Emit a warning, like VACUUM
does.
---
 doc/src/sgml/ref/cluster.sgml         |  3 ++-
 doc/src/sgml/ref/repack.sgml          |  5 +++--
 src/backend/commands/repack.c         | 18 ++++++++++++++++++
 src/test/regress/expected/cluster.out | 22 ++++++++++++++++++++++
 src/test/regress/sql/cluster.sql      | 18 ++++++++++++++++++
 5 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/ref/cluster.sgml b/doc/src/sgml/ref/cluster.sgml
index ffb3ff898c6..4dc3eb08430 100644
--- a/doc/src/sgml/ref/cluster.sgml
+++ b/doc/src/sgml/ref/cluster.sgml
@@ -118,7 +118,8 @@ CLUSTER [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
    <para>
     Clustering a partitioned table clusters each of its partitions using the
     partition of the specified partitioned index.  When clustering a partitioned
-    table, the index may not be omitted.  <command>CLUSTER</command> on a
+    table, the index may not be omitted.  Partitions that are foreign tables
+    are skipped, after a warning is emitted.  <command>CLUSTER</command> on a
     partitioned table cannot be executed inside a transaction block.
    </para>
 
diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 0cb72b6b289..732884b0ad0 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -372,8 +372,9 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
    <para>
     Repacking a partitioned table repacks each of its partitions. If an index
     is specified, each partition is repacked using the partition of that
-    index. <command>REPACK</command> on a partitioned table cannot be executed
-    inside a transaction block.
+    index. Partitions that are foreign tables are skipped, after a warning is
+    emitted. <command>REPACK</command> on a partitioned table cannot be
+    executed inside a transaction block.
    </para>
 
  </refsect1>
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 477c86b2ba6..bf2117a4e00 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -2229,6 +2229,7 @@ get_tables_to_repack_partitioned(RepackStmt *stmt, Relation rel,
 {
 	Oid			relid;
 	bool		rel_is_index;
+	List	   *tableoids;
 	List	   *inhoids;
 	List	   *rtcs = NIL;
 
@@ -2278,6 +2279,23 @@ get_tables_to_repack_partitioned(RepackStmt *stmt, Relation rel,
 		rel_is_index = false;
 	}
 
+	/*
+	 * Warn about foreign-table partitions, which have no local storage and
+	 * would otherwise be skipped silently.  When an index is specified, the
+	 * walk below covers the index's partition tree, which cannot contain
+	 * foreign tables, so walk the table's partition tree here in both cases.
+	 */
+	tableoids = find_all_inheritors(RelationGetRelid(rel), NoLock, NULL);
+	foreach_oid(tableoid, tableoids)
+	{
+		if (get_rel_relkind(tableoid) == RELKIND_FOREIGN_TABLE)
+			ereport(WARNING,
+			/*- translator: second %s is the name of a SQL command, eg. REPACK */
+					errmsg("skipping \"%s\" --- cannot execute %s on foreign tables",
+						   get_rel_name(tableoid),
+						   RepackCommandAsString(stmt->command)));
+	}
+
 	/*
 	 * Do not lock the children until they're processed.  Note that we do hold
 	 * a lock on the parent partitioned table.
diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out
index d1bc8a13286..d1507e9ce8e 100644
--- a/src/test/regress/expected/cluster.out
+++ b/src/test/regress/expected/cluster.out
@@ -537,6 +537,28 @@ SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM o
  clstrpart33 |     2 | r       | f
 (7 rows)
 
+-- Check that partitions that are foreign tables are skipped, with a warning
+CREATE FOREIGN DATA WRAPPER clstr_fdw;
+CREATE SERVER clstr_fserv FOREIGN DATA WRAPPER clstr_fdw;
+CREATE FOREIGN TABLE clstrpart4 PARTITION OF clstrpart FOR VALUES FROM (20) TO (30) SERVER clstr_fserv;
+CLUSTER clstrpart USING clstrpart_idx;
+WARNING:  skipping "clstrpart4" --- cannot execute CLUSTER on foreign tables
+REPACK clstrpart USING INDEX clstrpart_idx;
+WARNING:  skipping "clstrpart4" --- cannot execute REPACK on foreign tables
+REPACK clstrpart;
+WARNING:  skipping "clstrpart4" --- cannot execute REPACK on foreign tables
+-- ... while processing a foreign table directly is still an error
+REPACK clstrpart4;
+ERROR:  "clstrpart4" is not a table or materialized view
+-- ... and a parent with only foreign partitions is not a silent no-op
+CREATE TABLE clstrpart_foreign (a int) PARTITION BY RANGE (a);
+CREATE FOREIGN TABLE clstrpart_foreign1 PARTITION OF clstrpart_foreign FOR VALUES FROM (1) TO (10) SERVER clstr_fserv;
+REPACK clstrpart_foreign;
+WARNING:  skipping "clstrpart_foreign1" --- cannot execute REPACK on foreign tables
+DROP TABLE clstrpart_foreign;
+DROP FOREIGN TABLE clstrpart4;
+DROP SERVER clstr_fserv;
+DROP FOREIGN DATA WRAPPER clstr_fdw;
 -- Ownership of partitions is checked
 CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i);
 CREATE INDEX ptnowner_i_idx ON ptnowner(i);
diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql
index e7a62367adf..6776988897f 100644
--- a/src/test/regress/sql/cluster.sql
+++ b/src/test/regress/sql/cluster.sql
@@ -248,6 +248,24 @@ REPACK clstrpart;
 CREATE TEMP TABLE new_cluster_info AS SELECT relname, level, relfilenode, relkind FROM pg_partition_tree('clstrpart'::regclass) AS tree JOIN pg_class c ON c.oid=tree.relid ;
 SELECT relname, old.level, old.relkind, old.relfilenode = new.relfilenode FROM old_cluster_info AS old JOIN new_cluster_info AS new USING (relname) ORDER BY relname COLLATE "C";
 
+-- Check that partitions that are foreign tables are skipped, with a warning
+CREATE FOREIGN DATA WRAPPER clstr_fdw;
+CREATE SERVER clstr_fserv FOREIGN DATA WRAPPER clstr_fdw;
+CREATE FOREIGN TABLE clstrpart4 PARTITION OF clstrpart FOR VALUES FROM (20) TO (30) SERVER clstr_fserv;
+CLUSTER clstrpart USING clstrpart_idx;
+REPACK clstrpart USING INDEX clstrpart_idx;
+REPACK clstrpart;
+-- ... while processing a foreign table directly is still an error
+REPACK clstrpart4;
+-- ... and a parent with only foreign partitions is not a silent no-op
+CREATE TABLE clstrpart_foreign (a int) PARTITION BY RANGE (a);
+CREATE FOREIGN TABLE clstrpart_foreign1 PARTITION OF clstrpart_foreign FOR VALUES FROM (1) TO (10) SERVER clstr_fserv;
+REPACK clstrpart_foreign;
+DROP TABLE clstrpart_foreign;
+DROP FOREIGN TABLE clstrpart4;
+DROP SERVER clstr_fserv;
+DROP FOREIGN DATA WRAPPER clstr_fdw;
+
 -- Ownership of partitions is checked
 CREATE TABLE ptnowner(i int unique not null) PARTITION BY LIST (i);
 CREATE INDEX ptnowner_i_idx ON ptnowner(i);
-- 
2.55.0

