Subject: Re: Initial COPY of Logical Replication is too slow

From: Marcos Pegoraro <[email-masked]>
Date: Mon, 19 Jan 2026 14:44:18 -0300

Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email-masked]>
escreveu:

> Yeah, if we pass a publication that a lot of tables belong to to
> pg_get_publication_tables(), it could take a long time to return as it
> needs to construct many entries.

Well, I don't know how to help but I'm sure it's working badly.
Today I added some fields on my server, then seeing logs I could see how
slow this process is.

duration: 2213.872 ms  statement: SELECT DISTINCT  (CASE WHEN
(array_length(gpt.attrs, 1) = c.relnatts)   THEN NULL ELSE gpt.attrs END)
 FROM pg_publication p,  LATERAL pg_get_publication_tables(p.pubname) gpt,
 pg_class c WHERE gpt.relid = 274376788 AND c.oid = gpt.relid   AND
p.pubname IN ( 'mypub' )

2 seconds to get the list of fields of a table is really too slow.
How can we solve this ?

regards
Marcos

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 26 Jan 2026 12:30:03 -0800

On Mon, Jan 19, 2026 at 9:44 AM Marcos Pegoraro <[email-masked]> wrote:
>
> Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email-masked]> escreveu:
>>
> Today I added some fields on my server, then seeing logs I could see how slow this process is.
>
> duration: 2213.872 ms  statement: SELECT DISTINCT  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)   THEN NULL ELSE gpt.attrs END)  FROM pg_publication p,  LATERAL pg_get_publication_tables(p.pubname) gpt,  pg_class c WHERE gpt.relid = 274376788 AND c.oid = gpt.relid   AND p.pubname IN ( 'mypub' )
>

After more investigation of slowness, it seems that the
list_concat_unique_oid() called below is quite slow when the database
has a lot of tables to publish:

    relids = GetPublicationRelations(pub_elem->oid,
                                     pub_elem->pubviaroot ?
                                     PUBLICATION_PART_ROOT :
                                     PUBLICATION_PART_LEAF);
    schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
                                                     pub_elem->pubviaroot ?
                                                     PUBLICATION_PART_ROOT :
                                                     PUBLICATION_PART_LEAF);
    pub_elem_tables = list_concat_unique_oid(relids, schemarelids);

This is simply because it's O(n^2), where n is the number of oids in
schemarelids in the test case. A simple change would be to do sort &
dedup instead. With the attached experimental patch, the
pg_get_publication_tables() execution time gets halved in my
environment (796ms -> 430ms with 50k tables). If the number of tables
is not large, this method might be slower than today but it's not a
huge regression.

In the initial tablesync cases, it could be optimized further in a way
that we introduce a new SQL function that gets the column list and
expr of the specific table. This way, we can filter the result by
relid at an early stage instead of getting all information and
filtering by relid as the tablesync worker does today, avoiding
overheads of gathering system catalog scan results.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 25 Feb 2026 11:03:08 -0800

> overheads of gathering system catalog scan results.

I've drafted this idea and I find it looks like a better approach. The
patch introduces the pg_get_publication_table_info() SQL function that
returns the column list and row filter expression like
pg_get_publication_tables() returns but it checks only the specific
table unlike pg_get_publication_tables(). On my env, the tablesync
worker's query in question becomes 0.6ms from 288 ms with 50k tables
in one publication. Feedback is very welcome.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 54af2b794d741865fd06e97738b7fdb34e29b17e Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Wed, 25 Feb 2026 10:56:45 -0800
Subject: [PATCH] Add pg_get_publication_table_info() to optimize logical
 replication tablesync.

---
 src/backend/catalog/pg_publication.c        | 222 +++++++++++++++++++-
 src/backend/replication/logical/tablesync.c |   9 +-
 src/include/catalog/pg_proc.dat             |   9 +
 3 files changed, 234 insertions(+), 6 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9a4791c573e..0a3015ffc91 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1116,6 +1116,111 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 	return OidIsValid(oid) ? GetPublication(oid) : NULL;
 }
 
+/*
+ * pg_get_publication_tables() and pg_get_publication_table_info() use
+ * the same record type.
+ */
+#define NUM_PUBLICATION_TABLES_ELEM 4
+
+/*
+ * Common routine for pg_get_publication_tables() and
+ * pg_get_publication_table_info() to construct the result tuple.
+ */
+static HeapTuple
+construct_published_rel_tuple(published_rel *table_info, TupleDesc tuple_desc)
+{
+	Publication *pub;
+	Oid		   relid = table_info->relid;
+	Oid		   schemaid = get_rel_namespace(relid);
+	HeapTuple   pubtuple = NULL;
+	Datum	   values[NUM_PUBLICATION_TABLES_ELEM] = {0};
+	bool		   nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
+
+	pub = GetPublication(table_info->pubid);
+
+	values[0] = ObjectIdGetDatum(pub->oid);
+	values[1] = ObjectIdGetDatum(relid);
+
+	values[0] = ObjectIdGetDatum(pub->oid);
+	values[1] = ObjectIdGetDatum(relid);
+
+	/*
+	 * We don't consider row filters or column lists for FOR ALL TABLES or
+	 * FOR TABLES IN SCHEMA publications.
+	 */
+	if (!pub->alltables &&
+		!SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+							   ObjectIdGetDatum(schemaid),
+							   ObjectIdGetDatum(pub->oid)))
+		pubtuple = SearchSysCacheCopy2(PUBLICATIONRELMAP,
+									   ObjectIdGetDatum(relid),
+									   ObjectIdGetDatum(pub->oid));
+
+	if (HeapTupleIsValid(pubtuple))
+	{
+		/* Lookup the column list attribute. */
+		values[2] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+									Anum_pg_publication_rel_prattrs,
+									&(nulls[2]));
+
+		/* Null indicates no filter. */
+		values[3] = SysCacheGetAttr(PUBLICATIONRELMAP, pubtuple,
+									Anum_pg_publication_rel_prqual,
+									&(nulls[3]));
+	}
+	else
+	{
+		nulls[2] = true;
+		nulls[3] = true;
+	}
+
+	/* Show all columns when the column list is not specified. */
+	if (nulls[2])
+	{
+		Relation	   rel = table_open(relid, AccessShareLock);
+		int		   nattnums = 0;
+		int16	  *attnums;
+		TupleDesc   desc = RelationGetDescr(rel);
+		int		   i;
+
+		attnums = palloc_array(int16, desc->natts);
+
+		for (i = 0; i < desc->natts; i++)
+		{
+			Form_pg_attribute att = TupleDescAttr(desc, i);
+
+			if (att->attisdropped)
+				continue;
+
+			if (att->attgenerated)
+			{
+				/* We only support replication of STORED generated cols. */
+				if (att->attgenerated != ATTRIBUTE_GENERATED_STORED)
+					continue;
+
+				/*
+				 * User hasn't requested to replicate STORED generated
+				 * cols.
+				 */
+				if (pub->pubgencols_type != PUBLISH_GENCOLS_STORED)
+					continue;
+			}
+
+			attnums[nattnums++] = att->attnum;
+		}
+
+		if (nattnums > 0)
+		{
+			values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
+			nulls[2] = false;
+		}
+
+		table_close(rel, AccessShareLock);
+	}
+
+	return heap_form_tuple(tuple_desc, values, nulls);
+}
+
 /*
  * Get information of the tables in the given publication array.
  *
@@ -1124,7 +1229,6 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 Datum
 pg_get_publication_tables(PG_FUNCTION_ARGS)
 {
-#define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
 	List	   *table_infos = NIL;
 
@@ -1342,6 +1446,122 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+/*
+ * Similar to pg_get_publication_tables(), but retrieves publication
+ * information only for the specified table. This function is useful for
+ * obtaining the column filter list and row filter expression for a specific
+ * table without processing all tables in a publication. It is significantly
+ * faster than pg_get_publication_tables() because it avoids constructing
+ * a list of all table OIDs.
+ */
+Datum
+pg_get_publication_table_info(PG_FUNCTION_ARGS)
+{
+  FuncCallContext *funcctx;
+  published_rel *table_info = NULL;
+
+  if (SRF_IS_FIRSTCALL())
+  {
+	  TupleDesc   tupdesc;
+	  MemoryContext oldcontext;
+	  Oid         relid;
+	  Name        pubname;
+	  Relation    rel;
+	  Publication *pub;
+	  bool        publish = false;
+	  published_rel *pubrel = NULL;
+
+	  /* create a function context for cross-call persistence */
+	  funcctx = SRF_FIRSTCALL_INIT();
+
+	  /* switch to memory context appropriate for multiple function calls */
+	  oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+	  relid = PG_GETARG_OID(0);
+	  pubname = PG_GETARG_NAME(1);
+
+	  rel = table_open(relid, AccessShareLock);
+	  pub = GetPublicationByName(NameStr(*pubname), false);
+
+	  /*
+	   * Verify that the specified table is published by the given
+	   * publication.
+	   */
+	  if (pub->alltables)
+	  {
+		  /* ALL TALBES publication */
+		  publish = true;
+	  }
+	  else if (!pub->pubviaroot && rel->rd_rel->relispartition)
+	  {
+		  List *ancestors = get_partition_ancestors(RelationGetRelid(rel));
+
+		  /*
+		   * Check if its ancestor is in the specified publication
+		   * as publications with publish_via_partition_root being false
+		   * create pg_publication_rel entries only for the top most
+		   * partitioned table.
+		   */
+		  if (OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors,
+														 NULL)))
+			  publish = true;
+	  }
+	  else if (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									 ObjectIdGetDatum(RelationGetRelid(rel)),
+									 ObjectIdGetDatum(pub->oid)) ||
+			   SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									 ObjectIdGetDatum(RelationGetNamespace(rel)),
+									 ObjectIdGetDatum(pub->oid)))
+	  {
+		  /*
+		   * Looks for the entry in pg_publication_rel or
+		   * pg_publication_namespace
+		   */
+		  publish = true;
+	  }
+
+	  table_close(rel, AccessShareLock);
+
+	  /* Construct a tuple descriptor for the result rows. */
+	  tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
+	  TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
+						 OIDOID, -1, 0);
+	  TupleDescInitEntry(tupdesc, (AttrNumber) 2, "relid",
+						 OIDOID, -1, 0);
+	  TupleDescInitEntry(tupdesc, (AttrNumber) 3, "attrs",
+						 INT2VECTOROID, -1, 0);
+	  TupleDescInitEntry(tupdesc, (AttrNumber) 4, "qual",
+						 PG_NODE_TREEOID, -1, 0);
+
+	  if (publish)
+	  {
+		  pubrel = palloc_object(published_rel);
+		  pubrel->relid = relid;
+		  pubrel->pubid = pub->oid;
+	  }
+
+	  funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+	  funcctx->user_fctx = pubrel;
+
+	  MemoryContextSwitchTo(oldcontext);
+  }
+
+  /* stuff done on every call of the function */
+  funcctx = SRF_PERCALL_SETUP();
+  table_info = (published_rel *) funcctx->user_fctx;
+
+  if (table_info && funcctx->call_cntr == 0)
+  {
+	  HeapTuple   rettuple;
+
+	  rettuple = construct_published_rel_tuple(table_info, funcctx->tuple_desc);
+
+	  SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(rettuple));
+  }
+
+  SRF_RETURN_DONE(funcctx);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 2f2f0121ecf..5331eb034b0 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -801,9 +801,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
 						 "   THEN NULL ELSE gpt.attrs END)"
 						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+						 "  LATERAL pg_get_publication_table_info(%u, p.pubname) gpt,"
 						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+						 " WHERE c.oid = gpt.relid"
 						 "   AND p.pubname IN ( %s )",
 						 lrel->remoteid,
 						 pub_names->data);
@@ -983,9 +983,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
 						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
+						 "  LATERAL pg_get_publication_table_info(%u, p.pubname) gpt"
+						 " WHERE p.pubname IN ( %s )",
 						 lrel->remoteid,
 						 pub_names->data);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..3cd6004d7dc 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12388,6 +12388,15 @@
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
   prosrc => 'pg_get_publication_tables' },
+{ oid => '9761',
+  descr => 'get information of the table that is part of the specified publication',
+  proname => 'pg_get_publication_table_info', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => 'oid name',
+  proallargtypes => '{oid,name,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{relid,pubname,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_table_info' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
-- 
2.53.0

---

From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:47:33 -0800

> in one publication. Feedback is very welcome.

Another variant of this approach is to extend
pg_get_publication_table() so that it can accept a relid to get the
publication information of the specific table. I've attached the patch
for this idea. I'm going to add regression test cases.

pg_get_publication_table() is a VARIACID array function so the patch
changes its signature to {text[] [, oid]}, breaking the tool
compatibility. Given this function is mostly an internal-use function
(we don't have the documentation for it), it would probably be okay
with it. I find it's clearer than the other approach of introducing
pg_get_publication_table_info(). Feedback is very welcome.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 152b798903cd181b4e9b5ca39409d5616ade1bbd Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH] Avoid full table scans when getting publication table
 information by tablesync workers.

Author:
Reviewed-by:
Discussion: https://postgr.es/m/
---
 src/backend/catalog/pg_publication.c        | 147 ++++++++++++++++----
 src/backend/catalog/system_views.sql        |   2 +-
 src/backend/commands/subscriptioncmds.c     |   4 +-
 src/backend/replication/logical/tablesync.c |   9 +-
 src/include/catalog/pg_proc.dat             |  15 +-
 5 files changed, 138 insertions(+), 39 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9a4791c573e..2d48580ad9a 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1116,13 +1116,83 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 	return OidIsValid(oid) ? GetPublication(oid) : NULL;
 }
 
+/*
+ * Returns true if the table of the given relid is published by the publication.
+ *
+ * Note that being published here means we actually use its OID as the published
+ * table OID, which depends on publication's publish_via_partition_root value.
+ * For example, even if pg_publication_rel has the entry for the parent table,
+ * this function returns false as we use its leaf partitions' OIDs as the
+ * published OIDs.
+ */
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	if (pub->pubviaroot)
+	{
+		/*
+		 * For ALL TABLES publication with pubviaroot, the table is published
+		 * if not a partition.
+		 */
+		if (pub->alltables)
+			return !get_rel_relispartition(relid);
+
+		/*
+		 * For pubviaroot publications, we can simply check if the given
+		 * relation's OIS exists on either pg_publication_rel or
+		 * pg_publication_namespace.
+		 */
+		return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(relid),
+									  ObjectIdGetDatum(pub->oid)) ||
+				SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									  ObjectIdGetDatum(get_rel_namespace(relid)),
+									  ObjectIdGetDatum(pub->oid)));
+	}
+
+	/*
+	 * For non-pubviaroot publications, partitioned table's OID can never be a
+	 * published OID.
+	 */
+	if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	if (pub->alltables)
+		return true;
+
+	/*
+	 * For the partition in the !pubviaroot publication, we need to check its
+	 * ancestors instead of the given relation itself.
+	 */
+	if (get_rel_relispartition(relid))
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors,
+															  NULL);
+
+		return OidIsValid(topmost);
+	}
+
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
 /*
  * Get information of the tables in the given publication array.
  *
  * Returns pubid, relid, column list, row filter for each table.
+ *
+ * If relid is an valid OID, it returns only these information of the table
+ * of the given relid instead of all tables in the given publication array,
+ * returning at most one tuple.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, Oid relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1161,29 +1231,38 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
-															 pub_elem->pubviaroot);
+			if (OidIsValid(relid))
+			{
+				if (is_table_publishable_in_publication(relid, pub_elem))
+					pub_elem_tables = list_make1_oid(relid);
+			}
 			else
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetPublicationRelations(pub_elem->oid,
-												 pub_elem->pubviaroot ?
-												 PUBLICATION_PART_ROOT :
-												 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetPublicationRelations(pub_elem->oid,
+													 pub_elem->pubviaroot ?
+													 PUBLICATION_PART_ROOT :
+													 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 			}
 
 			/*
@@ -1246,8 +1325,8 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		HeapTuple	rettuple;
 		Publication *pub;
 		published_rel *table_info = (published_rel *) list_nth(table_infos, funcctx->call_cntr);
-		Oid			relid = table_info->relid;
-		Oid			schemaid = get_rel_namespace(relid);
+		Oid			tableoid = table_info->relid;
+		Oid			schemaid = get_rel_namespace(tableoid);
 		Datum		values[NUM_PUBLICATION_TABLES_ELEM] = {0};
 		bool		nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
 
@@ -1258,7 +1337,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		pub = GetPublication(table_info->pubid);
 
 		values[0] = ObjectIdGetDatum(pub->oid);
-		values[1] = ObjectIdGetDatum(relid);
+		values[1] = ObjectIdGetDatum(tableoid);
 
 		/*
 		 * We don't consider row filters or column lists for FOR ALL TABLES or
@@ -1269,7 +1348,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 								   ObjectIdGetDatum(schemaid),
 								   ObjectIdGetDatum(pub->oid)))
 			pubtuple = SearchSysCacheCopy2(PUBLICATIONRELMAP,
-										   ObjectIdGetDatum(relid),
+										   ObjectIdGetDatum(tableoid),
 										   ObjectIdGetDatum(pub->oid));
 
 		if (HeapTupleIsValid(pubtuple))
@@ -1293,7 +1372,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* Show all columns when the column list is not specified. */
 		if (nulls[2])
 		{
-			Relation	rel = table_open(relid, AccessShareLock);
+			Relation	rel = table_open(tableoid, AccessShareLock);
 			int			nattnums = 0;
 			int16	   *attnums;
 			TupleDesc	desc = RelationGetDescr(rel);
@@ -1342,6 +1421,18 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	return pg_get_publication_tables(fcinfo, InvalidOid);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	return pg_get_publication_tables(fcinfo, PG_GETARG_OID(1));
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..0c867cf0bf0 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -411,7 +411,7 @@ CREATE VIEW pg_publication_tables AS
         ) AS attnames,
         pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
     FROM pg_publication P,
-         LATERAL pg_get_publication_tables(P.pubname) GPT,
+         LATERAL pg_get_publication_tables(ARRAY[P.pubname]) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
     WHERE C.oid = GPT.relid;
 
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 5e3c0964d38..0bf7db71d5a 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -2577,7 +2577,7 @@ check_publications_origin_tables(WalReceiverConn *wrconn, List *publications,
 	appendStringInfoString(&cmd,
 						   "SELECT DISTINCT P.pubname AS pubname\n"
 						   "FROM pg_publication P,\n"
-						   "     LATERAL pg_get_publication_tables(P.pubname) GPT\n"
+						   "     LATERAL pg_get_publication_tables(ARRAY[P.pubname]) GPT\n"
 						   "     JOIN pg_subscription_rel PS ON (GPT.relid = PS.srrelid OR"
 						   "     GPT.relid IN (SELECT relid FROM pg_partition_ancestors(PS.srrelid) UNION"
 						   "                   SELECT relid FROM pg_partition_tree(PS.srrelid))),\n"
@@ -2956,7 +2956,7 @@ fetch_relation_list(WalReceiverConn *wrconn, List *publications)
 		appendStringInfo(&cmd, "SELECT DISTINCT n.nspname, c.relname, c.relkind, gpt.attrs\n"
 						 "   FROM pg_class c\n"
 						 "         JOIN pg_namespace n ON n.oid = c.relnamespace\n"
-						 "         JOIN ( SELECT (pg_get_publication_tables(VARIADIC array_agg(pubname::text))).*\n"
+						 "         JOIN ( SELECT (pg_get_publication_tables(array_agg(pubname::text))).*\n"
 						 "                FROM pg_publication\n"
 						 "                WHERE pubname IN ( %s )) AS gpt\n"
 						 "             ON gpt.relid = c.oid\n",
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index 2f2f0121ecf..a7f52755d05 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -801,9 +801,9 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
 						 "   THEN NULL ELSE gpt.attrs END)"
 						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+						 "  LATERAL pg_get_publication_tables(ARRAY[p.pubname], %u) gpt,"
 						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+						 " WHERE c.oid = gpt.relid"
 						 "   AND p.pubname IN ( %s )",
 						 lrel->remoteid,
 						 pub_names->data);
@@ -983,9 +983,8 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		appendStringInfo(&cmd,
 						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
 						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
+						 "  LATERAL pg_get_publication_tables(ARRAY[p.pubname], %u) gpt"
+						 " WHERE  p.pubname IN ( %s )",
 						 lrel->remoteid,
 						 pub_names->data);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..f6b775fe25b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12382,12 +12382,21 @@
 { oid => '6119',
   descr => 'get information of the tables that are part of the specified publications',
   proname => 'pg_get_publication_tables', prorows => '1000',
-  provariadic => 'text', proretset => 't', provolatile => 's',
+  proretset => 't', provolatile => 's',
   prorettype => 'record', proargtypes => '_text',
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
-  proargmodes => '{v,o,o,o,o}',
+  proargmodes => '{i,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the tables that are part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
-- 
2.53.0

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Tue, 3 Mar 2026 10:22:29 +0000

On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email-masked]> wrote:
> To: Marcos Pegoraro <[email-masked]>
> Cc: PostgreSQL Hackers <[email-masked]>
> Subject: Re: Initial COPY of Logical Replication is too slow
> 
> pg_get_publication_table() so that it can accept a relid to get the publication
> information of the specific table. I've attached the patch for this idea. I'm
> going to add regression test cases.
> 
> pg_get_publication_table() is a VARIACID array function so the patch changes
> its signature to {text[] [, oid]}, breaking the tool compatibility. Given this
> function is mostly an internal-use function (we don't have the documentation
> for it), it would probably be okay with it. I find it's clearer than the other
> approach of introducing pg_get_publication_table_info(). Feedback is very
> welcome.

Thanks for updating the patch.

I have few comments for the function change:

1.

If we change the function signature, will it affect use cases where the
publisher version is newer and the subscriber version is older ? E.g., when
publisher is passing text style publication name to pg_get_publication_tables().

Besides, for upgrade scenarios where the publisher version is older, I think
the patch needs to add version checks to avoid passing the relid to
pg_get_publication_tables.

2.

In the following example, I expected it to output a table with valid row
filter, but it returns 0 row after applying the patch.

CREATE TABLE measurements (
    city_id         int not null,
    logdate         date not null,
    peaktemp        int,
    unitsales       int
) PARTITION BY RANGE (logdate);

-- Create partitions
CREATE TABLE measurements_2023_q1 PARTITION OF measurements
    FOR VALUES FROM ('2023-01-01') TO ('2023-04-01');

CREATE PUBLICATION pub FOR TABLE measurements_2023_q1 WHERE (city_id = 2);

select pg_get_publication_tables(ARRAY['pub2'], 'measurements_2023_q1'::regclass);
 pg_get_publication_tables
---------------------------
(0 rows)

Best Regards,
Hou zj

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 9 Mar 2026 15:09:38 -0700

> publisher is passing text style publication name to pg_get_publication_tables().

Good point.

I noticed that changing the function signature of
pg_get_publication_tables() breaks logical replication setups where
the subscriber is 18 or older. In the latest patch, I've switched the
approach back to the pg_get_publication_table_info() idea.

>  pg_get_publication_tables

Thank you for testing the patch. I've fixed it and added regression
tests in the latest patch.

I've attached the updated patch.

Regards,

---

From: Amit Kapila <[email-masked]>
Date: Wed, 18 Mar 2026 19:26:10 +0530

On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email-masked]> wrote:
> the subscriber is 18 or older.
>

Why adding a new function with additional parameters (Oid relid)
couldn't help with such a case? I am asking because your previous
version code looks simpler as compared to the new patch version.

-- 
With Regards,
Amit Kapila.

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 18 Mar 2026 09:44:46 -0700

> version code looks simpler as compared to the new patch version.

I tried to pass a relid to pg_get_publication_tables() but we cannot
avoid changing its signature because it's a VARIADIC array function.
The previous patch changed pg_get_publication_tables(VARIADIC text[])
to pg_get_publication_tables(text[] {, relid}). However, changing the
function signature would break the logical replication from v19 to an
older version.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 18 Mar 2026 15:31:19 -0700

On Wed, Mar 18, 2026 at 1:11 PM Jan Wieck <[email-masked]> wrote:
>
> On 3/18/26 12:44, Masahiko Sawada wrote:
> Would it be possible to use function overloading to provide both
> signatures handled by different C functions internally?

Yes, we can define both pg_get_publication_tables(VARIADIC text[]) and
pg_get_publication_tables(text, oid), which seems like a less invasive
approach. I'll give this idea a shot and see how it goes.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 18 Mar 2026 16:29:10 -0700

> approach. I'll give this idea a shot and see how it goes.

I've attached the patch to implement this idea. The patch still
introduces a new function but it overloads
pg_get_publication_tables(). We might be able to handle different
input (array or text) in pg_get_publication_tables() better, but it's
enough for discussion at least.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From c3647910dadb645895cea1ef15c11aaa6cd25d18 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v3] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email-masked]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email-masked]>
Reviewed-by: Matheus Alcantara <[email-masked]>
Reviewed-by: Chao Li <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/pg_publication.c        | 294 +++++++++++++++-----
 src/backend/replication/logical/tablesync.c |  74 +++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 131 +++++++++
 src/test/regress/sql/publication.sql        |  69 +++++
 5 files changed, 490 insertions(+), 89 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a79157c43bf..5f687491a4e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1208,12 +1208,129 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
+ */
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	if (pub->pubviaroot)
+	{
+		if (pub->alltables)
+		{
+			/*
+			 * ALL TABLE publications with pubviaroot=true include only tables
+			 * that are either regular tables or top-most partitioned tables.
+			 */
+			if (get_rel_relispartition(relid))
+				return false;
+
+			/*
+			 * Check if the table is specified in the EXCEPT clause in the
+			 * publication. ALL TABLE publications have pg_publication_rel
+			 * entries only for EXCEPT'ed tables, so it's sufficient to check
+			 * the existence of its entry.
+			 */
+			return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+										  ObjectIdGetDatum(relid),
+										  ObjectIdGetDatum(pub->oid));
+		}
+
+		/*
+		 * Check if its corresponding entry exists either in
+		 * pg_publication_rel or pg_publication_namespace.
+		 */
+		return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(relid),
+									  ObjectIdGetDatum(pub->oid)) ||
+				SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+									  ObjectIdGetDatum(get_rel_namespace(relid)),
+									  ObjectIdGetDatum(pub->oid)));
+	}
+
+	/*
+	 * For non-pubviaroot publications, partitioned table's OID can never be a
+	 * published OID.
+	 */
+	if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (get_rel_relispartition(relid))
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it's specified in the EXCEPT clause.
+		 * ALL TABLE publications have pg_publication_rel entries only for
+		 * EXCEPT'ed tables, so it's sufficient to check the existence of its
+		 * entry.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	if (get_rel_relispartition(relid))
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors,
+															  NULL);
+
+		list_free(ancestors);
+
+		/* This table is published if its ancestor is published */
+		if (OidIsValid(topmost))
+			return true;
+
+		/* The partition itself might be published, so check below */
+	}
+
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * The parameters pubnames and {pubname, target_relid} are mutually exclusive.
+ * If target_relid is provided, the function returns information only for that
+ * specific table. Otherwise, if returns information for all tables within the
+ * specified publications.
  *
  * Returns pubid, relid, column list, row filter for each table.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  text *pubname, Oid target_relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1224,11 +1341,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
-		Datum	   *elems;
-		int			nelems,
-					i;
-		bool		viaroot = false;
 
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
@@ -1236,81 +1348,111 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* switch to memory context appropriate for multiple function calls */
 		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
 
-		/*
-		 * Deconstruct the parameter into elements where each element is a
-		 * publication name.
-		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
-
-		/* Get Oids of tables from each publication. */
-		for (i = 0; i < nelems; i++)
+		if (pubname != NULL)
 		{
-			Publication *pub_elem;
-			List	   *pub_elem_tables = NIL;
-			ListCell   *lc;
+			Publication *pub;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			Assert(OidIsValid(target_relid));
+			pub = GetPublicationByName(text_to_cstring(pubname), false);
 
-			/*
-			 * Publications support partitioned tables. If
-			 * publish_via_partition_root is false, all changes are replicated
-			 * using leaf partition identity and schema, so we only need
-			 * those. Otherwise, get the partitioned table itself.
-			 */
-			if (pub_elem->alltables)
-				pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
-															 RELKIND_RELATION,
-															 pub_elem->pubviaroot);
-			else
+			if (is_table_publishable_in_publication(target_relid, pub))
 			{
-				List	   *relids,
-						   *schemarelids;
-
-				relids = GetIncludedPublicationRelations(pub_elem->oid,
-														 pub_elem->pubviaroot ?
-														 PUBLICATION_PART_ROOT :
-														 PUBLICATION_PART_LEAF);
-				schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
-																pub_elem->pubviaroot ?
-																PUBLICATION_PART_ROOT :
-																PUBLICATION_PART_LEAF);
-				pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				published_rel *table_info = palloc_object(published_rel);
+
+				table_info->relid = target_relid;
+				table_info->pubid = pub->oid;
+				table_infos = lappend(table_infos, table_info);
 			}
+		}
+		else
+		{
+			Datum	   *elems;
+			int			nelems,
+						i;
+			bool		viaroot = false;
+
+			Assert(pubnames != NULL);
 
 			/*
-			 * Record the published table and the corresponding publication so
-			 * that we can get row filters and column lists later.
-			 *
-			 * When a table is published by multiple publications, to obtain
-			 * all row filters and column lists, the structure related to this
-			 * table will be recorded multiple times.
+			 * Deconstruct the parameter into elements where each element is a
+			 * publication name.
 			 */
-			foreach(lc, pub_elem_tables)
+			deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+			/* Get Oids of tables from each publication. */
+			for (i = 0; i < nelems; i++)
 			{
-				published_rel *table_info = palloc_object(published_rel);
+				Publication *pub_elem;
+				List	   *pub_elem_tables = NIL;
+				ListCell   *lc;
+
+				pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+
+				/*
+				 * Publications support partitioned tables. If
+				 * publish_via_partition_root is false, all changes are
+				 * replicated using leaf partition identity and schema, so we
+				 * only need those. Otherwise, get the partitioned table
+				 * itself.
+				 */
+				if (pub_elem->alltables)
+					pub_elem_tables = GetAllPublicationRelations(pub_elem->oid,
+																 RELKIND_RELATION,
+																 pub_elem->pubviaroot);
+				else
+				{
+					List	   *relids,
+							   *schemarelids;
+
+					relids = GetIncludedPublicationRelations(pub_elem->oid,
+															 pub_elem->pubviaroot ?
+															 PUBLICATION_PART_ROOT :
+															 PUBLICATION_PART_LEAF);
+					schemarelids = GetAllSchemaPublicationRelations(pub_elem->oid,
+																	pub_elem->pubviaroot ?
+																	PUBLICATION_PART_ROOT :
+																	PUBLICATION_PART_LEAF);
+					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+				}
 
-				table_info->relid = lfirst_oid(lc);
-				table_info->pubid = pub_elem->oid;
-				table_infos = lappend(table_infos, table_info);
+				/*
+				 * Record the published table and the corresponding
+				 * publication so that we can get row filters and column lists
+				 * later.
+				 *
+				 * When a table is published by multiple publications, to
+				 * obtain all row filters and column lists, the structure
+				 * related to this table will be recorded multiple times.
+				 */
+				foreach(lc, pub_elem_tables)
+				{
+					published_rel *table_info = palloc_object(published_rel);
+
+					table_info->relid = lfirst_oid(lc);
+					table_info->pubid = pub_elem->oid;
+					table_infos = lappend(table_infos, table_info);
+				}
+
+				/*
+				 * At least one publication is using
+				 * publish_via_partition_root.
+				 */
+				if (pub_elem->pubviaroot)
+					viaroot = true;
 			}
 
-			/* At least one publication is using publish_via_partition_root. */
-			if (pub_elem->pubviaroot)
-				viaroot = true;
+			/*
+			 * If the publication publishes partition changes via their
+			 * respective root partitioned tables, we must exclude partitions
+			 * in favor of including the root partitioned tables. Otherwise,
+			 * the function could return both the child and parent tables
+			 * which could cause data of the child table to be
+			 * double-published on the subscriber side.
+			 */
+			if (viaroot)
+				filter_partitions(table_infos);
 		}
 
-		/*
-		 * If the publication publishes partition changes via their respective
-		 * root partitioned tables, we must exclude partitions in favor of
-		 * including the root partitioned tables. Otherwise, the function
-		 * could return both the child and parent tables which could cause
-		 * data of the child table to be double-published on the subscriber
-		 * side.
-		 */
-		if (viaroot)
-			filter_partitions(table_infos);
-
 		/* Construct a tuple descriptor for the result rows. */
 		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
@@ -1435,6 +1577,20 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the tables in the given publications */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), NULL, InvalidOid);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/* Get the information of the specified table in the given publication */
+	return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0), PG_GETARG_OID(1));
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..884b56bb26c 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,37 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table_info() since
+			 * version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1002,30 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
 		/* Check for row filters. */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
-						 " WHERE gpt.relid = %u"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass relid to pg_get_publication_table_info() since
+			 * version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt"
+							 " WHERE p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt"
+							 " WHERE gpt.relid = %u"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		res = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data, 1, qualRow);
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..294ee717a6d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12453,7 +12453,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publication',
+  proname => 'pg_get_publication_tables', prorows => '1',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => 'text oid',
+  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 681d2564ed5..91c339bd278 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2182,6 +2182,137 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_novia_root | tbl_part1 | 1 2 3 | 
+(1 row)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR:  type "relname" does not exist
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 405579dad52..0f3f1400abe 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1378,6 +1378,75 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text, oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_novia_root FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubname text, relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+
+SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_novia_root;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_novia_root;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0

---

From: Bharath Rupireddy <[email-masked]>
Date: Mon, 23 Mar 2026 17:53:59 -0700

Hi,

> enough for discussion at least.

Overall, the intent of this patch looks good to me. It avoids the cost
of the table sync worker querying all the pg_publication_rel tables to
filter them out later in the join.

I quickly reviewed the patch and here are some comments:

1/ Typo: s/pg_get_publication_table_info/pg_get_publication_tables

2/ I think it's good to have some quick numbers on how the query
latency looks for pre-V19 and the new one that the table sync worker
executes on the publisher, say, with 100, 1000, and 10000 tables at
least.

3/ + Assert(OidIsValid(target_relid));

Why not error out (by treating it as function input parameter
validation) when target_relid is invalid because asserts go unnoticed
on production systems?

--
Bharath Rupireddy

---

From: Ajin Cherian <[email-masked]>
Date: Tue, 24 Mar 2026 17:54:09 +1100

> enough for discussion at least.

The patch looks like a good performance improvement. Some minor comments:

1. src/test/regress/expected/publication.out

+-- Clean up

Cleanup actually fails. Second parameter should be text, not relname.

2. src/include/catalog/pg_proc.dat

Having two arguments with the same name "relid" seems odd, although
one is input and other is output parameter, how about calling input
parameter as target_relid?

3. src/backend/replication/logical/tablesync.c

+

In multiple places in the code pg_get_publication_table_info() is
used, instead of pg_get_publication_tables()

thanks,
Ajin Cherian
Fujitsu Australia

---

From: Peter Smith <[email-masked]>
Date: Tue, 24 Mar 2026 17:59:25 +1100

Hi Sawada-San.

Here are some review comments for the v3-0001 test code.

======
src/test/regress/expected/publication.out

1.
+-- Clean up

This seems a mistake. If the DROP FUNCTION was differently written
there there would be no error. PSA.

======
src/test/regress/sql/publication.sql

2.

These tests seem strangely different from all the others because
everything else has both a "good result" test and a "no result" test
for every publication.

So I think there should be a "no result" test for 'pub_normal'

So I think there should be a "good result" test for 'pub_schema'

~~~

3.
Consider renaming that 'tbl_parent' to something like 'tbl_root'.
because 'parent' always makes me think of INHERITED parent/child
tables, rather than partitioned tables and their partitions. If you do
this, then you might also want to tweak several publication names --
e.g. 'pub_part_parent' -> 'pub_part_root'

~~~

PSA a diff file that makes those suggested changes #1 and #2.

======
Kind Regards,
Peter Smith.
Fujitsu Australia
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 3dded67ab98..60033ea2fff 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2307,6 +2307,17 @@ SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
  pub_normal | tbl_normal | 1     | (id < 10)
 (1 row)
 
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
@@ -2389,8 +2400,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 (1 row)
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
-ERROR:  type "relname" does not exist
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 10fd97fe544..9801179e645 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1463,6 +1463,9 @@ BEGIN ATOMIC
 END;
 
 SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_normal', 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
 SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
 
 SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
@@ -1486,7 +1489,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
 SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 
 -- Clean up
-DROP FUNCTION test_gpt(text[], relname);
+DROP FUNCTION test_gpt(pubname text, relname text);
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;

---

From: Amit Kapila <[email-masked]>
Date: Tue, 24 Mar 2026 16:17:03 +0530

*
+ /*

Why in the above query we need a join with pg_publication? Can't we
directly pass 'pub_names' and 'relid' to pg_get_publication_tables()
to get the required information?

--
With Regards,
Amit Kapila.

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 11:41:24 -0700

> I quickly reviewed the patch and here are some comments:

Thank you for reviewing the patch!

> 1/ Typo: s/pg_get_publication_table_info/pg_get_publication_tables

Fixed.

> executes on the publisher, say, with 100, 1000, and 10000 tables at

You can refer to the performance test results that I previously
shared[1]. The patch I used was somewhat different from the current
patch but the performance trend should be similar as the both are
using the same approach.

> validation) when target_relid is invalid because asserts go unnoticed

Agreed. It would return no row if the specified relid is invalid or
there is no corresponding table.

I'll share the updated patch soon.

Regards,

[1] https://www.postgresql.org/message-id/CAD21AoDQM62GOtaTzD_CVMSsFhv6o9c0Au1dSM1QuxeKFkWAKw%40mail.gmail.com

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 11:42:10 -0700

> used, instead of pg_get_publication_tables()

Thank you for reviewing the patch! I agree with all the above points.
I'll share the updated patch soon.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 11:45:13 -0700

Fixed.

> So I think there should be a "good result" test for 'pub_schema'

Added.

> e.g. 'pub_part_parent' -> 'pub_part_root'

Hmm, there are already some queries using 'parent' in the same .sql
file. I'll leave these names.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 11:57:50 -0700

> to get the required information?

Since the 'pub_names' is the list of publication names we cannot
directly pass it to the pg_get_publication_tables(). But if we make
pg_get_publication_tables() take {pubname text[], target_relid oid}
instead of {pubname text, target_relid oid}, yes. And it seems to help
somewhat simplify the patch. If having both
pg_get_publication_tables(VARIADIC text[]) and
pg_get_publication_tables(text[], oid) is not odd, it would be worth
trying it.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 22:06:54 -0700

> pg_get_publication_tables(text[], oid) is not odd, it would be worth

I figured out that the join with pg_publication works as a filter;
non-existence publication names are not passed to the function. If we
pass the list of publication names to the new function signature,
while we can simplify the patch and avoid a join, we would change the
existing function behavior so that it ignores non-existence
publications.

I've attached the updated patch. The 0001 patch just incorporated the
review comments so far, and the 0002 patch is a draft change for the
above idea. Since pg_get_publication_tables(VARIADIC text) is not a
documented function, I think we can accept small behavior changes. So
I'm going to go with this direction. Feedback is very welcome.

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 2bcf744710589e88bfcdb370ba5c2b098cbdae9c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Tue, 24 Mar 2026 20:59:26 -0700
Subject: [PATCH v4 2/2] POC: pass the list of publications to
 pg_get_publication_tables().

---
 src/backend/catalog/pg_publication.c        | 140 ++++++++++----------
 src/backend/replication/logical/tablesync.c |  26 ++--
 src/include/catalog/pg_proc.dat             |   6 +-
 src/test/regress/expected/publication.out   |  62 ++++++---
 src/test/regress/sql/publication.sql        |  49 ++++---
 5 files changed, 154 insertions(+), 129 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index f4649dbd8b9..181f999916c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1377,7 +1377,6 @@ is_table_publishable_in_publication(Oid relid, Publication *pub)
  * Helper function to get information of the tables in the given
  * publication(s).
  *
- * The parameters pubnames and {pubname, target_relid} are mutually exclusive.
  * If target_relid is provided, the function returns information only for that
  * specific table. Otherwise, if returns information for all tables within the
  * specified publications.
@@ -1386,7 +1385,7 @@ is_table_publishable_in_publication(Oid relid, Publication *pub)
  */
 static Datum
 pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
-						  text *pubname, Oid target_relid)
+						  Oid target_relid)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1397,6 +1396,10 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
+		Datum	   *elems;
+		int			nelems,
+					i;
+		bool		viaroot = false;
 
@@ -1404,49 +1407,37 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 
-		if (pubname != NULL)
-		{
-			/* Try to retrieve the specified table information */
-			if (SearchSysCacheExists1(RELOID, target_relid))
-			{
-				Publication *pub;
-
-				pub = GetPublicationByName(text_to_cstring(pubname), false);
+		Assert(pubnames != NULL);
 
-				if (is_table_publishable_in_publication(target_relid, pub))
-				{
-					published_rel *table_info = palloc_object(published_rel);
+		/*
+		 * Deconstruct the parameter into elements where each element is a
+		 * publication name.
+		 */
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
 
-					table_info->relid = target_relid;
-					table_info->pubid = pub->oid;
-					table_infos = lappend(table_infos, table_info);
-				}
-			}
-		}
-		else
+		/* Get Oids of tables from each publication. */
+		for (i = 0; i < nelems; i++)
 		{
-			Datum	   *elems;
-			int			nelems,
-						i;
-			bool		viaroot = false;
+			Publication *pub_elem;
+			List	   *pub_elem_tables = NIL;
+			ListCell   *lc;
 
-			Assert(pubnames != NULL);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), true);
 
-			/*
-			 * Deconstruct the parameter into elements where each element is a
-			 * publication name.
-			 */
-			deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+			if (pub_elem == NULL)
+				continue;
 
-			/* Get Oids of tables from each publication. */
-			for (i = 0; i < nelems; i++)
+			if (OidIsValid(target_relid))
+			{
+				/* Try to retrieve the specified table information */
+				if (SearchSysCacheExists1(RELOID, target_relid) &&
+					is_table_publishable_in_publication(target_relid, pub_elem))
+				{
+					pub_elem_tables = list_make1_oid(target_relid);
+				}
+			}
+			else
 			{
-				Publication *pub_elem;
-				List	   *pub_elem_tables = NIL;
-				ListCell   *lc;
-
-				pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
-
 				/*
 				 * Publications support partitioned tables. If
 				 * publish_via_partition_root is false, all changes are
@@ -1473,45 +1464,45 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
 																	PUBLICATION_PART_LEAF);
 					pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
 				}
+			}
 
-				/*
-				 * Record the published table and the corresponding
-				 * publication so that we can get row filters and column lists
-				 * later.
-				 *
-				 * When a table is published by multiple publications, to
-				 * obtain all row filters and column lists, the structure
-				 * related to this table will be recorded multiple times.
-				 */
-				foreach(lc, pub_elem_tables)
-				{
-					published_rel *table_info = palloc_object(published_rel);
-
-					table_info->relid = lfirst_oid(lc);
-					table_info->pubid = pub_elem->oid;
-					table_infos = lappend(table_infos, table_info);
-				}
+			/*
+			 * Record the published table and the corresponding
+			 * publication so that we can get row filters and column lists
+			 * later.
+			 *
+			 * When a table is published by multiple publications, to
+			 * obtain all row filters and column lists, the structure
+			 * related to this table will be recorded multiple times.
+			 */
+			foreach(lc, pub_elem_tables)
+			{
+				published_rel *table_info = palloc_object(published_rel);
 
-				/*
-				 * At least one publication is using
-				 * publish_via_partition_root.
-				 */
-				if (pub_elem->pubviaroot)
-					viaroot = true;
+				table_info->relid = lfirst_oid(lc);
+				table_info->pubid = pub_elem->oid;
+				table_infos = lappend(table_infos, table_info);
 			}
 
 			/*
-			 * If the publication publishes partition changes via their
-			 * respective root partitioned tables, we must exclude partitions
-			 * in favor of including the root partitioned tables. Otherwise,
-			 * the function could return both the child and parent tables
-			 * which could cause data of the child table to be
-			 * double-published on the subscriber side.
+			 * At least one publication is using
+			 * publish_via_partition_root.
 			 */
-			if (viaroot)
-				filter_partitions(table_infos);
+			if (pub_elem->pubviaroot)
+				viaroot = true;
 		}
 
+		/*
+		 * If the publication publishes partition changes via their
+		 * respective root partitioned tables, we must exclude partitions
+		 * in favor of including the root partitioned tables. Otherwise,
+		 * the function could return both the child and parent tables
+		 * which could cause data of the child table to be
+		 * double-published on the subscriber side.
+		 */
+		if (viaroot)
+			filter_partitions(table_infos);
+
 		/* Construct a tuple descriptor for the result rows. */
 		tupdesc = CreateTemplateTupleDesc(NUM_PUBLICATION_TABLES_ELEM);
 		TupleDescInitEntry(tupdesc, (AttrNumber) 1, "pubid",
@@ -1640,14 +1631,21 @@ Datum
 pg_get_publication_tables_a(PG_FUNCTION_ARGS)
 {
 	/* Get the information of the tables in the given publications */
-	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), NULL, InvalidOid);
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
 }
 
 Datum
 pg_get_publication_tables_b(PG_FUNCTION_ARGS)
 {
+	Oid relid = PG_GETARG_OID(1);
+
+	if (!OidIsValid(relid))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid relation OID %u", relid)));
+
 	/* Get the information of the specified table in the given publication */
-	return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0), PG_GETARG_OID(1));
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), relid);
 }
 
 /*
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index ec8840ebf42..d70c172e0f5 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -802,20 +802,18 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		if (server_version >= 190000)
 		{
 			/*
-			 * We can pass relid to pg_get_publication_table() since version
-			 * 19.
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_table() since version 19.
 			 */
 			appendStringInfo(&cmd,
 							 "SELECT DISTINCT"
 							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
 							 "   THEN NULL ELSE gpt.attrs END)"
-							 "  FROM pg_publication p,"
-							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt,"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
 							 "  pg_class c"
-							 " WHERE c.oid = gpt.relid"
-							 "   AND p.pubname IN ( %s )",
-							 lrel->remoteid,
-							 pub_names->data);
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
 		}
 		else
 			appendStringInfo(&cmd,
@@ -1006,16 +1004,14 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		if (server_version >= 190000)
 		{
 			/*
-			 * We can pass relid to pg_get_publication_table() since version
-			 * 19.
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_table() since version 19.
 			 */
 			appendStringInfo(&cmd,
 							 "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
-							 "  FROM pg_publication p,"
-							 "  LATERAL pg_get_publication_tables(p.pubname, %u) gpt"
-							 " WHERE p.pubname IN ( %s )",
-							 lrel->remoteid,
-							 pub_names->data);
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+							 pub_names->data,
+							 lrel->remoteid);
 		}
 		else
 			appendStringInfo(&cmd,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6c23f36495f..33729d9573a 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12473,10 +12473,10 @@
   descr => 'get information of the specified table that is part of the specified publication',
   proname => 'pg_get_publication_tables', prorows => '1',
   proretset => 't', provolatile => 's',
-  prorettype => 'record', proargtypes => 'text oid',
-  proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{i,i,o,o,o,o}',
-  proargnames => '{pubname,target_relid,pubid,relid,attrs,qual}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
   prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 2c859de6c5e..c5f8e045307 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2287,7 +2287,7 @@ CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition
 CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
 CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
 RESET client_min_messages;
-CREATE FUNCTION test_gpt(pubname text, relname text)
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
 RETURNS TABLE (
   pubname text,
   relname name,
@@ -2296,104 +2296,125 @@ RETURNS TABLE (
 )
 BEGIN ATOMIC
   SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
-    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
     JOIN pg_publication p ON p.oid = gpt.pubid
     JOIN pg_class c ON c.oid = gpt.relid
   ORDER BY p.pubname, c.relname;
 END;
-SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
   pubname   |  relname   | attrs |   qual    
 ------------+------------+-------+-----------
  pub_normal | tbl_normal | 1     | (id < 10)
 (1 row)
 
-SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
   pubname   | relname | attrs | qual 
 ------------+---------+-------+------
  pub_schema | tbl_sch | 1     | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
      pubname     |  relname   | attrs |    qual    
 -----------------+------------+-------+------------
  pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
 (1 row)
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_part1');
           pubname           |  relname  | attrs | qual 
 ----------------------------+-----------+-------+------
  pub_part_parent_novia_root | tbl_part1 | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
     pubname    |  relname  | attrs | qual 
 ---------------+-----------+-------+------
  pub_part_leaf | tbl_part1 | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all', 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
  pubname |  relname   | attrs | qual 
 ---------+------------+-------+------
  pub_all | tbl_parent | 1 2 3 | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_novia_root'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, partitions are excluded
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
     pubname     |  relname   | attrs | qual 
 ----------------+------------+-------+------
  pub_all_except | tbl_normal | 1     | 
 (1 row)
 
-SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result (excluded)
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_parent'); -- no result
  pubname | relname | attrs | qual 
 ---------+---------+-------+------
 (0 rows)
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_part1');
       pubname       |  relname  | attrs | qual 
 --------------------+-----------+-------+------
  pub_all_novia_root | tbl_part1 | 1 2 3 | 
@@ -2401,6 +2422,7 @@ SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
 
 -- Clean up
 DROP FUNCTION test_gpt(text, text);
+ERROR:  function test_gpt(text, text) does not exist
 DROP PUBLICATION pub_all;
 DROP PUBLICATION pub_all_novia_root;
 DROP PUBLICATION pub_all_except;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index c1c83f7d701..2016c0aac08 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1447,7 +1447,7 @@ CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 =
 CREATE PUBLICATION pub_part_parent_novia_root FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
 RESET client_min_messages;
 
-CREATE FUNCTION test_gpt(pubname text, relname text)
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
 RETURNS TABLE (
   pubname text,
   relname name,
@@ -1456,37 +1456,46 @@ RETURNS TABLE (
 )
 BEGIN ATOMIC
   SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
-    FROM pg_get_publication_tables(pubname, relname::regclass::oid) gpt
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
     JOIN pg_publication p ON p.oid = gpt.pubid
     JOIN pg_class c ON c.oid = gpt.relid
   ORDER BY p.pubname, c.relname;
 END;
 
-SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
-SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
 
-SELECT * FROM test_gpt('pub_schema', 'gpt_test_sch.tbl_sch');
-SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
 
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_parent');
-SELECT * FROM test_gpt('pub_part_parent', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
 
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_part_parent_novia_root', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_novia_root'], 'tbl_part1');
 
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_part_leaf', 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
 
-SELECT * FROM test_gpt('pub_all', 'tbl_parent');
-SELECT * FROM test_gpt('pub_all', 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
 
-SELECT * FROM test_gpt('pub_all_except', 'tbl_normal');
-SELECT * FROM test_gpt('pub_all_except', 'gpt_test_sch.tbl_sch'); -- no result (excluded)
-SELECT * FROM test_gpt('pub_all_except', 'tbl_parent'); -- no result (excluded)
-SELECT * FROM test_gpt('pub_all_except', 'tbl_part1'); -- no result (excluded)
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
 
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_parent'); -- no result
-SELECT * FROM test_gpt('pub_all_novia_root', 'tbl_part1');
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_novia_root'], 'tbl_parent');
+
+-- no result, partitions are excluded
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result (excluded)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_parent'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_novia_root'], 'tbl_part1');
 
 -- Clean up
 DROP FUNCTION test_gpt(text, text);
-- 
2.53.0

From adb8822ddd5fe1f5f616d31512930d62358a272c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v4 1/2] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email-masked]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email-masked]>
Reviewed-by: Matheus Alcantara <[email-masked]>
Reviewed-by: Chao Li <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/pg_publication.c        | 299 +++++++++++++++-----
 src/backend/replication/logical/tablesync.c |  74 +++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 141 +++++++++
 src/test/regress/sql/publication.sql        |  72 +++++
 5 files changed, 507 insertions(+), 90 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..f4649dbd8b9 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1264,12 +1264,129 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
@@ -1280,11 +1397,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
@@ -1292,81 +1404,114 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
-		/*
-		 * Deconstruct the parameter into elements where each element is a
-		 * publication name.
-		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
-
-		/* Get Oids of tables from each publication. */
-		for (i = 0; i < nelems; i++)
+		if (pubname != NULL)
 		{
-			Publication *pub_elem;
-			List	   *pub_elem_tables = NIL;
-			ListCell   *lc;
+			/* Try to retrieve the specified table information */
+			if (SearchSysCacheExists1(RELOID, target_relid))
+			{
+				Publication *pub;

-			/*
-			{
 
 			/*

 
-		/*
@@ -1491,6 +1636,20 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
+Datum
index f49a4852ecb..ec8840ebf42 100644
+			 * We can pass relid to pg_get_publication_table() since version
+			 * 19.

+			 * We can pass relid to pg_get_publication_table() since version
+			 * 19.

index 0118e970dda..6c23f36495f 100644
@@ -12468,7 +12468,16 @@
+  proargnames => '{pubname,target_relid,pubid,relid,attrs,qual}',
index a220f48b285..2c859de6c5e 100644
@@ -2271,6 +2271,147 @@ DROP TABLE testpub_merge_pk;
+SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+DROP FUNCTION test_gpt(text, text);
index 22e0a30b5c7..c1c83f7d701 100644
@@ -1429,6 +1429,78 @@ RESET SESSION AUTHORIZATION;
 
+SELECT * FROM test_gpt('pub_normal', 'gpt_test_sch.tbl_sch'); -- no result
+DROP FUNCTION test_gpt(text, text);

---

From: Peter Smith <[email-masked]>
Date: Wed, 25 Mar 2026 19:48:32 +1100

Hi Swada-San. Here are some minor review comments for v4-0001/2 combined.

======
src/backend/catalog/pg_publication.c

is_table_publishable_in_publication:

1.
This function logic has a format like

if (cond)
{
 ...
 return;
}

if (cond2)
{
 ...
 return;
}

etc.

There are many return points, and most of those "if" blocks cannot
fall through (they return).

I found it slightly difficult to read the code because I kept having
to think, "OK, if we reached here, it means pubviaroot must be false,"
or "OK, if we reached this far, then puballtables must be false, and
pubviaroot must be false," etc.

Maybe a few more if/else, or a few Assert() can make it easier to
understand how we reached points deeper in this function.

~~~

pg_get_publication_tables:

2.
Several code comments appear to wrap prematurely. It looks as if
pg_indent was run when the code was different to what it is now.

~~~

pg_get_publication_tables_b:

3.
  /* Get the information of the specified table in the given publication */
- return pg_get_publication_tables(fcinfo, NULL, PG_GETARG_TEXT_P(0),
PG_GETARG_OID(1));
+ return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), relid);

Should that comment be plural now, the same as the other one?
/publication/publications/

======
src/include/catalog/pg_proc.dat

4.
proargnames => '{pubname,pubid,relid,attrs,qual}',
prosrc => 'pg_get_publication_tables_a' },

Shouldn’t the original function proargnames here also be calling the
first arg 'pubnames' instead of 'pubname'

======

5.
-- Test pg_get_publication_tables(text, oid) function

Should that comment now say text[] instead of text?

~~~

6.
Many of those tests have a "good result" test and a "no result" test
for each publication. It might be better if they were consistently in
the same order (eg, good then none, or none then good).

~~~

7.
Only 3 of the tests are passing multiple publications. Maybe those can
be separated from the others (e.g. put last, just to keep all the ones
that look alike together).

======
Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Thu, 26 Mar 2026 05:44:01 +0000

On Wednesday, March 25, 2026 2:07 PM Masahiko Sawada <[email-masked]> wrote:
> On Tue, Mar 24, 2026 at 11:57 AM Masahiko Sawada
> 
> I figured out that the join with pg_publication works as a filter; non-existence
> publication names are not passed to the function. If we pass the list of
> publication names to the new function signature, while we can simplify the
> patch and avoid a join, we would change the existing function behavior so that
> it ignores non-existence publications.
> 
> I've attached the updated patch. The 0001 patch just incorporated the review
> comments so far, and the 0002 patch is a draft change for the above idea. Since
> pg_get_publication_tables(VARIADIC text) is not a documented function, I think
> we can accept small behavior changes. So I'm going to go with this direction.
> Feedback is very welcome.

Thanks for updating the patches. I have few comments for 0001:

1.

if => it

2.

I think the function shall reject relids that do not reference a valid
publishable table (e.g., sequences, views, or materialized views).

3.

With publish_via_root = true, when both a partitioned table and its child are in
a publication, I expected passing the child's relid will return NULL (changes are
published via the root). Currently it returns the child's relid:

CREATE TABLE sales (
    id SERIAL,
    sale_date DATE NOT NULL
) PARTITION BY RANGE (sale_date);

CREATE TABLE sales_2024_q1 PARTITION OF sales
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');

CREATE publication pub for table sales, sales_2024_q1 with ( publish_via_partition_root );

select pg_get_publication_tables('pub', 'sales_2024_q1'::regclass);

Best Regards,
Hou zj

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Thu, 26 Mar 2026 08:35:51 +0000

Dear Sawada-san,
(Sending again because blocked by some rules)

I ran the performance testing independently for the 0001 patch. Overall performance looked
very nice, new function spent O(1) time based on the total number of tables.
It seems good enough.

Source code:
----------------
HEAD (4287c50f) + v4-0001 patch.

Setup:
---------
A database cluster was set up with shared_buffers=100GB. Several tables were
defined on the public schema, and same number of tables were on the sch1.
Total number of tables were {50, 500, 5000, 50000}.
A publication included a schema sch1 and all public tables individually.

Attached script setup the same. The suffix is changed to .txt to pass the rule.

Workload Run:
--------------------
I ran two types of SQLs and measured the execution time via \timing metacommand.
Cases were emulated which tablesync worker would do.

Case 1: old SQL
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname) gpt,
  pg_class c
 WHERE gpt.relid = 17885 AND c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

Case 2: new SQL
```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_publication p,
  LATERAL pg_get_publication_tables(p.pubname, 16535) gpt,
  pg_class c
 WHERE c.oid = gpt.relid
   AND p.pubname IN ( 'pub' );
```

Result Observations:
---------------
Attached bar graph shows the result. A logarithmic scale is used for the execution
time (y-axis) to see both small/large scale case. The spent time became approximately
10x longer for 500->5000, and 5000->50000, in case of old SQL is used.
Apart from that, the spent time for the new SQL is mostly the stable based on the
number of tables.

Detailed Result:
--------------
Each cell are the median of 10 runs.

Total tables	Execution time for the old SQL was done [ms]	Execution time for the old SQL was done [ms]
50		5.77						4.19
500		15.75						4.28
5000		120.39						4.22
50000		1741.89						4.60
500000		73287.16					4.95

Also, here is a small code comment. I think we can have an Assert at the
begining of the pg_get_publication_tables(), something like below.

```
@@ -1392,6 +1392,9 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
        FuncCallContext *funcctx;
        List       *table_infos = NIL;
 
+       Assert((pubnames && (!pubname && !OidIsValid(target_relid))) ||
+                  (!pubnames && (pubname && OidIsValid(target_relid))));
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Amit Kapila <[email-masked]>
Date: Thu, 26 Mar 2026 16:13:50 +0530

On Wed, Mar 25, 2026 at 10:37 AM Masahiko Sawada <[email-masked]> wrote:
> I'm going to go with this direction.
>

What behaviour change are you referring to? In general, the direction
appears right to me.

-- 
With Regards,
Amit Kapila.

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Thu, 26 Mar 2026 12:46:27 +0000

> very nice, new function spent O(1) time based on the total number of tables.

...and I tested 0002 as well with the same settings, and the trend was the same as 0001.

Both 0001 and 0002 were applied and below SQL was run, which was same was what
tablesync worker would try:

```
SELECT DISTINCT
  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)
   THEN NULL ELSE gpt.attrs END)
  FROM pg_get_publication_tables(ARRAY['pub'], 16535) gpt,
  pg_class c
 WHERE c.oid = gpt.relid;
```

And below is the result. Each cell shows the execution time of the SQL. HEAD
column is the case when [1] was done. 0001 column is the case for [2].
Looks like the SQL used by 0002 looks slightly faster, which is same as the
expectation. JOIN was removed once.

Total tables	HEAD [ms]	0001 [ms]	0001 + 0002 [ms]
50		5.77		4.19		3.74
500		15.75		4.28		3.76
5000		120.39		4.22		3.79
50000		1741.89		4.60		4.11
500000		73287.16	4.95		4.38

Attached graph visualized the table.

[1]:
```
SELECT DISTINCT

[2]:
```
SELECT DISTINCT

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Masahiko Sawada <[email-masked]>
Date: Thu, 26 Mar 2026 16:51:14 -0700

> What behaviour change are you referring to? In general, the direction

When passing a non-existent publication name, the current behavior
raises an error while the new behavior does nothing (i.e., the
difference is calling GetPublicationByName() with missing_ok = true or
false).

Regards,

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Fri, 27 Mar 2026 03:16:29 +0000

Dear Sawada-san,

> difference is calling GetPublicationByName() with missing_ok = true or

To confirm; It's because in PG18-, p.pubname was chosen from the pg_publication
in the publisher, but this patch the name list is taken from the subscriber, right?
If some publications are dropped on the publisher, the ERROR could be raised.

For the backward compatibility I suggest switching the policy based on the API
version. E.g.,

```
 static Datum
 pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
-                                                 Oid target_relid)
+                                                 Oid target_relid, bool missing_ok)
...
@@ -1631,7 +1631,7 @@ Datum
 pg_get_publication_tables_a(PG_FUNCTION_ARGS)
 {
        /* Get the information of the tables in the given publications */
-       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid);
+       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid, false);
```

Another comment for publication.sql.

```
-- Clean up

It should be test_gpt(text[], text);

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Amit Kapila <[email-masked]>
Date: Fri, 27 Mar 2026 09:21:38 +0530

> +       return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0), InvalidOid, false);

Sounds like a good idea for backward compatibility.

-- 
With Regards,
Amit Kapila.

---

From: Masahiko Sawada <[email-masked]>
Date: Thu, 26 Mar 2026 23:20:07 -0700

> Sounds like a good idea for backward compatibility.

+1.

I've attached the updated patch. I believe I've addressed all comments
I got so far. In addition to that, I've refactored
is_table_publishable_in_publication() and added more regression tests.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 601c764b27df8994c801d3ced81c5821ba1400c6 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v5] Avoid full table scans when getting publication table
 information by tablesync workers.

Reported-by: Marcos Pegoraro <[email-masked]>
Reviewed-by: Zhijie Hou <[email-masked]>
Reviewed-by: Matheus Alcantara <[email-masked]>
Reviewed-by: Amit Kapila <[email-masked]>
Reviewed-by: Peter Smith <[email-masked]>
Reviewed-by: Hayato Kuroda <[email-masked]>
Reviewed-by: Chao Li <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/pg_publication.c        | 213 +++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 225 ++++++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 ++++++++++
 5 files changed, 576 insertions(+), 50 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..b382e31f7c1 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1264,12 +1264,120 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * Returns true if the table of the given relid is published for the specified
+ * publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and true for its leaf
+ * partitions, even if the parent is the one explicitly added to the publication.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (pub->alltables)
+	{
+		Oid			target_relid = relid;
+
+		if (pub->pubviaroot)
+		{
+			/*
+			 * ALL TABLES with pubviaroot includes only regular tables or
+			 * top-most partitioned tables -- never child partitions.
+			 */
+			if (relispartition)
+				return false;
+		}
+		else if (relispartition)
+		{
+			List	   *ancestors = get_partition_ancestors(relid);
+
+			/*
+			 * Only the top-most ancestor can appear in the EXCEPT clause.
+			 * Therefore, for a partition, exclusion must be evaluated at the
+			 * top-most ancestor.
+			 */
+			target_relid = llast_oid(ancestors);
+			list_free(ancestors);
+		}
+
+		/*
+		 * The table is published unless it appears in the EXCEPT clause. ALL
+		 * TABLES publications store only EXCEPT'ed tables in
+		 * pg_publication_rel, so checking existence is sufficient.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(target_relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-alltables
+	 */
+	if (relispartition)
+	{
+		List	   *ancestors = get_partition_ancestors(relid);
+		Oid			topmost = GetTopMostAncestorInPublication(pub->oid, ancestors, NULL);
+
+		list_free(ancestors);
+
+		if (OidIsValid(topmost))
+		{
+			/*
+			 * If pubviaroot is true, the ancestor is published instead of the
+			 * partition, so exclude it. Otherwise, the ancestor covers the
+			 * partition, so include it.
+			 */
+			return !pub->pubviaroot;
+		}
+
+		/* Ancestor not published; fall through to check the partition itself */
+	}
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,11 +1388,11 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
 		bool		viaroot = false;
+		char		relkind = '\0';
 
@@ -1292,12 +1400,16 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
+		Assert(pubnames != NULL);
+
 		/*
 		 * Deconstruct the parameter into elements where each element is a
 		 * publication name.
 		 */
-		arr = PG_GETARG_ARRAYTYPE_P(0);
-		deconstruct_array_builtin(arr, TEXTOID, &elems, NULL, &nelems);
+		deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+		if (filter_by_relid)
+			relkind = get_rel_relkind(target_relid);
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1418,49 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;
 
-			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+			pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+											pub_missing_ok);
 
-			/*
+			if (filter_by_relid)
+				/* Check if the given table is published for the publication */
+				if ((relkind == RELKIND_RELATION || relkind == RELKIND_PARTITIONED_TABLE) &&
 
 			/*
@@ -1491,6 +1620,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum
+pg_get_publication_tables_a(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for all tables in the given publications.
+	 * filter_by_relid is false so all tables are returned; pub_missing_ok is
+	 * false for backward compatibility.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 InvalidOid, false, false);
+}
+
+Datum
+pg_get_publication_tables_b(PG_FUNCTION_ARGS)
+{
+	/*
+	 * Get information for the specified table in the given publications. The
+	 * SQL-level function is declared STRICT, so target_relid is guaranteed to
+	 * be non-NULL here.
+	 */
+	return pg_get_publication_tables(fcinfo, PG_GETARG_ARRAYTYPE_P(0),
+									 PG_GETARG_OID(1), true, true);
+}
+
 /*
  * Returns Oids of sequences in a publication.
  */
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..eb718114297 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,35 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 		 * publications).
 		 */
 		resetStringInfo(&cmd);
-		appendStringInfo(&cmd,
-						 "SELECT DISTINCT"
-						 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
-						 "   THEN NULL ELSE gpt.attrs END)"
-						 "  FROM pg_publication p,"
-						 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
-						 "  pg_class c"
-						 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
-						 "   AND p.pubname IN ( %s )",
-						 lrel->remoteid,
-						 pub_names->data);
+
+		if (server_version >= 190000)
+		{
+			/*
+			 * We can pass both publication names and relid to
+			 * pg_get_publication_tables() since version 19.
+			 */
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_get_publication_tables(ARRAY[%s], %u) gpt,"
+							 "  pg_class c"
+							 " WHERE c.oid = gpt.relid",
+							 pub_names->data,
+							 lrel->remoteid);
+		}
+		else
+			appendStringInfo(&cmd,
+							 "SELECT DISTINCT"
+							 "  (CASE WHEN (array_length(gpt.attrs, 1) = c.relnatts)"
+							 "   THEN NULL ELSE gpt.attrs END)"
+							 "  FROM pg_publication p,"
+							 "  LATERAL pg_get_publication_tables(p.pubname) gpt,"
+							 "  pg_class c"
+							 " WHERE gpt.relid = %u AND c.oid = gpt.relid"
+							 "   AND p.pubname IN ( %s )",
+							 lrel->remoteid,
+							 pub_names->data);
 
 		pubres = walrcv_exec(LogRepWorkerWalRcvConn, cmd.data,
 							 lengthof(attrsRow), attrsRow);
@@ -982,14 +1000,28 @@ fetch_remote_table_info(char *nspname, char *relname, LogicalRepRelation *lrel,
 
+			 * pg_get_publication_tables() since version 19.

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..7ec43034b74 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12468,7 +12468,16 @@
   proallargtypes => '{_text,oid,oid,int2vector,pg_node_tree}',
   proargmodes => '{v,o,o,o,o}',
   proargnames => '{pubname,pubid,relid,attrs,qual}',
-  prosrc => 'pg_get_publication_tables' },
+  prosrc => 'pg_get_publication_tables_a' },
+{ oid => '8060',
+  descr => 'get information of the specified table that is part of the specified publications',
+  proname => 'pg_get_publication_tables', prorows => '10',
+  proretset => 't', provolatile => 's',
+  prorettype => 'record', proargtypes => '_text oid',
+  proallargtypes => '{_text,oid,oid,oid,int2vector,pg_node_tree}',
+  proargmodes => '{i,i,o,o,o,o}',
+  proargnames => '{pubnames,target_relid,pubid,relid,attrs,qual}',
+  prosrc => 'pg_get_publication_tables_b' },
 { oid => '8052', descr => 'get OIDs of sequences in a publication',
   proname => 'pg_get_publication_sequences', prorows => '1000', proretset => 't',
   provolatile => 's', prorettype => 'oid', proargtypes => 'text',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index a220f48b285..df38d6d45db 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2271,6 +2271,231 @@ DROP TABLE testpub_merge_pk;
 RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_normal | tbl_normal | 1     | (id < 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+  pubname   | relname | attrs | qual 
+------------+---------+-------+------
+ pub_schema | tbl_sch | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+          pubname           |  relname  | attrs | qual 
+----------------------------+-----------+-------+------
+ pub_part_parent_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+    pubname    |  relname  | attrs | qual 
+---------------+-----------+-------+------
+ pub_part_leaf | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+ pubname |  relname   | attrs | qual 
+---------+------------+-------+------
+ pub_all | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+      pubname       |  relname  | attrs | qual 
+--------------------+-----------+-------+------
+ pub_all_no_viaroot | tbl_part1 | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+        pubname        |  relname   | attrs | qual 
+-----------------------+------------+-------+------
+ pub_part_parent_child | tbl_parent | 1 2 3 | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+    pubname     |  relname   | attrs | qual 
+----------------+------------+-------+------
+ pub_all_except | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+          pubname          |  relname   | attrs | qual 
+---------------------------+------------+-------+------
+ pub_all_except_no_viaroot | tbl_normal | 1     | 
+(1 row)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+  pubname   |  relname   | attrs |   qual    
+------------+------------+-------+-----------
+ pub_all    | tbl_normal | 1     | 
+ pub_normal | tbl_normal | 1     | (id < 10)
+(2 rows)
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+     pubname     |  relname   | attrs |    qual    
+-----------------+------------+-------+------------
+ pub_part_parent | tbl_parent | 1 2   | (id1 = 10)
+(1 row)
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+ pubname | relname | attrs | qual 
+---------+---------+-------+------
+(0 rows)
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+ pubid | relid | attrs | qual 
+-------+-------+-------+------
+(0 rows)
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+NOTICE:  drop cascades to table gpt_test_sch.tbl_sch
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 22e0a30b5c7..057e364c753 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1429,6 +1429,113 @@ RESET SESSION AUTHORIZATION;
 DROP ROLE regress_publication_user, regress_publication_user2;
 DROP ROLE regress_publication_user_dummy;
 
+-- Test pg_get_publication_tables(text[], oid) function
+CREATE SCHEMA gpt_test_sch;
+CREATE TABLE gpt_test_sch.tbl_sch (id int);
+CREATE TABLE tbl_normal (id int);
+CREATE TABLE tbl_parent (id1 int, id2 int, id3 int) PARTITION BY RANGE (id1);
+CREATE TABLE tbl_part1 PARTITION OF tbl_parent FOR VALUES FROM (1) TO (10);
+CREATE VIEW gpt_test_view AS SELECT * FROM tbl_normal;
+
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION pub_all FOR ALL TABLES WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_all_no_viaroot FOR ALL TABLES WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT TABLE (tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_schema FOR TABLES IN SCHEMA gpt_test_sch;
+CREATE PUBLICATION pub_normal FOR TABLE tbl_normal WHERE (id < 10);
+CREATE PUBLICATION pub_part_leaf FOR TABLE tbl_part1 WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent FOR TABLE tbl_parent (id1, id2) WHERE (id1 = 10) WITH (publish_via_partition_root = true);
+CREATE PUBLICATION pub_part_parent_no_viaroot FOR TABLE tbl_parent WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_part_parent_child FOR TABLE tbl_parent, tbl_part1 WITH (publish_via_partition_root = true);
+RESET client_min_messages;
+
+CREATE FUNCTION test_gpt(pubnames text[], relname text)
+RETURNS TABLE (
+  pubname text,
+  relname name,
+  attrs text,
+  qual text
+)
+BEGIN ATOMIC
+  SELECT p.pubname, c.relname, gpt.attrs::text, pg_get_expr(gpt.qual, gpt.relid)
+    FROM pg_get_publication_tables(pubnames, relname::regclass::oid) gpt
+    JOIN pg_publication p ON p.oid = gpt.pubid
+    JOIN pg_class c ON c.oid = gpt.relid
+  ORDER BY p.pubname, c.relname;
+END;
+
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_normal'], 'gpt_test_sch.tbl_sch'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'gpt_test_sch.tbl_sch');
+SELECT * FROM test_gpt(ARRAY['pub_schema'], 'tbl_normal'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_part_leaf'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'tbl_part1'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_part1');
+SELECT * FROM test_gpt(ARRAY['pub_all_no_viaroot'], 'tbl_parent'); -- no result
+
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_parent');
+SELECT * FROM test_gpt(ARRAY['pub_part_parent_child'], 'tbl_part1'); -- no result
+
+-- test for the EXCLUDE clause
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except'], 'tbl_part1'); -- no result
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_normal');
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'gpt_test_sch.tbl_sch'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_parent'); -- no result (excluded)
+SELECT * FROM test_gpt(ARRAY['pub_all_except_no_viaroot'], 'tbl_part1'); -- no result
+
+-- two rows with different row filter
+SELECT * FROM test_gpt(ARRAY['pub_all', 'pub_normal'], 'tbl_normal');
+
+-- one row with 'pub_part_parent'
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_part_parent_no_viaroot'], 'tbl_parent');
+
+-- no result, tbl_parent is the effective published OID due to pubviaroot
+SELECT * FROM test_gpt(ARRAY['pub_part_parent', 'pub_all'], 'tbl_part1');
+
+-- no result, non-existent publication
+SELECT * FROM test_gpt(ARRAY['no_such_pub'], 'tbl_normal');
+
+-- no result, non-table object
+SELECT * FROM test_gpt(ARRAY['pub_all'], 'gpt_test_view');
+
+-- no result, empty publication array
+SELECT * FROM test_gpt(ARRAY[]::text[], 'tbl_normal');
+
+-- no result, OID 0 as target_relid
+SELECT * FROM pg_get_publication_tables(ARRAY['pub_normal'], 0::oid);
+
+-- Clean up
+DROP FUNCTION test_gpt(text[], text);
+DROP PUBLICATION pub_all;
+DROP PUBLICATION pub_all_no_viaroot;
+DROP PUBLICATION pub_all_except;
+DROP PUBLICATION pub_all_except_no_viaroot;
+DROP PUBLICATION pub_schema;
+DROP PUBLICATION pub_normal;
+DROP PUBLICATION pub_part_leaf;
+DROP PUBLICATION pub_part_parent;
+DROP PUBLICATION pub_part_parent_no_viaroot;
+DROP PUBLICATION pub_part_parent_child;
+DROP VIEW gpt_test_view;
+DROP TABLE tbl_normal, tbl_parent, tbl_part1;
+DROP SCHEMA gpt_test_sch CASCADE;
+
 -- stage objects for pg_dump tests
 CREATE SCHEMA pubme CREATE TABLE t0 (c int, d int) CREATE TABLE t1 (c int);
 CREATE SCHEMA pubme2 CREATE TABLE t0 (c int, d int);
-- 
2.53.0

---

From: Marcos Pegoraro <[email-masked]>
Date: Fri, 27 Mar 2026 10:07:11 -0300

> is_table_publishable_in_publication() and added more regression tests.

Today I had to create a few more schemas and see that problem again, how
the publisher is affected, almost crashing due to the overload.
That was because max_sync_workers_per_subscription was set to 10, which
caused 10 simultaneous connections to call this function immediately after
the refresh publication command.
Wouldn't it be good to document on this GUC that if your publisher server
is running version <= 18 then is it advisable to set this GUC to a really
low value ?
Because ok, version 19 is fine, will be covered, but all publisher servers
that are not updated will continue to have this trouble.
The publisher will be severely penalized when the subscription refreshes
its publication.

What do you think, change something on DOCs too ?

regards
Marcos

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Mon, 30 Mar 2026 07:16:43 +0000

On Friday, March 27, 2026 2:20 PM Masahiko Sawada <[email-masked]> wrote:
> I've attached the updated patch. I believe I've addressed all comments I got
> so far. In addition to that, I've refactored

The latest patch looks mostly good to me. However, I noticed one issue: the
function returns table information even for unlogged or temporary tables. I
think we should return NULL for those cases instead.

BTW, I think we could use is_publishable_class() as a reference to check once
whether all unpublishable table types are properly ignored in this function.

Best Regards,
Hou zj

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Mon, 30 Mar 2026 07:42:46 +0000

Dear Sawada-san,

Thanks for updating the patch. I think the patch has a good shape.
Below contains minor comments.

```
+               if (filter_by_relid)

Can we return here if the relkind is not RELKIND_RELATION nor RELKIND_PARTITIONED_TABLE?
Key assumption here is that pg_get_publication_tables_b() returns at most one
tuple, thus this is would be called only once.

```
+       /*
+        * Non-alltables
+        */
+       if (relispartition)
```

else-if might be usalbe to clarify we're in the non-alltables case.

```

Personally I prefer to do Assert() before the SRF_FIRSTCALL_INIT(). Because it's
only related with argument and not related with other function calls.

```

Can prorows be 1? Because only a row would be returned here.

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 30 Mar 2026 17:29:01 -0700

> very nice, new function spent O(1) time based on the total number of tables.

Thank you for doing the performance tests! These observation match the
results of my local performance test.

BTW the new is_table_publishable_in_publication() can be useful other
places too where we check if the particular table is published by the
publication, for example get-rel_sync_entry(). It would be a separate
patch though.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 30 Mar 2026 18:00:23 -0700

> Below contains minor comments.

Thank you for the comments!

> tuple, thus this is would be called only once.

Yeah, I refactored these logic and do the preliminary check before
checking the publications.

> else-if might be usalbe to clarify we're in the non-alltables case.

Hmm, we have the return statement at the end of the if branch so we
don't necessarily need else-if. Adding a new line after the comment
might help readability.

> only related with argument and not related with other function calls.

If we move it before the SRF_FIRSTCALL_INIT(), we would end up
executing the assertion every time we call
pg_get_publication_table_b() since it could return more than one
tuple, which seems unnecessary to me. I think we can remove this
assertion because both _a() and _b() are strict functions.

> Can prorows be 1? Because only a row would be returned here.

If multiple publications are specified, it could return more than one tuples.

I'll submit the updated patch soon.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Mon, 30 Mar 2026 21:08:52 -0700

> think we should return NULL for those cases instead.

Indeed. Good catch!

> whether all unpublishable table types are properly ignored in this function.

+1. I've added is_publishable_class() check.

Regards,

-- 
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From b10a79d482e6fe0f178b0ff004767dcbc6bc86db Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v6] Add target_relid parameter to pg_get_publication_tables().

When a tablesync worker checks whether a specific table is published,
it previously called pg_get_publication_tables() and filtered the
result by relid on the subscriber side. This forced a full enumeration
of all tables in the publication before any filtering could occur. For
publications covering a large number of tables, this resulted in
expensive scans on the publisher and unnecessary overhead.

This commit adds a new overloaded form of pg_get_publication_tables()
that accepts an array of publication names and a target table
OID. Instead of enumerating all published tables, it evaluates
membership for the specified relation via syscache lookups, using the
new is_table_publishable_in_publication() helper. This helper
correctly accounts for publish_via_partition_root, ALL TABLES with
EXCEPT clauses, schema publications, and partition inheritance, while
avoiding the overhead of building the complete published table list.

The existing a VARIADIC array form of pg_get_publication_tables() is
preserved for backward compatibility. Tablesync workers use the new
two-argument form when connected to a publisher running PostgreSQL 19
or later.

Bump catalog version.

Reported-by: Marcos Pegoraro <[email-masked]>
Reviewed-by: Zhijie Hou <[email-masked]>
Reviewed-by: Matheus Alcantara <[email-masked]>
Reviewed-by: Amit Kapila <[email-masked]>
Reviewed-by: Peter Smith <[email-masked]>
Reviewed-by: Hayato Kuroda <[email-masked]>
Reviewed-by: Chao Li <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 src/backend/catalog/pg_publication.c        | 246 +++++++++++++++++---
 src/backend/replication/logical/tablesync.c |  70 ++++--
 src/include/catalog/pg_proc.dat             |  11 +-
 src/test/regress/expected/publication.out   | 225 ++++++++++++++++++
 src/test/regress/sql/publication.sql        | 107 +++++++++
 5 files changed, 609 insertions(+), 50 deletions(-)

diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index c92ff3f51c3..0c49b1c69a6 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -163,6 +163,37 @@ is_publishable_relation(Relation rel)
 	return is_publishable_class(RelationGetRelid(rel), rel->rd_rel);
 }
 
+/*
+ * Similar to is_publishable_calss() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
+{
+	HeapTuple	tuple;
+	Form_pg_class relform;
+
+	tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(tableoid));
+	if (!HeapTupleIsValid(tuple))
+		return false;
+
+	relform = (Form_pg_class) GETSTRUCT(tuple);
+
+	/*
+	 * Sequences are publishable according to is_publishable_class() so
+	 * explicitly exclude here.
+	 */
+	if (relform->relkind != RELKIND_SEQUENCE &&
+		is_publishable_class(tableoid, relform))
+	{
+		ReleaseSysCache(tuple);
+		return true;
+	}
+
+	ReleaseSysCache(tuple);
+	return true;
+}
+
 /*
  * SQL-callable variant of the above
  *
@@ -1264,12 +1295,121 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
@@ -1280,7 +1420,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
@@ -1296,8 +1435,15 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
+		 * Preliminary check if the specified table can be published in the
+		 * first place. If not, we can return early without checking the given
+		 * publications and the table.
+		if (filter_by_relid && !is_publishable_table(target_relid))
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1452,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;

-			/*
+				if (is_table_publishable_in_publication(target_relid, pub_elem))
 
 			/*
@@ -1491,6 +1653,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum

 
 
index 3579cec5744..afdcc915f08 100644

---

From: Amit Kapila <[email-masked]>
Date: Tue, 31 Mar 2026 15:06:00 +0530

> pubviaroot must be false," etc.

I can't say exactly why, but I find it difficult to read this
function. So, I share your concerns about the code of this function.
Because of its complexity it is difficult to ascertain that the
functionality is correct or we missed something. Also, considering it
is correct today, in its current form, it may become difficult to
enhance it in future.

One more comment on latest patch:
*
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+                          Oid target_relid, bool filter_by_relid,

Why do we need filter_by_relid as a separate parameter? Isn't the
valid value of target_relid the same? If so, can't we use target_relid
for the required checks?

--
With Regards,
Amit Kapila.

---

From: "Hayato Kuroda (Fujitsu)" <[email-masked]>
Date: Tue, 31 Mar 2026 11:39:01 +0000

Dear Sawada-san,

Thanks for updating the patch! Few comments.

01.
```
+/*

s/is_publishable_calss/is_publishable_class/.

02.
```

Is it correct? I expected to return false here.

03.
```
+               /*

I think we must switch to the old context.

04.
```

It needs to be rebased due to 5984ea86.

05.
```
CREATE VIEW pg_publication_tables AS
    SELECT
        P.pubname AS pubname,
        N.nspname AS schemaname,
        C.relname AS tablename,
        ( SELECT array_agg(a.attname ORDER BY a.attnum)
          FROM pg_attribute a
          WHERE a.attrelid = GPT.relid AND
                a.attnum = ANY(GPT.attrs)
        ) AS attnames,
        pg_get_expr(GPT.qual, GPT.relid) AS rowfilter
     FROM pg_publication P,
          LATERAL pg_get_publication_tables(P.pubname) GPT,
          pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
     WHERE C.oid = GPT.relid;
```

Can we use the new API of pg_get_publication_tables() here? Below change can pass
tests on my env.

```
-         LATERAL pg_get_publication_tables(P.pubname) GPT,
-         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace)
-    WHERE C.oid = GPT.relid;
+         pg_class C JOIN pg_namespace N ON (N.oid = C.relnamespace),
+         LATERAL pg_get_publication_tables(ARRAY[P.pubname], C.oid) GPT;
```

Best regards,
Hayato Kuroda
FUJITSU LIMITED

---

From: "Zhijie Hou (Fujitsu)" <[email-masked]>
Date: Tue, 31 Mar 2026 12:07:42 +0000

On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email-masked]> wrote:
> I can't say exactly why, but I find it difficult to read this function. So, I share
> your concerns about the code of this function.
> Because of its complexity it is difficult to ascertain that the functionality is
> correct or we missed something. Also, considering it is correct today, in its
> current form, it may become difficult to enhance it in future.
> 

I attempted to refactor the code a bit based on my preferred style, as shown in
the attachment. While the number of return points couldn't be reduced, I tried
to eliminate if-else branches where possible. Sharing this top-up patch as a
reference for an alternative style that reduces code size.

Best Regards,
Hou zj

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 31 Mar 2026 10:28:11 -0700

> is correct today, in its current form, it may become difficult to

Okay, I'll refactor that function.

> valid value of target_relid the same? If so, can't we use target_relid

If we don't have filter_by_relid, we would end up not filtering
anything if users pass 0 (InvalidOid) as the target_relid to the new
pg_get_publication_tables(). This is the same as the behavior of the
existing pg_get_publication_tables(), so I'm concerned that it could
be confusing that the function behaves the same even though passing
different arguments . We can check whether the given target_relid is
valid in pg_get_publication_b() but we would end up checking it
multiple times unnecessarily.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 31 Mar 2026 12:29:37 -0700

> It needs to be rebased due to 5984ea86.

Agreed with the all above points. I'll fix them in the next version patch.

> +         LATERAL pg_get_publication_tables(ARRAY[P.pubname], C.oid) GPT;

I'm not sure the new API of pg_get_publication_tables() is better here
since this view is going to get the publication information of all
published tables, in which case the existing one might be faster.
Also, if a few tables among a huge number of tables (or whatever
relations) are published, checking all relations with the new API of
pg_get_publication_tables() would be quite slow.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 31 Mar 2026 12:40:14 -0700

> reference for an alternative style that reduces code size.

Thanks. It looks like a good refactoring! I'd prefer to free the
ancestors list to avoid memory leak.

I've attached the patch that incorporated all comments I got so far.
Feedback is very welcome.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Tue, 31 Mar 2026 17:03:23 -0700

On Fri, Mar 27, 2026 at 6:07 AM Marcos Pegoraro <[email-masked]> wrote:
>
> Em sex., 27 de mar. de 2026 às 03:20, Masahiko Sawada <[email-masked]> escreveu:
>>
> Today I had to create a few more schemas and see that problem again, how the publisher is affected, almost crashing due to the overload.
> That was because max_sync_workers_per_subscription was set to 10, which caused 10 simultaneous connections to call this function immediately after the refresh publication command.
> Wouldn't it be good to document on this GUC that if your publisher server is running version <= 18 then is it advisable to set this GUC to a really low value ?
> Because ok, version 19 is fine, will be covered, but all publisher servers that are not updated will continue to have this trouble.
> The publisher will be severely penalized when the subscription refreshes its publication.
>

I agree that the publisher overload is a serious issue that users
should be aware of. But I'm not sure it's a good idea to broadly
suggest lowing the GUC value as it ultimatly depends on multiple
factors. A value of 10 or more is perfectly fine depending on the
hardware and the number of tables etc. A definition of a large number
of tables also varies on systems. I guess the release note would be a
better place to mention this.

Regards,

---

From: Amit Kapila <[email-masked]>
Date: Wed, 1 Apr 2026 10:34:35 +0530

On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email-masked]> wrote:
> existing pg_get_publication_tables(),
>

Isn't that what we want when a user passes InvalidOid? What is the
expected behavior in that case?

-- 
With Regards,
Amit Kapila.

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 1 Apr 2026 10:35:43 -0700

> expected behavior in that case?

While it could be contrivarsial what we expect when "users wants to
filter the result by InvalidOid", I think the new
pg_get_publication_tables() should not return anything in this case
rather than return all table information. I think this behavior is
consistent with the case where users pass non-table OID to the
function. I don't want to make passing InvalidOid a special case in
the new function.

Regards,

---

From: Masahiko Sawada <[email-masked]>
Date: Wed, 1 Apr 2026 15:23:06 -0700

> Feedback is very welcome.

I decided to simplify the code flow in the
is_table_publishable_in_publication() by taking more proposed changes
from Hou-san while accepting some memory leak. This function is
limited to be used only in per-call-memory context so we don't need to
worry about the actual memory leak. While we would need to change this
function in the futuer when we want to use it other places too, I
think it would be better to keep the function simple until then. I
hope the added new comment also help understand the code flow of this
function.

I think the patch is good shape, so planning to push it barring any objections.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From 66b52499f830572e28d57239eb4b397b4a6643aa Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email-masked]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v8] Add target_relid parameter to pg_get_publication_tables().

Bump catalog version.

Reviewed-by: Haoyan Wang <[email-masked]>
 src/backend/catalog/pg_publication.c        | 241 +++++++++++++++++---
 5 files changed, 604 insertions(+), 50 deletions(-)

index 82a22061d5b..a4c305f0695 100644
 
+/*
+ * Similar to is_publishable_class() but checks whether the given OID
@@ -1264,12 +1295,116 @@ GetPublicationByName(const char *pubname, bool missing_ok)
 }
 
 /*
- * Get information of the tables in the given publication array.
+ * A helper function for pg_get_publication_tables() to check whether the
+ * table of the given relid is published for the specified publication.
+ *
+ * This function evaluates the effective published OID based on the
+ * publish_via_partition_root setting, rather than just checking catalog entries
+ * (e.g., pg_publication_rel). For instance, when publish_via_partition_root is
+ * false, it returns false for a parent partitioned table and returns true
+ * for its leaf partitions, even if the parent is the one explicitly added
+ * to the publication.
+ *
+ * For performance reasons, this function avoids the overhead of constructing
+ * the complete list of published tables during the evaluation. It can execute
+ * quickly even when the publication contains a large number of relations.
  *
- * Returns pubid, relid, column list, row filter for each table.
+ * Note: this leaks memory for the ancestors list into the current memory
+ * context.
  */
-Datum
-pg_get_publication_tables(PG_FUNCTION_ARGS)
+static bool
+is_table_publishable_in_publication(Oid relid, Publication *pub)
+{
+	bool		relispartition;
+	List	   *ancestors = NIL;
+
+	/*
+	 * For non-pubviaroot publications, a partitioned table is never the
+	 * effective published OID; only its leaf partitions can be.
+	 */
+	if (!pub->pubviaroot && get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE)
+		return false;
+
+	relispartition = get_rel_relispartition(relid);
+
+	if (relispartition)
+		ancestors = get_partition_ancestors(relid);
+
+	if (pub->alltables)
+	{
+		/*
+		 * ALL TABLES with pubviaroot includes only regular tables or top-most
+		 * partitioned tables -- never child partitions.
+		 */
+		if (pub->pubviaroot && relispartition)
+			return false;
+
+		/*
+		 * For ALL TABLES publications, the table is published unless it
+		 * appears in the EXCEPT clause. Only the top-most can appear in the
+		 * EXCEPT clause, so exclusion must be evaluated at the top-most
+		 * ancestor if it has. These publications store only EXCEPT'ed tables
+		 * in pg_publication_rel, so checking existence is sufficient.
+		 *
+		 * Note that this existence check below would incorrectly return true
+		 * (published) for partitions when pubviaroot is enabled; however,
+		 * that case is already caught and returned false by the above check.
+		 */
+		return !SearchSysCacheExists2(PUBLICATIONRELMAP,
+									  ObjectIdGetDatum(ancestors
+													   ? llast_oid(ancestors) : relid),
+									  ObjectIdGetDatum(pub->oid));
+	}
+
+	/*
+	 * Non-ALL-TABLE publication cases.
+	 *
+	 * A table is published if it (or a containing schema) was explicitly
+	 * added, or if it is a partition whose ancestor was added.
+	 */
+
+	/*
+	 * If an ancestor is published, the partition's status depends on
+	 * publish_via_partition_root value.
+	 *
+	 * If it's true, the ancestor's relation OID is the effective published
+	 * OID, so the partition itself should be excluded (return false).
+	 *
+	 * If it's false, the partition is covered by its ancestor's presence in
+	 * the publication, it should be included (return true).
+	 */
+	if (relispartition &&
+		OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL)))
+		return !pub->pubviaroot;
+
+	/*
+	 * Check whether the table is explicitly published via pg_publication_rel
+	 * or pg_publication_namespace.
+	 */
+	return (SearchSysCacheExists2(PUBLICATIONRELMAP,
+								  ObjectIdGetDatum(relid),
+								  ObjectIdGetDatum(pub->oid)) ||
+			SearchSysCacheExists2(PUBLICATIONNAMESPACEMAP,
+								  ObjectIdGetDatum(get_rel_namespace(relid)),
+								  ObjectIdGetDatum(pub->oid)));
+}
+
+/*
+ * Helper function to get information of the tables in the given
+ * publication(s).
+ *
+ * If filter_by_relid is true, only the row for target_relid is returned;
+ * if target_relid does not exist or is not part of the publications, zero
+ * rows are returned.  If filter_by_relid is false, rows for all tables
+ * within the specified publications are returned and target_relid is
+ * ignored.
+ *
+ * Returns pubid, relid, column list, and row filter for each table.
+ */
+static Datum
+pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
+						  Oid target_relid, bool filter_by_relid,
+						  bool pub_missing_ok)
 {
 #define NUM_PUBLICATION_TABLES_ELEM	4
 	FuncCallContext *funcctx;
@@ -1280,7 +1415,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	{
 		TupleDesc	tupdesc;
 		MemoryContext oldcontext;
-		ArrayType  *arr;
 		Datum	   *elems;
 		int			nelems,
 					i;
@@ -1289,6 +1423,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 		/* create a function context for cross-call persistence */
 		funcctx = SRF_FIRSTCALL_INIT();
 
+		/*
 
@@ -1296,8 +1438,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 
 		/* Get Oids of tables from each publication. */
 		for (i = 0; i < nelems; i++)
@@ -1306,32 +1447,48 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 			List	   *pub_elem_tables = NIL;
 			ListCell   *lc;

-			/*
 
 			/*
@@ -1491,6 +1648,30 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
 	SRF_RETURN_DONE(funcctx);
 }
 
+Datum

 
 
index d2aa9d45e4a..6f55a394ce1 100644
@@ -2292,6 +2292,231 @@ DROP TABLE testpub_merge_pk;
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);
index 6bafad27571..94908e4f965 100644
@@ -1438,6 +1438,113 @@ RESET SESSION AUTHORIZATION;
 
+CREATE PUBLICATION pub_all_except FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = false);
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT (TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH (publish_via_partition_root = true);

---

From: Amit Kapila <[email-masked]>
Date: Thu, 2 Apr 2026 10:58:24 +0530

> function. I don't want to make passing InvalidOid a special case in

Fair enough. I am fine with this definition.

-- 
With Regards,
Amit Kapila.

---

From: Amit Kapila <[email-masked]>
Date: Thu, 2 Apr 2026 11:15:21 +0530

> hope the added new comment also help understand the code flow of this

Yes, the function looks better and helps to understand the flow.
Thanks for improving it.

-- 
With Regards,
Amit Kapila.

---

From: Peter Smith <[email-masked]>
Date: Thu, 2 Apr 2026 17:12:03 +1100

Hi Sawada-San

Some review comments for v8-0001.

======
Commit message

1.

~

Typo? "The existing a VARIADIC"

======

is_publishable_table:

2.
+ /*

It seemed strange to say that "sequences are publishable according to
is_publishable_class() so explicitly exclude", but then you proceed to
call is_publishable_class() anyway.

Maybe using a variable, and a different comment could be a better way
of expressing this?

SUGGESTION
bool ret;
...
/* Sequences are not tables, so this function returns false. */
if (relform->relkind == RELKIND_SEQUENCE)
    ret = false;
else
    ret = is_publishable_class(tableoid, relform);

ReleaseSysCache(tuple);
return ret;

~~~

3.

/table of the given relid/table with the given relid/

/is published for the/is published in the/

~~~

4.

Should that say "only the row(s) for target_relid", e.g. possibly
plural, because if same table is in multiple publications then there
are be multiple result rows, right?

======

5.
Missed my previous [1] review comment #4?

First arg of pg_get_publication_tables_a should be plural 'pubnames',
same as first arg of pg_get_publication_tables_b.

======

6.
+CREATE PUBLICATION pub_all_except_no_viaroot FOR ALL TABLES EXCEPT
(TABLE tbl_parent, gpt_test_sch.tbl_sch) WITH
(publish_via_partition_root = true);

Why is this publication called '...no_viaroot' when
publish_via_partition_root = true?

~~~

7.

Typo? /EXCLUDE clause/EXCEPT clause/

======
[1] https://www.postgresql.org/message-id/CAHut%2BPuSkabUB8H_hcwQz%3DBX5TWEj-8Ba%2BCP_PX78zN1fkhtKA%40mail.gmail.com

Kind Regards,
Peter Smith.
Fujitsu Australia

---

From: Masahiko Sawada <[email-masked]>
Date: Thu, 2 Apr 2026 15:13:18 -0700

I've pushed the patch after incorporating these points.

Regards,