public inbox for [email protected]
help / color / mirror / Atom feedRe: Initial COPY of Logical Replication is too slow
48+ messages / 8 participants
[nested] [flat]
* Re: Initial COPY of Logical Replication is too slow
@ 2026-01-19 17:44 Marcos Pegoraro <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Marcos Pegoraro @ 2026-01-19 17:44 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email protected]>
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-01-26 20:30 Masahiko Sawada <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-01-26 20:30 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Mon, Jan 19, 2026 at 9:44 AM Marcos Pegoraro <[email protected]> wrote:
>
> Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email protected]> 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 ?
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
Attachments:
[application/octet-stream] sort_and_dedup.patch (637B, 2-sort_and_dedup.patch)
download | inline diff:
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 9a4791c573e..2ea3d8dd9a8 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1183,7 +1183,10 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
pub_elem->pubviaroot ?
PUBLICATION_PART_ROOT :
PUBLICATION_PART_LEAF);
- pub_elem_tables = list_concat_unique_oid(relids, schemarelids);
+
+ pub_elem_tables = list_concat(relids, schemarelids);
+ list_sort(pub_elem_tables, list_oid_cmp);
+ list_deduplicate_oid(pub_elem_tables);
}
/*
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-02-25 19:03 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-02-25 19:03 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Mon, Jan 26, 2026 at 12:30 PM Masahiko Sawada <[email protected]> wrote:
>
> On Mon, Jan 19, 2026 at 9:44 AM Marcos Pegoraro <[email protected]> wrote:
> >
> > Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email protected]> 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 ?
>
> 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.
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
Attachments:
[text/x-patch] 0001-Add-pg_get_publication_table_info-to-optimize-logica.patch (9.7K, 2-0001-Add-pg_get_publication_table_info-to-optimize-logica.patch)
download | inline diff:
From 54af2b794d741865fd06e97738b7fdb34e29b17e Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-02-27 23:47 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-02-27 23:47 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Wed, Feb 25, 2026 at 11:03 AM Masahiko Sawada <[email protected]> wrote:
>
> On Mon, Jan 26, 2026 at 12:30 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Mon, Jan 19, 2026 at 9:44 AM Marcos Pegoraro <[email protected]> wrote:
> > >
> > > Em sex., 19 de dez. de 2025 às 22:59, Masahiko Sawada <[email protected]> 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 ?
> >
> > 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.
>
> 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.
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
Attachments:
[text/x-patch] 0001-Avoid-full-table-scans-when-getting-publication-tabl.patch (12.3K, 2-0001-Avoid-full-table-scans-when-getting-publication-tabl.patch)
download | inline diff:
From 152b798903cd181b4e9b5ca39409d5616ade1bbd Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-03 10:22 Zhijie Hou (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-03-03 10:22 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; Marcos Pegoraro <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>
On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> To: Marcos Pegoraro <[email protected]>
> Cc: PostgreSQL Hackers <[email protected]>
> Subject: Re: Initial COPY of Logical Replication is too slow
>
> 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.
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-09 22:09 Masahiko Sawada <[email protected]>
parent: Zhijie Hou (Fujitsu) <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-09 22:09 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>
On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > To: Marcos Pegoraro <[email protected]>
> > Cc: PostgreSQL Hackers <[email protected]>
> > Subject: Re: Initial COPY of Logical Replication is too slow
> >
> > 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.
>
> 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().
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.
>
> 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)
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Attachments:
[application/x-patch] v2-0001-Avoid-full-table-scans-when-getting-publication-t.patch (26.5K, 2-v2-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
download | inline diff:
From 7ffa55e77413743b63092a824c1a70f74dd122f0 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v2] Avoid full table scans when getting publication table
information by tablesync workers.
Reported-by: Marcos Pegoraro <[email protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
src/backend/catalog/pg_publication.c | 382 +++++++++++++++-----
src/backend/replication/logical/tablesync.c | 68 +++-
src/include/catalog/pg_proc.dat | 9 +
src/test/regress/expected/publication.out | 129 +++++++
src/test/regress/sql/publication.sql | 67 ++++
5 files changed, 543 insertions(+), 112 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index aadc7c202c6..5213f1d0a23 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1207,6 +1207,240 @@ GetPublicationByName(const char *pubname, bool missing_ok)
return OidIsValid(oid) ? GetPublication(oid) : NULL;
}
+/*
+ * 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)));
+}
+
+/*
+ * pg_get_publication_tables() and pg_get_publication_table_info() use
+ * the same record type.
+ */
+#define NUM_PUBLICATION_TABLES_ELEM 4
+
+/*
+ * Construct a tuple descriptor for both pg_get_publication_tales() and
+ * pg_get_publication_table_info() functions.
+ */
+static TupleDesc
+create_published_rel_tuple_desc(void)
+{
+ TupleDesc tupdesc;
+
+ 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);
+
+ return BlessTupleDesc(tupdesc);
+}
+
+/*
+ * Common routine for pg_get_publication_tables() and
+ * pg_get_publication_table_info() to construct the result tuple.
+ * tuple_desc should be the tuple description returned by
+ * create_published_rel_tuple_desc().
+ */
+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);
+
+ /*
+ * 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);
+
+ attnums = palloc_array(int16, desc->natts);
+
+ for (int 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.
*
@@ -1215,14 +1449,12 @@ 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;
/* stuff done only on the first call of the function */
if (SRF_IS_FIRSTCALL())
{
- TupleDesc tupdesc;
MemoryContext oldcontext;
ArrayType *arr;
Datum *elems;
@@ -1311,18 +1543,7 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
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",
- 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);
-
- funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+ funcctx->tuple_desc = create_published_rel_tuple_desc();
funcctx->user_fctx = table_infos;
MemoryContextSwitchTo(oldcontext);
@@ -1334,99 +1555,74 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
if (funcctx->call_cntr < list_length(table_infos))
{
- HeapTuple pubtuple = NULL;
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);
- Datum values[NUM_PUBLICATION_TABLES_ELEM] = {0};
- bool nulls[NUM_PUBLICATION_TABLES_ELEM] = {0};
-
- /*
- * Form tuple with appropriate data.
- */
- pub = GetPublication(table_info->pubid);
+ rettuple = construct_published_rel_tuple(table_info, funcctx->tuple_desc);
- values[0] = ObjectIdGetDatum(pub->oid);
- values[1] = ObjectIdGetDatum(relid);
+ SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(rettuple));
+ }
- /*
- * 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;
- }
+ SRF_RETURN_DONE(funcctx);
+}
- /* 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;
+/*
+ * Similar to pg_get_publication_tables(), but retrieves publication
+ * information only for the specified table.
+ */
+Datum
+pg_get_publication_table_info(PG_FUNCTION_ARGS)
+{
+ FuncCallContext *funcctx;
+ published_rel *table_info = NULL;
- attnums = palloc_array(int16, desc->natts);
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcontext;
+ Oid relid;
+ Name pubname;
+ Relation rel;
+ Publication *pub;
+ published_rel *pubrel = NULL;
- for (i = 0; i < desc->natts; i++)
- {
- Form_pg_attribute att = TupleDescAttr(desc, i);
+ /* create a function context for cross-call persistence */
+ funcctx = SRF_FIRSTCALL_INIT();
- if (att->attisdropped)
- continue;
+ /* switch to memory context appropriate for multiple function calls */
+ oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
- 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;
- }
+ relid = PG_GETARG_OID(0);
+ pubname = PG_GETARG_NAME(1);
- if (nattnums > 0)
- {
- values[2] = PointerGetDatum(buildint2vector(attnums, nattnums));
- nulls[2] = false;
- }
+ rel = table_open(relid, AccessShareLock);
+ pub = GetPublicationByName(NameStr(*pubname), false);
- table_close(rel, AccessShareLock);
+ if (is_table_publishable_in_publication(relid, pub))
+ {
+ pubrel = palloc_object(published_rel);
+ pubrel->relid = relid;
+ pubrel->pubid = pub->oid;
}
- rettuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ table_close(rel, AccessShareLock);
+
+ /* Construct a tuple descriptor for the result rows. */
+ funcctx->tuple_desc = create_published_rel_tuple_desc();
+ 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;
+
+ /* The function returns zero or one tuple */
+ 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));
}
diff --git a/src/backend/replication/logical/tablesync.c b/src/backend/replication/logical/tablesync.c
index f49a4852ecb..ce7afd68533 100644
--- a/src/backend/replication/logical/tablesync.c
+++ b/src/backend/replication/logical/tablesync.c
@@ -798,17 +798,34 @@ 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)
+ {
+ /* pg_get_publication_table_info() is available since vesion 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_table_info(%u, p.pubname) 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 +999,27 @@ 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)
+ {
+ /* pg_get_publication_table_info() is available since version 19 */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+ " FROM pg_publication p,"
+ " LATERAL pg_get_publication_table_info(%u, p.pubname) 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 361e2cfffeb..b357a67ba7d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12451,6 +12451,15 @@
proargmodes => '{v,o,o,o,o}',
proargnames => '{pubname,pubid,relid,attrs,qual}',
prosrc => 'pg_get_publication_tables' },
+{ oid => '8060',
+ 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',
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 681d2564ed5..e9914c147fa 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2182,6 +2182,135 @@ 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_table_info() 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 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);
+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_table_info(relname::regclass::oid, pubname) 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..75f1bc2f2fc 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1378,6 +1378,73 @@ RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
+-- Test pg_get_publication_table_info() 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 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);
+
+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_table_info(relname::regclass::oid, pubname) 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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-18 13:56 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Amit Kapila @ 2026-03-18 13:56 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>
On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > > To: Marcos Pegoraro <[email protected]>
> > > Cc: PostgreSQL Hackers <[email protected]>
> > > Subject: Re: Initial COPY of Logical Replication is too slow
> > >
> > > 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.
> >
> > 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().
>
> 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.
>
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.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-18 16:44 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-18 16:44 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Marcos Pegoraro <[email protected]>; PostgreSQL Hackers <[email protected]>
On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
>
> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > > > To: Marcos Pegoraro <[email protected]>
> > > > Cc: PostgreSQL Hackers <[email protected]>
> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> > > >
> > > > 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.
> > >
> > > 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().
> >
> > 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.
> >
>
> 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.
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-18 22:31 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-18 22:31 UTC (permalink / raw)
To: Jan Wieck <[email protected]>; +Cc: [email protected]
On Wed, Mar 18, 2026 at 1:11 PM Jan Wieck <[email protected]> wrote:
>
> On 3/18/26 12:44, Masahiko Sawada wrote:
> > On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
> >>
> >> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> >> >
> >> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> >> > <[email protected]> wrote:
> >> > >
> >> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> >> > > > To: Marcos Pegoraro <[email protected]>
> >> > > > Cc: PostgreSQL Hackers <[email protected]>
> >> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> >> > > >
> >> > > > 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.
> >> > >
> >> > > 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().
> >> >
> >> > 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.
> >> >
> >>
> >> 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.
> >
> > 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.
>
> 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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-18 23:29 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 4 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-18 23:29 UTC (permalink / raw)
To: Jan Wieck <[email protected]>; +Cc: [email protected]
On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
>
> On Wed, Mar 18, 2026 at 1:11 PM Jan Wieck <[email protected]> wrote:
> >
> > On 3/18/26 12:44, Masahiko Sawada wrote:
> > > On Wed, Mar 18, 2026 at 6:56 AM Amit Kapila <[email protected]> wrote:
> > >>
> > >> On Tue, Mar 10, 2026 at 3:40 AM Masahiko Sawada <[email protected]> wrote:
> > >> >
> > >> > On Tue, Mar 3, 2026 at 2:22 AM Zhijie Hou (Fujitsu)
> > >> > <[email protected]> wrote:
> > >> > >
> > >> > > On Saturday, February 28, 2026 7:48 AM Masahiko Sawada <[email protected]> wrote:
> > >> > > > To: Marcos Pegoraro <[email protected]>
> > >> > > > Cc: PostgreSQL Hackers <[email protected]>
> > >> > > > Subject: Re: Initial COPY of Logical Replication is too slow
> > >> > > >
> > >> > > > 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.
> > >> > >
> > >> > > 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().
> > >> >
> > >> > 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.
> > >> >
> > >>
> > >> 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.
> > >
> > > 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.
> >
> > 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.
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
Attachments:
[text/x-patch] v3-0001-Avoid-full-table-scans-when-getting-publication-t.patch (25.5K, 2-v3-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
download | inline diff:
From c3647910dadb645895cea1ef15c11aaa6cd25d18 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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 protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 00:53 Bharath Rupireddy <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Bharath Rupireddy @ 2026-03-24 00:53 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
Hi,
On Wed, Mar 18, 2026 at 4:29 PM Masahiko Sawada <[email protected]> wrote:
>
> 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.
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
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 06:54 Ajin Cherian <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Ajin Cherian @ 2026-03-24 06:54 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Thu, Mar 19, 2026 at 10:30 AM Masahiko Sawada <[email protected]> wrote:
>
> 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.
>
The patch looks like a good performance improvement. Some minor comments:
1. src/test/regress/expected/publication.out
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR: type "relname" does not exist
Cleanup actually fails. Second parameter should be text, not relname.
2. src/include/catalog/pg_proc.dat
+ proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
+ proargmodes => '{i,i,o,o,o,o}',
+ proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
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
+
+ if (server_version >= 190000)
+ {
+ /*
+ * We can pass relid to pg_get_publication_table_info() since
+ * version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT"
In multiple places in the code pg_get_publication_table_info() is
used, instead of pg_get_publication_tables()
thanks,
Ajin Cherian
Fujitsu Australia
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 06:59 Peter Smith <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Peter Smith @ 2026-03-24 06:59 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
Hi Sawada-San.
Here are some review comments for the v3-0001 test code.
======
src/test/regress/expected/publication.out
1.
+-- Clean up
+DROP FUNCTION test_gpt(text[], relname);
+ERROR: type "relname" does not exist
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.
+SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
+SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
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;
Attachments:
[text/plain] PS_v3_testcode_topup.txt (2.1K, 2-PS_v3_testcode_topup.txt)
download | inline diff:
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;
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 10:47 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Amit Kapila @ 2026-03-24 10:47 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
>
> On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> >
>
> 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.
>
*
+ /*
+ * 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);
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.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 18:41 Masahiko Sawada <[email protected]>
parent: Bharath Rupireddy <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-24 18:41 UTC (permalink / raw)
To: Bharath Rupireddy <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Mon, Mar 23, 2026 at 5:54 PM Bharath Rupireddy
<[email protected]> wrote:
>
> Hi,
>
> On Wed, Mar 18, 2026 at 4:29 PM Masahiko Sawada <[email protected]> wrote:
> >
> > 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.
>
> 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:
Thank you for reviewing the patch!
>
> 1/ Typo: s/pg_get_publication_table_info/pg_get_publication_tables
Fixed.
>
> 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.
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.
>
> 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?
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.gma...
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 18:42 Masahiko Sawada <[email protected]>
parent: Ajin Cherian <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-24 18:42 UTC (permalink / raw)
To: Ajin Cherian <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Mon, Mar 23, 2026 at 11:54 PM Ajin Cherian <[email protected]> wrote:
>
> On Thu, Mar 19, 2026 at 10:30 AM Masahiko Sawada <[email protected]> wrote:
> >
> > 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.
> >
>
> The patch looks like a good performance improvement. Some minor comments:
>
> 1. src/test/regress/expected/publication.out
>
> +-- Clean up
> +DROP FUNCTION test_gpt(text[], relname);
> +ERROR: type "relname" does not exist
>
> Cleanup actually fails. Second parameter should be text, not relname.
>
> 2. src/include/catalog/pg_proc.dat
>
> + proallargtypes => '{text,oid,oid,oid,int2vector,pg_node_tree}',
> + proargmodes => '{i,i,o,o,o,o}',
> + proargnames => '{pubname,relid,pubid,relid,attrs,qual}',
>
> 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
>
> +
> + if (server_version >= 190000)
> + {
> + /*
> + * We can pass relid to pg_get_publication_table_info() since
> + * version 19.
> + */
> + appendStringInfo(&cmd,
> + "SELECT DISTINCT"
>
> In multiple places in the code pg_get_publication_table_info() is
> 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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 18:45 Masahiko Sawada <[email protected]>
parent: Peter Smith <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-24 18:45 UTC (permalink / raw)
To: Peter Smith <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Mon, Mar 23, 2026 at 11:59 PM Peter Smith <[email protected]> wrote:
>
> Hi Sawada-San.
>
> Here are some review comments for the v3-0001 test code.
Thank you for reviewing the patch!
>
> ======
> src/test/regress/expected/publication.out
>
> 1.
> +-- Clean up
> +DROP FUNCTION test_gpt(text[], relname);
> +ERROR: type "relname" does not exist
>
> This seems a mistake. If the DROP FUNCTION was differently written
> there there would be no error. PSA.
Fixed.
>
> ======
> src/test/regress/sql/publication.sql
>
> 2.
> +SELECT * FROM test_gpt('pub_normal', 'tbl_normal');
> +SELECT * FROM test_gpt('pub_schema', 'tbl_normal'); -- no result
>
> 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'
Added.
>
> ~~~
>
> 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'
Hmm, there are already some queries using 'parent' in the same .sql
file. I'll leave these names.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-24 18:57 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-24 18:57 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Tue, Mar 24, 2026 at 3:47 AM Amit Kapila <[email protected]> wrote:
>
> On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
> >
> > On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> > >
> >
> > 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.
> >
>
> *
> + /*
> + * 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);
>
> 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?
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-25 05:06 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 4 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-25 05:06 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Tue, Mar 24, 2026 at 11:57 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 24, 2026 at 3:47 AM Amit Kapila <[email protected]> wrote:
> >
> > On Thu, Mar 19, 2026 at 4:59 AM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Wed, Mar 18, 2026 at 3:31 PM Masahiko Sawada <[email protected]> wrote:
> > > >
> > >
> > > 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.
> > >
> >
> > *
> > + /*
> > + * 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);
> >
> > 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?
>
> 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.
>
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
Attachments:
[text/x-patch] v4-0002-POC-pass-the-list-of-publications-to-pg_get_publi.patch (21.1K, 2-v4-0002-POC-pass-the-list-of-publications-to-pg_get_publi.patch)
download | inline diff:
From 2bcf744710589e88bfcdb370ba5c2b098cbdae9c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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;
/* create a function context for cross-call persistence */
funcctx = SRF_FIRSTCALL_INIT();
@@ -1404,49 +1407,37 @@ pg_get_publication_tables(FunctionCallInfo fcinfo, ArrayType *pubnames,
/* switch to memory context appropriate for multiple function calls */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
- 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
[text/x-patch] v4-0001-Avoid-full-table-scans-when-getting-publication-t.patch (26.0K, 3-v4-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
download | inline diff:
From adb8822ddd5fe1f5f616d31512930d62358a272c Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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 protected]>
Reviewed-by: Zhijie Hou (Fujitsu) <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
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)
}
/*
- * 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;
@@ -1280,11 +1397,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();
@@ -1292,81 +1404,114 @@ 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;
+ /* Try to retrieve the specified table information */
+ if (SearchSysCacheExists1(RELOID, target_relid))
+ {
+ Publication *pub;
- pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+ 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
- {
- 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);
+ if (is_table_publishable_in_publication(target_relid, pub))
+ {
+ 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);
+ }
+
+ /*
+ * 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);
+ 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",
@@ -1491,6 +1636,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..ec8840ebf42 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() 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() 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 0118e970dda..6c23f36495f 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 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,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..2c859de6c5e 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2271,6 +2271,147 @@ 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_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');
+ 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
+---------+---------+-------+------
+(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, text);
+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 22e0a30b5c7..c1c83f7d701 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1429,6 +1429,78 @@ 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_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('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, text);
+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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-25 08:48 Peter Smith <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Peter Smith @ 2026-03-25 08:48 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected]
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'
======
src/test/regress/sql/publication.sql
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-26 05:44 Zhijie Hou (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 0 replies; 48+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-03-26 05:44 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Wednesday, March 25, 2026 2:07 PM Masahiko Sawada <[email protected]> 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.
+ * specific table. Otherwise, if returns information for all tables within the
+ * specified publications.
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-26 08:35 Hayato Kuroda (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 2 replies; 48+ messages in thread
From: Hayato Kuroda (Fujitsu) @ 2026-03-26 08:35 UTC (permalink / raw)
To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>
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
#!/bin/bash
####################
### Declarations ###
####################
## Publisher-related params
PORT_PUB=6633
DATA_PUB=data_pub
LOG_PUB=pub.log
## Number of runs
NUMRUN=1
## Measurement params
NUMTABLES=25000
# Setup an instance with above parameters.
function setup () {
################
### clean up ###
################
pg_ctl stop -D $DATA_PUB -w
rm -rf $DATA_PUB $LOG_PUB
#######################
### setup publisher ###
#######################
initdb -D data_pub -U postgres
cat << EOF >> data_pub/postgresql.conf
port=$PORT_PUB
autovacuum = false
shared_buffers = '100GB'
max_wal_size = 20GB
min_wal_size = 10GB
wal_level = logical
EOF
pg_ctl -D $DATA_PUB start -w -l $LOG_PUB
(
echo "CREATE SCHEMA sch1;"
echo "SELECT 'CREATE TABLE tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
echo "SELECT 'CREATE TABLE sch1.tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
echo "CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1;"
echo "SELECT 'ALTER PUBLICATION pub ADD TABLE tab_' || generate_series(1, $NUMTABLES) ; \gexec"
) | psql -U postgres -p $PORT_PUB
}
setup
Attachments:
[image/png] pg_get_publication_tables.png (30.3K, 2-pg_get_publication_tables.png)
download | view image
[text/plain] setup.txt (1.2K, 3-setup.txt)
download | inline:
#!/bin/bash
####################
### Declarations ###
####################
## Publisher-related params
PORT_PUB=6633
DATA_PUB=data_pub
LOG_PUB=pub.log
## Number of runs
NUMRUN=1
## Measurement params
NUMTABLES=25000
# Setup an instance with above parameters.
function setup () {
################
### clean up ###
################
pg_ctl stop -D $DATA_PUB -w
rm -rf $DATA_PUB $LOG_PUB
#######################
### setup publisher ###
#######################
initdb -D data_pub -U postgres
cat << EOF >> data_pub/postgresql.conf
port=$PORT_PUB
autovacuum = false
shared_buffers = '100GB'
max_wal_size = 20GB
min_wal_size = 10GB
wal_level = logical
EOF
pg_ctl -D $DATA_PUB start -w -l $LOG_PUB
(
echo "CREATE SCHEMA sch1;"
echo "SELECT 'CREATE TABLE tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
echo "SELECT 'CREATE TABLE sch1.tab_' || generate_series(1, $NUMTABLES) || '(id int primary key)' ; \gexec"
echo "CREATE PUBLICATION pub FOR TABLES IN SCHEMA sch1;"
echo "SELECT 'ALTER PUBLICATION pub ADD TABLE tab_' || generate_series(1, $NUMTABLES) ; \gexec"
) | psql -U postgres -p $PORT_PUB
}
setup
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-26 10:43 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
3 siblings, 1 reply; 48+ messages in thread
From: Amit Kapila @ 2026-03-26 10:43 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Wed, Mar 25, 2026 at 10:37 AM Masahiko Sawada <[email protected]> wrote:
>
> 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.
>
What behaviour change are you referring to? In general, the direction
appears right to me.
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-26 12:46 Hayato Kuroda (Fujitsu) <[email protected]>
parent: Hayato Kuroda (Fujitsu) <[email protected]>
1 sibling, 0 replies; 48+ messages in thread
From: Hayato Kuroda (Fujitsu) @ 2026-03-26 12:46 UTC (permalink / raw)
To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>
> 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.
...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
(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' );
```
[2]:
```
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' );
```
Best regards,
Hayato Kuroda
FUJITSU LIMITED
Attachments:
[image/png] pg_get_publication_tables.png (30.4K, 2-pg_get_publication_tables.png)
download | view image
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-26 23:51 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-26 23:51 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected]
On Thu, Mar 26, 2026 at 3:44 AM Amit Kapila <[email protected]> wrote:
>
> On Wed, Mar 25, 2026 at 10:37 AM Masahiko Sawada <[email protected]> wrote:
> >
> > 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.
> >
>
> What behaviour change are you referring to? In general, the direction
> appears right to me.
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-27 03:16 Hayato Kuroda (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Hayato Kuroda (Fujitsu) @ 2026-03-27 03:16 UTC (permalink / raw)
To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>
Dear Sawada-san,
> 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).
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
DROP FUNCTION test_gpt(text, text);
```
It should be test_gpt(text[], text);
Best regards,
Hayato Kuroda
FUJITSU LIMITED
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-27 03:51 Amit Kapila <[email protected]>
parent: Hayato Kuroda (Fujitsu) <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Amit Kapila @ 2026-03-27 03:51 UTC (permalink / raw)
To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Fri, Mar 27, 2026 at 8:46 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> > 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).
>
> 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);
> ```
>
Sounds like a good idea for backward compatibility.
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-27 06:20 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
0 siblings, 3 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-27 06:20 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Thu, Mar 26, 2026 at 8:51 PM Amit Kapila <[email protected]> wrote:
>
> On Fri, Mar 27, 2026 at 8:46 AM Hayato Kuroda (Fujitsu)
> <[email protected]> wrote:
> >
> > Dear Sawada-san,
> >
> > > 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).
> >
> > 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);
> > ```
> >
>
> 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
Attachments:
[text/x-patch] v5-0001-Avoid-full-table-scans-when-getting-publication-t.patch (28.4K, 2-v5-0001-Avoid-full-table-scans-when-getting-publication-t.patch)
download | inline diff:
From 601c764b27df8994c801d3ced81c5821ba1400c6 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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 protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
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';
/* create a function context for cross-call persistence */
funcctx = SRF_FIRSTCALL_INIT();
@@ -1292,12 +1400,16 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
/* switch to memory context appropriate for multiple function calls */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+ 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);
- /*
- * 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);
+ if (pub_elem == NULL)
+ continue;
+
+ if (filter_by_relid)
+ {
+ /* Check if the given table is published for the publication */
+ if ((relkind == RELKIND_RELATION || relkind == RELKIND_PARTITIONED_TABLE) &&
+ is_table_publishable_in_publication(target_relid, pub_elem))
+ {
+ pub_elem_tables = list_make1_oid(target_relid);
+ }
+ }
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);
+ /*
+ * 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);
+ }
}
/*
@@ -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,
/* 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 both publication names and relid to
+ * pg_get_publication_tables() since version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+ " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+ pub_names->data,
+ lrel->remoteid);
+ }
+ 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 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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-27 13:07 Marcos Pegoraro <[email protected]>
parent: Masahiko Sawada <[email protected]>
2 siblings, 1 reply; 48+ messages in thread
From: Marcos Pegoraro @ 2026-03-27 13:07 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
Em sex., 27 de mar. de 2026 às 03:20, Masahiko Sawada <[email protected]>
escreveu:
> 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.
>
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-30 07:16 Zhijie Hou (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
2 siblings, 1 reply; 48+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-03-30 07:16 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; Amit Kapila <[email protected]>; +Cc: Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Friday, March 27, 2026 2:20 PM Masahiko Sawada <[email protected]> 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
> is_table_publishable_in_publication() and added more regression tests.
Thanks for updating the patch.
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-30 07:42 Hayato Kuroda (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
2 siblings, 1 reply; 48+ messages in thread
From: Hayato Kuroda (Fujitsu) @ 2026-03-30 07:42 UTC (permalink / raw)
To: 'Masahiko Sawada' <[email protected]>; Amit Kapila <[email protected]>; +Cc: Jan Wieck <[email protected]>; [email protected] <[email protected]>
Dear Sawada-san,
Thanks for updating the patch. I think the patch has a good shape.
Below contains minor comments.
```
+ if (filter_by_relid)
+ relkind = get_rel_relkind(target_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.
```
+ Assert(pubnames != NULL);
```
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.
```
+ proname => 'pg_get_publication_tables', prorows => '10',
```
Can prorows be 1? Because only a row would be returned here.
Best regards,
Hayato Kuroda
FUJITSU LIMITED
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 00:29 Masahiko Sawada <[email protected]>
parent: Hayato Kuroda (Fujitsu) <[email protected]>
1 sibling, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 00:29 UTC (permalink / raw)
To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Thu, Mar 26, 2026 at 1:35 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> 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
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 01:00 Masahiko Sawada <[email protected]>
parent: Hayato Kuroda (Fujitsu) <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 01:00 UTC (permalink / raw)
To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Mon, Mar 30, 2026 at 12:42 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> Thanks for updating the patch. I think the patch has a good shape.
> Below contains minor comments.
Thank you for the comments!
>
>
> ```
> + if (filter_by_relid)
> + relkind = get_rel_relkind(target_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.
Yeah, I refactored these logic and do the preliminary check before
checking the publications.
>
> ```
> + /*
> + * Non-alltables
> + */
> + if (relispartition)
> ```
>
> 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.
>
> ```
> + Assert(pubnames != NULL);
> ```
>
> 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.
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.
>
> ```
> + proname => 'pg_get_publication_tables', prorows => '10',
> ```
>
> 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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 04:08 Masahiko Sawada <[email protected]>
parent: Zhijie Hou (Fujitsu) <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 04:08 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Mon, Mar 30, 2026 at 12:16 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Friday, March 27, 2026 2:20 PM Masahiko Sawada <[email protected]> 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
> > is_table_publishable_in_publication() and added more regression tests.
>
> Thanks for updating the patch.
>
> 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.
Indeed. Good catch!
>
> 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.
>
+1. I've added is_publishable_class() check.
I've attached the updated patch.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-patch] v6-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (30.2K, 2-v6-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
download | inline diff:
From b10a79d482e6fe0f178b0ff004767dcbc6bc86db Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
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 protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
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)
}
/*
- * 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,7 +1420,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
{
TupleDesc tupdesc;
MemoryContext oldcontext;
- ArrayType *arr;
Datum *elems;
int nelems,
i;
@@ -1296,8 +1435,15 @@ 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);
+ deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
+
+ /*
+ * 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))
+ SRF_RETURN_DONE(funcctx);
/* 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;
- pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+ pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+ pub_missing_ok);
- /*
- * 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);
+ if (pub_elem == NULL)
+ continue;
+
+ if (filter_by_relid)
+ {
+ /* Check if the given table is published for the publication */
+ if (is_table_publishable_in_publication(target_relid, pub_elem))
+ {
+ pub_elem_tables = list_make1_oid(target_relid);
+ }
+ }
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);
+ /*
+ * 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);
+ }
}
/*
@@ -1491,6 +1653,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,
/* 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 both publication names and relid to
+ * pg_get_publication_tables() since version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+ " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+ pub_names->data,
+ lrel->remoteid);
+ }
+ 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 3579cec5744..afdcc915f08 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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 09:36 Amit Kapila <[email protected]>
parent: Peter Smith <[email protected]>
0 siblings, 2 replies; 48+ messages in thread
From: Amit Kapila @ 2026-03-31 09:36 UTC (permalink / raw)
To: Peter Smith <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected]
On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
>
> 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.
>
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.
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-31 11:39 Hayato Kuroda (Fujitsu) <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Hayato Kuroda (Fujitsu) @ 2026-03-31 11:39 UTC (permalink / raw)
To: 'Masahiko Sawada' <[email protected]>; Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
Dear Sawada-san,
Thanks for updating the patch! Few comments.
01.
```
+/*
+ * Similar to is_publishable_calss() but checks whether the given OID
+ * is a publishable "table" or not.
+ */
+static bool
+is_publishable_table(Oid tableoid)
```
s/is_publishable_calss/is_publishable_class/.
02.
```
+ ReleaseSysCache(tuple);
+ return true;
```
Is it correct? I expected to return false here.
03.
```
+ /*
+ * 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))
+ SRF_RETURN_DONE(funcctx);
```
I think we must switch to the old context.
04.
```
+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);
```
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
^ permalink raw reply [nested|flat] 48+ messages in thread
* RE: Initial COPY of Logical Replication is too slow
@ 2026-03-31 12:07 Zhijie Hou (Fujitsu) <[email protected]>
parent: Amit Kapila <[email protected]>
1 sibling, 1 reply; 48+ messages in thread
From: Zhijie Hou (Fujitsu) @ 2026-03-31 12:07 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; +Cc: Masahiko Sawada <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
>
> On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> wrote:
> >
> > 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.
> >
>
> 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
Attachments:
[application/octet-stream] v1-0001-refactor-the-function.patch (3.5K, 2-v1-0001-refactor-the-function.patch)
download | inline diff:
From 706d7cb4b3ac7f30f131c64f859cabe00773b07d Mon Sep 17 00:00:00 2001
From: Zhijie Hou <[email protected]>
Date: Tue, 31 Mar 2026 19:35:44 +0800
Subject: [PATCH v1] refactor the function
---
src/backend/catalog/pg_publication.c | 79 +++++++++-------------------
1 file changed, 26 insertions(+), 53 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 14ae03fc0ff..a036cd8ff33 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1312,75 +1312,48 @@ static bool
is_table_publishable_in_publication(Oid relid, Publication *pub)
{
bool relispartition;
+ List *ancestors = NIL;
+ Oid topmost = InvalidOid;
/*
* 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)
+ 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);
+ /*
+ * ALL TABLES with pubviaroot includes only regular tables or
+ * top-most partitioned tables -- never child partitions.
+ */
+ if (pub->alltables && pub->pubviaroot && relispartition)
+ return false;
- /*
- * 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);
- }
+ if (relispartition)
+ ancestors = get_partition_ancestors(relid);
- /*
- * 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.
- */
+ /*
+ * 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.
+ */
+ if (pub->alltables)
return !SearchSysCacheExists2(PUBLICATIONRELMAP,
- ObjectIdGetDatum(target_relid),
+ ObjectIdGetDatum(ancestors
+ ? llast_oid(ancestors) : relid),
ObjectIdGetDatum(pub->oid));
- }
/*
- * Non-alltables
+ * If pubviaroot is true, the ancestor is published instead of the
+ * partition, so exclude it. Otherwise, the ancestor covers the partition,
+ * so include it.
*/
-
- 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 */
- }
+ if (relispartition &&
+ OidIsValid(GetTopMostAncestorInPublication(pub->oid, ancestors, NULL)))
+ return !pub->pubviaroot;
/*
* Check whether the table is explicitly published via pg_publication_rel
--
2.53.0.windows.2
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 17:28 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
1 sibling, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 17:28 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]
On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
>
> On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> >
> > 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.
> >
>
> 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.
Okay, I'll refactor that function.
>
> 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?
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 19:29 Masahiko Sawada <[email protected]>
parent: Hayato Kuroda (Fujitsu) <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 19:29 UTC (permalink / raw)
To: Hayato Kuroda (Fujitsu) <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Tue, Mar 31, 2026 at 4:39 AM Hayato Kuroda (Fujitsu)
<[email protected]> wrote:
>
> Dear Sawada-san,
>
> Thanks for updating the patch! Few comments.
Thank you for the comments!
>
> 01.
> ```
> +/*
> + * Similar to is_publishable_calss() but checks whether the given OID
> + * is a publishable "table" or not.
> + */
> +static bool
> +is_publishable_table(Oid tableoid)
> ```
>
> s/is_publishable_calss/is_publishable_class/.
>
> 02.
> ```
> + ReleaseSysCache(tuple);
> + return true;
> ```
>
> Is it correct? I expected to return false here.
>
> 03.
> ```
> + /*
> + * 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))
> + SRF_RETURN_DONE(funcctx);
> ```
>
> I think we must switch to the old context.
>
> 04.
> ```
> +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);
> ```
>
> It needs to be rebased due to 5984ea86.
Agreed with the all above points. I'll fix them in the next version patch.
>
> 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;
> ```
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-03-31 19:40 Masahiko Sawada <[email protected]>
parent: Zhijie Hou (Fujitsu) <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-03-31 19:40 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
<[email protected]> wrote:
>
> On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> >
> > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > wrote:
> > >
> > > 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.
> > >
> >
> > 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.
>
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
Attachments:
[application/octet-stream] v7-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (32.4K, 2-v7-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
download | inline diff:
From f64b6d4611fa146433709d30b9c8288d15d2e240 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v7] 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 protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Haoyan Wang <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
src/backend/catalog/pg_publication.c | 256 +++++++++++++++++---
src/backend/replication/logical/tablesync.c | 70 ++++--
src/backend/replication/pgoutput/pgoutput.c | 7 +-
src/include/catalog/pg_proc.dat | 11 +-
src/include/catalog/pg_publication.h | 2 +
src/test/regress/expected/publication.out | 225 +++++++++++++++++
src/test/regress/sql/publication.sql | 107 ++++++++
7 files changed, 624 insertions(+), 54 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index a3192f19d35..b90e9794e7d 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_class() 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 false;
+}
+
/*
* SQL-callable variant of the above
*
@@ -451,6 +482,26 @@ GetTopMostAncestorInPublication(Oid puboid, List *ancestors, int *ancestor_level
return topmost_relid;
}
+/*
+ * A variant of GetTopMostAncestorInPublication() returns the top most
+ * published ancestor of the given relid.
+ */
+Oid
+GetTopMostAncestorInPublicationRelid(Oid pubid, Oid relid,
+ int *ancestor_level)
+{
+ List *ancestors = get_partition_ancestors(relid);
+ Oid ancestor;
+
+ ancestor = GetTopMostAncestorInPublication(pubid, ancestors,
+ ancestor_level);
+
+ if (ancestors)
+ list_free(ancestors);
+
+ return ancestor;
+}
+
/*
* attnumstoint2vector
* Convert a Bitmapset of AttrNumbers into an int2vector.
@@ -1264,12 +1315,111 @@ 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;
+
+ /*
+ * ALL TABLES with pubviaroot includes only regular tables or top-most
+ * partitioned tables -- never child partitions.
+ */
+ if (pub->pubviaroot && relispartition)
+ return false;
+
+ 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 publication.
+ */
+
+ if (relispartition &&
+ OidIsValid(GetTopMostAncestorInPublicationRelid(pub->oid,
+ relid, NULL)))
+ {
+ /*
+ * 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;
+ }
+
+ /*
+ * 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 +1430,6 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
{
TupleDesc tupdesc;
MemoryContext oldcontext;
- ArrayType *arr;
Datum *elems;
int nelems,
i;
@@ -1289,6 +1438,14 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
/* create a function context for cross-call persistence */
funcctx = SRF_FIRSTCALL_INIT();
+ /*
+ * 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))
+ SRF_RETURN_DONE(funcctx);
+
/* switch to memory context appropriate for multiple function calls */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
@@ -1296,8 +1453,7 @@ 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);
+ deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
/* Get Oids of tables from each publication. */
for (i = 0; i < nelems; i++)
@@ -1306,32 +1462,48 @@ 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);
- /*
- * 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);
+ if (pub_elem == NULL)
+ continue;
+
+ if (filter_by_relid)
+ {
+ /* Check if the given table is published for the publication */
+ if (is_table_publishable_in_publication(target_relid, pub_elem))
+ {
+ pub_elem_tables = list_make1_oid(target_relid);
+ }
+ }
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);
+ /*
+ * 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);
+ }
}
/*
@@ -1491,6 +1663,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,
/* 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 both publication names and relid to
+ * pg_get_publication_tables() since version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+ " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+ pub_names->data,
+ lrel->remoteid);
+ }
+ 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/backend/replication/pgoutput/pgoutput.c b/src/backend/replication/pgoutput/pgoutput.c
index 4ecfcbff7ab..f4531efe7ec 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -2263,11 +2263,10 @@ get_rel_sync_entry(PGOutputData *data, Relation relation)
{
Oid ancestor;
int level;
- List *ancestors = get_partition_ancestors(relid);
- ancestor = GetTopMostAncestorInPublication(pub->oid,
- ancestors,
- &level);
+ ancestor = GetTopMostAncestorInPublicationRelid(pub->oid,
+ relid,
+ &level);
if (ancestor != InvalidOid)
{
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3579cec5744..afdcc915f08 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/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 89b4bb14f62..ad309e26e02 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -192,6 +192,8 @@ extern List *GetPubPartitionOptionRelations(List *result,
Oid relid);
extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
int *ancestor_level);
+extern Oid GetTopMostAncestorInPublicationRelid(Oid puboid, Oid relid,
+ int *ancestor_level);
extern bool is_publishable_relation(Relation rel);
extern bool is_schema_publication(Oid pubid);
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 91332e75eeb..3b0eaec4f21 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2292,6 +2292,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 6bafad27571..94908e4f965 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1438,6 +1438,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.47.3
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-01 00:03 Masahiko Sawada <[email protected]>
parent: Marcos Pegoraro <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-04-01 00:03 UTC (permalink / raw)
To: Marcos Pegoraro <[email protected]>; +Cc: Amit Kapila <[email protected]>; Hayato Kuroda (Fujitsu) <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Fri, Mar 27, 2026 at 6:07 AM Marcos Pegoraro <[email protected]> wrote:
>
> Em sex., 27 de mar. de 2026 às 03:20, Masahiko Sawada <[email protected]> escreveu:
>>
>> 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.
>
>
> 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 ?
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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-01 05:04 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Amit Kapila @ 2026-04-01 05:04 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]
On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> >
> > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > >
> > > 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.
> > >
> >
> > 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.
>
> Okay, I'll refactor that function.
>
> >
> > 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?
>
> 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(),
>
Isn't that what we want when a user passes InvalidOid? What is the
expected behavior in that case?
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-01 17:35 Masahiko Sawada <[email protected]>
parent: Amit Kapila <[email protected]>
0 siblings, 1 reply; 48+ messages in thread
From: Masahiko Sawada @ 2026-04-01 17:35 UTC (permalink / raw)
To: Amit Kapila <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]
On Tue, Mar 31, 2026 at 10:04 PM Amit Kapila <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> > >
> > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > > >
> > > > 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.
> > > >
> > >
> > > 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.
> >
> > Okay, I'll refactor that function.
> >
> > >
> > > 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?
> >
> > 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(),
> >
>
> Isn't that what we want when a user passes InvalidOid? What is the
> 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,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-01 22:23 Masahiko Sawada <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 2 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-04-01 22:23 UTC (permalink / raw)
To: Zhijie Hou (Fujitsu) <[email protected]>; +Cc: Amit Kapila <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Tue, Mar 31, 2026 at 12:40 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
> <[email protected]> wrote:
> >
> > On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> > >
> > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > > wrote:
> > > >
> > > > 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.
> > > >
> > >
> > > 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.
> >
>
> 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.
>
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
Attachments:
[text/x-patch] v8-0001-Add-target_relid-parameter-to-pg_get_publication_.patch (31.0K, 2-v8-0001-Add-target_relid-parameter-to-pg_get_publication_.patch)
download | inline diff:
From 66b52499f830572e28d57239eb4b397b4a6643aa Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Fri, 27 Feb 2026 15:42:38 -0800
Subject: [PATCH v8] 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 protected]>
Reviewed-by: Zhijie Hou <[email protected]>
Reviewed-by: Matheus Alcantara <[email protected]>
Reviewed-by: Amit Kapila <[email protected]>
Reviewed-by: Peter Smith <[email protected]>
Reviewed-by: Hayato Kuroda <[email protected]>
Reviewed-by: Chao Li <[email protected]>
Reviewed-by: Haoyan Wang <[email protected]>
Discussion: https://postgr.es/m/CAB-JLwbBFNuASyEnZWP0Tck9uNkthBZqi6WoXNevUT6+mV8XmA@mail.gmail.com
---
src/backend/catalog/pg_publication.c | 241 +++++++++++++++++---
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, 604 insertions(+), 50 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 82a22061d5b..a4c305f0695 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_class() 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 false;
+}
+
/*
* SQL-callable variant of the above
*
@@ -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();
+ /*
+ * 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))
+ SRF_RETURN_DONE(funcctx);
+
/* switch to memory context appropriate for multiple function calls */
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
@@ -1296,8 +1438,7 @@ 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);
+ deconstruct_array_builtin(pubnames, TEXTOID, &elems, NULL, &nelems);
/* 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;
- pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]), false);
+ pub_elem = GetPublicationByName(TextDatumGetCString(elems[i]),
+ pub_missing_ok);
- /*
- * 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);
+ if (pub_elem == NULL)
+ continue;
+
+ if (filter_by_relid)
+ {
+ /* Check if the given table is published for the publication */
+ if (is_table_publishable_in_publication(target_relid, pub_elem))
+ {
+ pub_elem_tables = list_make1_oid(target_relid);
+ }
+ }
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);
+ /*
+ * 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);
+ }
}
/*
@@ -1491,6 +1648,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,
/* 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 both publication names and relid to
+ * pg_get_publication_tables() since version 19.
+ */
+ appendStringInfo(&cmd,
+ "SELECT DISTINCT pg_get_expr(gpt.qual, gpt.relid)"
+ " FROM pg_get_publication_tables(ARRAY[%s], %u) gpt",
+ pub_names->data,
+ lrel->remoteid);
+ }
+ 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 3579cec5744..afdcc915f08 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 d2aa9d45e4a..6f55a394ce1 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2292,6 +2292,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 6bafad27571..94908e4f965 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1438,6 +1438,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
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-02 05:28 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Amit Kapila @ 2026-04-02 05:28 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected]
On Wed, Apr 1, 2026 at 11:06 PM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 10:04 PM Amit Kapila <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 10:58 PM Masahiko Sawada <[email protected]> wrote:
> > >
> > > On Tue, Mar 31, 2026 at 2:36 AM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]> wrote:
> > > > >
> > > > > 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.
> > > > >
> > > >
> > > > 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.
> > >
> > > Okay, I'll refactor that function.
> > >
> > > >
> > > > 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?
> > >
> > > 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(),
> > >
> >
> > Isn't that what we want when a user passes InvalidOid? What is the
> > 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.
>
Fair enough. I am fine with this definition.
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-02 05:45 Amit Kapila <[email protected]>
parent: Masahiko Sawada <[email protected]>
1 sibling, 0 replies; 48+ messages in thread
From: Amit Kapila @ 2026-04-02 05:45 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Peter Smith <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Thu, Apr 2, 2026 at 3:53 AM Masahiko Sawada <[email protected]> wrote:
>
> On Tue, Mar 31, 2026 at 12:40 PM Masahiko Sawada <[email protected]> wrote:
> >
> > On Tue, Mar 31, 2026 at 5:07 AM Zhijie Hou (Fujitsu)
> > <[email protected]> wrote:
> > >
> > > On Tuesday, March 31, 2026 5:36 PM Amit Kapila <[email protected]> wrote:
> > > >
> > > > On Wed, Mar 25, 2026 at 2:19 PM Peter Smith <[email protected]>
> > > > wrote:
> > > > >
> > > > > 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.
> > > > >
> > > >
> > > > 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.
> > >
> >
> > 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.
> >
>
> 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.
>
Yes, the function looks better and helps to understand the flow.
Thanks for improving it.
--
With Regards,
Amit Kapila.
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-02 06:12 Peter Smith <[email protected]>
parent: Masahiko Sawada <[email protected]>
1 sibling, 1 reply; 48+ messages in thread
From: Peter Smith @ 2026-04-02 06:12 UTC (permalink / raw)
To: Masahiko Sawada <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
Hi Sawada-San
Some review comments for v8-0001.
======
Commit message
1.
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.
~
Typo? "The existing a VARIADIC"
======
src/backend/catalog/pg_publication.c
is_publishable_table:
2.
+ /*
+ * 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;
+ }
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;
~~~
is_table_publishable_in_publication:
3.
+ * A helper function for pg_get_publication_tables() to check whether the
+ * table of the given relid is published for the specified publication.
/table of the given relid/table with the given relid/
/is published for the/is published in the/
~~~
pg_get_publication_tables:
4.
+ * 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.
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?
======
src/include/catalog/pg_proc.dat
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.
======
src/test/regress/sql/publication.sql
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.
+-- test for the EXCLUDE clause
Typo? /EXCLUDE clause/EXCEPT clause/
======
[1] https://www.postgresql.org/message-id/CAHut%2BPuSkabUB8H_hcwQz%3DBX5TWEj-8Ba%2BCP_PX78zN1fkhtKA%40ma...
Kind Regards,
Peter Smith.
Fujitsu Australia
^ permalink raw reply [nested|flat] 48+ messages in thread
* Re: Initial COPY of Logical Replication is too slow
@ 2026-04-02 22:13 Masahiko Sawada <[email protected]>
parent: Peter Smith <[email protected]>
0 siblings, 0 replies; 48+ messages in thread
From: Masahiko Sawada @ 2026-04-02 22:13 UTC (permalink / raw)
To: Peter Smith <[email protected]>; +Cc: Zhijie Hou (Fujitsu) <[email protected]>; Amit Kapila <[email protected]>; Jan Wieck <[email protected]>; [email protected] <[email protected]>
On Wed, Apr 1, 2026 at 11:12 PM Peter Smith <[email protected]> wrote:
>
> Hi Sawada-San
>
> Some review comments for v8-0001.
>
> ======
> Commit message
>
> 1.
> 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.
>
> ~
>
> Typo? "The existing a VARIADIC"
>
> ======
> src/backend/catalog/pg_publication.c
>
> is_publishable_table:
>
> 2.
> + /*
> + * 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;
> + }
>
> 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;
>
> ~~~
>
> is_table_publishable_in_publication:
>
> 3.
> + * A helper function for pg_get_publication_tables() to check whether the
> + * table of the given relid is published for the specified publication.
>
> /table of the given relid/table with the given relid/
>
> /is published for the/is published in the/
>
> ~~~
>
> pg_get_publication_tables:
>
> 4.
> + * 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.
>
> 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?
>
> ======
> src/include/catalog/pg_proc.dat
>
> 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.
>
> ======
> src/test/regress/sql/publication.sql
>
> 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.
> +-- test for the EXCLUDE clause
>
> Typo? /EXCLUDE clause/EXCEPT clause/
>
Thank you for the comments!
I've pushed the patch after incorporating these points.
Regards,
--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 48+ messages in thread
end of thread, other threads:[~2026-04-02 22:13 UTC | newest]
Thread overview: 48+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-01-19 17:44 Re: Initial COPY of Logical Replication is too slow Marcos Pegoraro <[email protected]>
2026-01-26 20:30 ` Masahiko Sawada <[email protected]>
2026-02-25 19:03 ` Masahiko Sawada <[email protected]>
2026-02-27 23:47 ` Masahiko Sawada <[email protected]>
2026-03-03 10:22 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-09 22:09 ` Masahiko Sawada <[email protected]>
2026-03-18 13:56 ` Amit Kapila <[email protected]>
2026-03-18 16:44 ` Masahiko Sawada <[email protected]>
2026-03-18 22:31 ` Masahiko Sawada <[email protected]>
2026-03-18 23:29 ` Masahiko Sawada <[email protected]>
2026-03-24 00:53 ` Bharath Rupireddy <[email protected]>
2026-03-24 18:41 ` Masahiko Sawada <[email protected]>
2026-03-24 06:54 ` Ajin Cherian <[email protected]>
2026-03-24 18:42 ` Masahiko Sawada <[email protected]>
2026-03-24 06:59 ` Peter Smith <[email protected]>
2026-03-24 18:45 ` Masahiko Sawada <[email protected]>
2026-03-24 10:47 ` Amit Kapila <[email protected]>
2026-03-24 18:57 ` Masahiko Sawada <[email protected]>
2026-03-25 05:06 ` Masahiko Sawada <[email protected]>
2026-03-25 08:48 ` Peter Smith <[email protected]>
2026-03-31 09:36 ` Amit Kapila <[email protected]>
2026-03-31 12:07 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-31 19:40 ` Masahiko Sawada <[email protected]>
2026-04-01 22:23 ` Masahiko Sawada <[email protected]>
2026-04-02 05:45 ` Amit Kapila <[email protected]>
2026-04-02 06:12 ` Peter Smith <[email protected]>
2026-04-02 22:13 ` Masahiko Sawada <[email protected]>
2026-03-31 17:28 ` Masahiko Sawada <[email protected]>
2026-04-01 05:04 ` Amit Kapila <[email protected]>
2026-04-01 17:35 ` Masahiko Sawada <[email protected]>
2026-04-02 05:28 ` Amit Kapila <[email protected]>
2026-03-26 05:44 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-26 08:35 ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-26 12:46 ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 00:29 ` Masahiko Sawada <[email protected]>
2026-03-26 10:43 ` Amit Kapila <[email protected]>
2026-03-26 23:51 ` Masahiko Sawada <[email protected]>
2026-03-27 03:16 ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-27 03:51 ` Amit Kapila <[email protected]>
2026-03-27 06:20 ` Masahiko Sawada <[email protected]>
2026-03-27 13:07 ` Marcos Pegoraro <[email protected]>
2026-04-01 00:03 ` Masahiko Sawada <[email protected]>
2026-03-30 07:16 ` Zhijie Hou (Fujitsu) <[email protected]>
2026-03-31 04:08 ` Masahiko Sawada <[email protected]>
2026-03-31 11:39 ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 19:29 ` Masahiko Sawada <[email protected]>
2026-03-30 07:42 ` Hayato Kuroda (Fujitsu) <[email protected]>
2026-03-31 01:00 ` Masahiko Sawada <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox