From bec0384109927a3d65320cf395cbb970b6d9f05c Mon Sep 17 00:00:00 2001
From: Dilip Kumar <dilipkumarb@google.com>
Date: Tue, 16 Dec 2025 14:02:55 +0530
Subject: [PATCH v1] Add relispublishable column to pg_class catalog

Previously, determining if a relation was eligible for logical
replication was performed dynamically via is_publishable_class().
This function relied on hard-coded OID checks and and relkind that
is performed at runtime.

This patch change by adding a "relispublishable" boolean column to
pg_class.  With this we don't need to check OID and relkind dynamically
we can just rely on ispublishable field.

Author: Dilip Kumar based on suggestion by Amit Kapila
---
 src/backend/bootstrap/bootparse.y           |  1 +
 src/backend/catalog/heap.c                  |  4 ++
 src/backend/catalog/pg_publication.c        | 49 ++-------------------
 src/backend/catalog/toasting.c              |  1 +
 src/backend/commands/cluster.c              |  1 +
 src/backend/commands/tablecmds.c            | 15 +++++++
 src/backend/replication/pgoutput/pgoutput.c |  4 +-
 src/backend/utils/cache/relcache.c          |  2 +-
 src/include/catalog/heap.h                  |  1 +
 src/include/catalog/pg_class.h              |  3 ++
 src/include/catalog/pg_publication.h        |  1 -
 11 files changed, 33 insertions(+), 49 deletions(-)

diff --git a/src/backend/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 9833f52c1be..01e4bd301e6 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -243,6 +243,7 @@ Boot_CreateStmt:
 													  false,
 													  true,
 													  false,
+													  false,
 													  InvalidOid,
 													  NULL);
 						elog(DEBUG4, "relation created with OID %u", id);
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 265cc3e5fbf..5760794853c 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -953,6 +953,7 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relrewrite - 1] = ObjectIdGetDatum(rd_rel->relrewrite);
 	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
 	values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid);
+	values[Anum_pg_class_relispublishable - 1] = BoolGetDatum(rd_rel->relispublishable);
 	if (relacl != (Datum) 0)
 		values[Anum_pg_class_relacl - 1] = relacl;
 	else
@@ -1138,6 +1139,7 @@ heap_create_with_catalog(const char *relname,
 						 bool use_user_acl,
 						 bool allow_system_table_mods,
 						 bool is_internal,
+						 bool ispublisable,
 						 Oid relrewrite,
 						 ObjectAddress *typaddress)
 {
@@ -1329,6 +1331,8 @@ heap_create_with_catalog(const char *relname,
 	Assert(relid == RelationGetRelid(new_rel_desc));
 
 	new_rel_desc->rd_rel->relrewrite = relrewrite;
+	new_rel_desc->rd_rel->relispublishable = (ispublisable &&
+											relid >= FirstNormalObjectId);
 
 	/*
 	 * Decide whether to create a pg_type entry for the relation's rowtype.
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 7aa3f179924..13aa7b273b3 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -111,47 +111,6 @@ check_publication_add_schema(Oid schemaid)
 				 errdetail("Temporary schemas cannot be replicated.")));
 }
 
-/*
- * Returns if relation represented by oid and Form_pg_class entry
- * is publishable.
- *
- * Does same checks as check_publication_add_relation() above except for
- * RELKIND_SEQUENCE, but does not need relation to be opened and also does
- * not throw errors. Here, the additional check is to support ALL SEQUENCES
- * publication.
- *
- * XXX  This also excludes all tables with relid < FirstNormalObjectId,
- * ie all tables created during initdb.  This mainly affects the preinstalled
- * information_schema.  IsCatalogRelationOid() only excludes tables with
- * relid < FirstUnpinnedObjectId, making that test rather redundant,
- * but really we should get rid of the FirstNormalObjectId test not
- * IsCatalogRelationOid.  We can't do so today because we don't want
- * information_schema tables to be considered publishable; but this test
- * is really inadequate for that, since the information_schema could be
- * dropped and reloaded and then it'll be considered publishable.  The best
- * long-term solution may be to add a "relispublishable" bool to pg_class,
- * and depend on that instead of OID checks.
- */
-static bool
-is_publishable_class(Oid relid, Form_pg_class reltuple)
-{
-	return (reltuple->relkind == RELKIND_RELATION ||
-			reltuple->relkind == RELKIND_PARTITIONED_TABLE ||
-			reltuple->relkind == RELKIND_SEQUENCE) &&
-		!IsCatalogRelationOid(relid) &&
-		reltuple->relpersistence == RELPERSISTENCE_PERMANENT &&
-		relid >= FirstNormalObjectId;
-}
-
-/*
- * Another variant of is_publishable_class(), taking a Relation.
- */
-bool
-is_publishable_relation(Relation rel)
-{
-	return is_publishable_class(RelationGetRelid(rel), rel->rd_rel);
-}
-
 /*
  * SQL-callable variant of the above
  *
@@ -169,7 +128,7 @@ pg_relation_is_publishable(PG_FUNCTION_ARGS)
 	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
 	if (!HeapTupleIsValid(tuple))
 		PG_RETURN_NULL();
-	result = is_publishable_class(relid, (Form_pg_class) GETSTRUCT(tuple));
+	result = ((Form_pg_class) GETSTRUCT(tuple))->relispublishable;
 	ReleaseSysCache(tuple);
 	PG_RETURN_BOOL(result);
 }
@@ -890,7 +849,7 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 		Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 		Oid			relid = relForm->oid;
 
-		if (is_publishable_class(relid, relForm) &&
+		if (relForm->relispublishable &&
 			!(relForm->relispartition && pubviaroot))
 			result = lappend_oid(result, relid);
 	}
@@ -911,7 +870,7 @@ GetAllPublicationRelations(char relkind, bool pubviaroot)
 			Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
 			Oid			relid = relForm->oid;
 
-			if (is_publishable_class(relid, relForm) &&
+			if (relForm->relispublishable &&
 				!relForm->relispartition)
 				result = lappend_oid(result, relid);
 		}
@@ -1018,7 +977,7 @@ GetSchemaPublicationRelations(Oid schemaid, PublicationPartOpt pub_partopt)
 		Oid			relid = relForm->oid;
 		char		relkind;
 
-		if (!is_publishable_class(relid, relForm))
+		if (!relForm->relispublishable)
 			continue;
 
 		relkind = get_rel_relkind(relid);
diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c
index 874a8fc89ad..429ba2dcae7 100644
--- a/src/backend/catalog/toasting.c
+++ b/src/backend/catalog/toasting.c
@@ -263,6 +263,7 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
 										   false,
 										   true,
 										   true,
+										   false,
 										   OIDOldToast,
 										   NULL);
 	Assert(toast_relid != InvalidOid);
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 2120c85ccb4..7528ae9e2e3 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -774,6 +774,7 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 										  false,
 										  true,
 										  true,
+										  false,
 										  OIDOldHeap,
 										  NULL);
 	Assert(OIDNewHeap != InvalidOid);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 953fadb9c6b..22d508c3ed7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -790,6 +790,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	ObjectAddress address;
 	LOCKMODE	parentLockmode;
 	Oid			accessMethodId = InvalidOid;
+	bool		ispublishable;
 
 	/*
 	 * Truncate relname to appropriate length (probably a waste of time, as
@@ -1051,6 +1052,18 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			accessMethodId = get_table_am_oid(default_table_access_method, false);
 	}
 
+	/*
+	 * Determine if the relation is eligible for logical replication.
+	 *
+	 * To be publishable, the relation must be a regular table, a partitioned
+	 * table, or a sequence.  Additionally, it must be a permanent relation
+	 * created during normal processing to exclude system catalogs.
+	 */
+	ispublishable = ((relkind == RELKIND_RELATION ||
+					 relkind == RELKIND_PARTITIONED_TABLE ||
+					 relkind == RELKIND_SEQUENCE) &&
+					 stmt->relation->relpersistence == RELPERSISTENCE_PERMANENT);
+
 	/*
 	 * Create the relation.  Inherited defaults and CHECK constraints are
 	 * passed in for immediate handling --- since they don't need parsing,
@@ -1076,6 +1089,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 										  true,
 										  allowSystemTableMods,
 										  false,
+										  ispublishable,
 										  InvalidOid,
 										  typaddress);
 
@@ -22529,6 +22543,7 @@ createPartitionTable(List **wqueue, RangeVar *newPartName,
 										true,
 										allowSystemTableMods,
 										true,
+										parent_rel->rd_rel->relispublishable,
 										InvalidOid,
 										NULL);
 
diff --git a/src/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 787998abb8a..ead0b393d87 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -1491,7 +1491,7 @@ pgoutput_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 	TupleTableSlot *old_slot = NULL;
 	TupleTableSlot *new_slot = NULL;
 
-	if (!is_publishable_relation(relation))
+	if (!relation->rd_rel->relispublishable)
 		return;
 
 	/*
@@ -1675,7 +1675,7 @@ pgoutput_truncate(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
 		Relation	relation = relations[i];
 		Oid			relid = RelationGetRelid(relation);
 
-		if (!is_publishable_relation(relation))
+		if (!relation->rd_rel->relispublishable)
 			continue;
 
 		relentry = get_rel_sync_entry(data, relation);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 2d0cb7bcfd4..8d0596a9c8f 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5804,7 +5804,7 @@ RelationBuildPublicationDesc(Relation relation, PublicationDesc *pubdesc)
 	 * If not publishable, it publishes no actions.  (pgoutput_change() will
 	 * ignore it.)
 	 */
-	if (!is_publishable_relation(relation))
+	if (!relation->rd_rel->relispublishable)
 	{
 		memset(pubdesc, 0, sizeof(PublicationDesc));
 		pubdesc->rf_valid_for_update = true;
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index dbd339e9df4..3ecdb670e2c 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -83,6 +83,7 @@ extern Oid	heap_create_with_catalog(const char *relname,
 									 bool use_user_acl,
 									 bool allow_system_table_mods,
 									 bool is_internal,
+									 bool ispublishable,
 									 Oid relrewrite,
 									 ObjectAddress *typaddress);
 
diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h
index 07d182da796..bf959094a85 100644
--- a/src/include/catalog/pg_class.h
+++ b/src/include/catalog/pg_class.h
@@ -122,6 +122,9 @@ CATALOG(pg_class,1259,RelationRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83,Relat
 	/* is relation a partition? */
 	bool		relispartition BKI_DEFAULT(f);
 
+	/* is relation publishable */
+	bool		relispublishable BKI_DEFAULT(f);
+
 	/* link to original rel during table rewrite; otherwise 0 */
 	Oid			relrewrite BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_class);
 
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 22f48bb8975..9c4613b62af 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -183,7 +183,6 @@ extern List *GetPubPartitionOptionRelations(List *result,
 extern Oid	GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
 											int *ancestor_level);
 
-extern bool is_publishable_relation(Relation rel);
 extern bool is_schema_publication(Oid pubid);
 extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
 										MemoryContext mcxt, Bitmapset **cols);
-- 
2.49.0

