Restrict publishing of partitioned table with a foreign table as partition
Hi,
As part of a discussion in [1]/messages/by-id/CAA4eK1Lhh4SgiYQLNiWSNKGdVSzbd53=sr2tQCKooEphDkUtgw@mail.gmail.com, I am starting this thread to address
the issue reported for foreign tables.
Logical replication of foreign tables is not supported, and we throw
an error in this case. But when we create a publication on a
partitioned table that has a foreign table as a partition, the initial
sync of such a table is successful. We should also throw an error in
such cases.
With this patch, we will throw an error when we try to create a
publication on (or add to an existing publication) a partitioned table
with a foreign table as its partition or attach such a table to
existing published tables.
[1]: /messages/by-id/CAA4eK1Lhh4SgiYQLNiWSNKGdVSzbd53=sr2tQCKooEphDkUtgw@mail.gmail.com
Thanks and Regards,
Shlok Kyal
Attachments:
v1-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchapplication/octet-stream; name=v1-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchDownload
From bb07f35d4d3a8871eade69a5e78f5081ca90041a Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 29 Jan 2025 10:18:53 +0530
Subject: [PATCH v1] Restrict publishing of partitioned table with a foreign
table as partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign table as partition, the initial sync of such
table is successful. We should also throw an error in such cases.
With this patch we will throw an error when we try create a publication
on (or add to existing publication) a partitioned table with foreign
table as its partition. We will also throw an error when we try to
attach such table to existing published tables.
---
src/backend/catalog/pg_publication.c | 152 ++++++++++++++++++++++
src/backend/commands/publicationcmds.c | 3 +
src/backend/commands/tablecmds.c | 33 +++++
src/include/catalog/pg_publication.h | 8 ++
src/test/regress/expected/publication.out | 32 +++++
src/test/regress/sql/publication.sql | 30 +++++
6 files changed, 258 insertions(+)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 41ffd494c8..ef8a392077 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -55,6 +55,8 @@ typedef struct
static void
check_publication_add_relation(Relation targetrel)
{
+ Oid foreign_tbl_relid;
+
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -64,6 +66,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * Check if it is a partitioned table and any foreign table is its
+ * partition
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(targetrel), &foreign_tbl_relid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -695,6 +710,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /* check if schema has any foreign table as partition table */
+ check_foreign_tables_in_schema(schemaid);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1324,3 +1342,137 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign table as its partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_LEAF,
+ relform->oid);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ has_foreign_tbl = true;
+ *foreign_tbl_relid = relid;
+ }
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Check if a schema has a partitioned table which has a foreign table as its
+ * partition
+ */
+void
+check_foreign_tables_in_schema(Oid schemaid)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Oid foreign_tbl_relid;
+
+ if (check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(void)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ NameStr(relForm->relname), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 951ffabb65..7258d6c33d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -855,6 +855,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ check_foreign_tables();
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63..4cf7b7454d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19222,6 +19222,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
Oid defaultPartOid;
List *partBoundConstraint;
ParseState *pstate = make_parsestate(NULL);
+ Oid foreign_tbl_relid;
pstate->p_sourcetext = context->queryString;
@@ -19347,6 +19348,38 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If table is a partitioned table and has a foreign table as its
+ * partition and the parent relation is published.
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ if (puboids)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign table as partition of a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(attachrel))));
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..ba51f4a721 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,11 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform,
+ Oid *foreign_tbl_name);
+
+extern void check_foreign_tables_in_schema(Oid schemaid);
+
+extern void check_foreign_tables(void);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bc3898fbe5..bc00c22f16 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1885,6 +1885,38 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach a partitioned table with a foreign table as partition of a published table
+DETAIL: foreign table "part2_1" is a partition of partitioned table "part2"
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 47f0329c24..d0d5e22949 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1186,6 +1186,36 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
29.01.2025 12:16, Shlok Kyal пишет:
Hi,
As part of a discussion in [1], I am starting this thread to address
the issue reported for foreign tables.Logical replication of foreign tables is not supported, and we throw
an error in this case. But when we create a publication on a
partitioned table that has a foreign table as a partition, the initial
sync of such a table is successful. We should also throw an error in
such cases.
With this patch, we will throw an error when we try to create a
publication on (or add to an existing publication) a partitioned table
with a foreign table as its partition or attach such a table to
existing published tables.[1] : /messages/by-id/CAA4eK1Lhh4SgiYQLNiWSNKGdVSzbd53=sr2tQCKooEphDkUtgw@mail.gmail.com
Thanks and Regards,
Shlok Kyal
Hi!
Thanks for patch.
I reviewed it and made some changes.
1. we should check foreign tables (not partitioned)
2. added checking for foreign table creation
3. some little corrections
See attach
Attachments:
v2-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchtext/x-patch; charset=UTF-8; name=v2-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchDownload
From bb07f35d4d3a8871eade69a5e78f5081ca90041a Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 29 Jan 2025 10:18:53 +0530
Subject: [PATCH v1] Restrict publishing of partitioned table with a foreign
table as partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign table as partition, the initial sync of such
table is successful. We should also throw an error in such cases.
With this patch we will throw an error when we try create a publication
on (or add to existing publication) a partitioned table with foreign
table as its partition. We will also throw an error when we try to
attach such table to existing published tables.
---
src/backend/catalog/pg_publication.c | 152 ++++++++++++++++++++++
src/backend/commands/publicationcmds.c | 3 +
src/backend/commands/tablecmds.c | 33 +++++
src/include/catalog/pg_publication.h | 8 ++
src/test/regress/expected/publication.out | 32 +++++
src/test/regress/sql/publication.sql | 30 +++++
6 files changed, 258 insertions(+)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 41ffd494c8..ef8a392077 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -55,6 +55,8 @@ typedef struct
static void
check_publication_add_relation(Relation targetrel)
{
+ Oid foreign_tbl_relid;
+
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -64,6 +66,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * Check if it is a partitioned table and any foreign table is its
+ * partition
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(targetrel), &foreign_tbl_relid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -695,6 +710,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /* check if schema has any foreign table as partition table */
+ check_foreign_tables_in_schema(schemaid);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1324,3 +1342,137 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign table as its partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_LEAF,
+ relform->oid);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ has_foreign_tbl = true;
+ *foreign_tbl_relid = relid;
+ }
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Check if a schema has a partitioned table which has a foreign table as its
+ * partition
+ */
+void
+check_foreign_tables_in_schema(Oid schemaid)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Oid foreign_tbl_relid;
+
+ if (check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(void)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ NameStr(relForm->relname), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 951ffabb65..7258d6c33d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -855,6 +855,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ check_foreign_tables();
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63..4cf7b7454d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19222,6 +19222,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
Oid defaultPartOid;
List *partBoundConstraint;
ParseState *pstate = make_parsestate(NULL);
+ Oid foreign_tbl_relid;
pstate->p_sourcetext = context->queryString;
@@ -19347,6 +19348,38 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If table is a partitioned table and has a foreign table as its
+ * partition and the parent relation is published.
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ if (puboids)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign table as partition of a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(attachrel))));
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..ba51f4a721 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,11 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform,
+ Oid *foreign_tbl_name);
+
+extern void check_foreign_tables_in_schema(Oid schemaid);
+
+extern void check_foreign_tables(void);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bc3898fbe5..bc00c22f16 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1885,6 +1885,38 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach a partitioned table with a foreign table as partition of a published table
+DETAIL: foreign table "part2_1" is a partition of partitioned table "part2"
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 47f0329c24..d0d5e22949 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1186,6 +1186,36 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
v2-0002-Restrict-publishing-of-foreign-tables-and-partitione.patchtext/x-patch; charset=UTF-8; name=v2-0002-Restrict-publishing-of-foreign-tables-and-partitione.patchDownload
From 6692a2faa87c8829189baf55dc40527882abf09f Mon Sep 17 00:00:00 2001
From: Sergey Tatarintsev <s.tatarintsev@postgrespro.ru>
Date: Wed, 29 Jan 2025 20:40:16 +0700
Subject: [PATCH] Restrict publishing of foreign tables and partitioned tables
with a foreign table as partition
Added foreign table checking, creating foreign paritions + some fixes
---
src/backend/catalog/pg_publication.c | 19 ++++++++------
src/backend/commands/foreigncmds.c | 37 ++++++++++++++++++++++++++++
src/backend/commands/tablecmds.c | 30 +++++++++++++++-------
src/test/regress/sql/publication.sql | 14 +++++++++++
4 files changed, 84 insertions(+), 16 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index ef8a392077..583248c48e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1349,6 +1349,7 @@ check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
{
bool has_foreign_tbl = false;
+ *foreign_tbl_relid = InvalidOid;
if (relform->relkind == RELKIND_PARTITIONED_TABLE)
{
List *relids = NIL;
@@ -1384,7 +1385,7 @@ void
check_foreign_tables_in_schema(Oid schemaid)
{
Relation classRel;
- ScanKeyData key[2];
+ ScanKeyData key[1];
TableScanDesc scan;
HeapTuple tuple;
@@ -1394,18 +1395,15 @@ check_foreign_tables_in_schema(Oid schemaid)
Anum_pg_class_relnamespace,
BTEqualStrategyNumber, F_OIDEQ,
schemaid);
- ScanKeyInit(&key[1],
- Anum_pg_class_relkind,
- BTEqualStrategyNumber, F_CHAREQ,
- CharGetDatum(RELKIND_PARTITIONED_TABLE));
- scan = table_beginscan_catalog(classRel, 2, key);
+ scan = table_beginscan_catalog(classRel, 1, key);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
Oid foreign_tbl_relid;
- if (check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
+ if (relForm->relkind == RELKIND_PARTITIONED_TABLE &&
+ check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
{
List *ancestors = get_partition_ancestors(relForm->oid);
Oid parent_oid = relForm->oid;
@@ -1419,6 +1417,7 @@ check_foreign_tables_in_schema(Oid schemaid)
parent_oid = ancestor;
}
+ list_free(ancestors);
parent_name = get_rel_name(parent_oid);
ereport(ERROR,
@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid)
errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
get_rel_name(foreign_tbl_relid), parent_name)));
}
+ else if (relForm->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ get_rel_name(relForm->oid)),
+ errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE)));
}
table_endscan(scan);
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..f1c27ab352 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,42 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ if (stmt->base.inhRelations)
+ {
+ Oid foreign_tbl_relid;
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(RelationGetForm(rootrel), &foreign_tbl_relid))
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(relid);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(relid);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ if (puboids)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign table as partition of a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(rootrel))));
+ }
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4cf7b7454d..512028810e 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19349,10 +19349,11 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
errmsg("cannot attach temporary relation of another session as partition")));
/*
- * If table is a partitioned table and has a foreign table as its
- * partition and the parent relation is published.
+ * In case of foreign table of if table is a partitioned table and has a
+ * foreign table as its partition and the parent relation is published
*/
- if (check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid))
+ if (check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid) ||
+ attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
{
Oid schemaid = RelationGetNamespace(rel);
List *puboids = GetRelationPublications(rel->rd_id);
@@ -19370,14 +19371,25 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
puboids = list_concat_unique_oid(puboids,
GetSchemaPublications(schemaid));
}
+ list_free(ancestors);
if (puboids)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot attach a partitioned table with a foreign table as partition of a published table"),
- errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
- get_rel_name(foreign_tbl_relid),
- RelationGetRelationName(attachrel))));
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign table as partition of a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(attachrel))));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ get_rel_name(attachrel->rd_rel->oid)),
+ errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE)));
+
+ }
}
/*
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index d0d5e22949..d142238060 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1211,6 +1211,20 @@ ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create foreign partition of published table
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Can't create publication for schema with foreign tables
+CREATE SCHEMA sch4;
+CREATE FOREIGN TABLE sch4.ftable(id int) SERVER fdw_server;
+CREATE PUBLICATION pub_sch4 FOR TABLES IN SCHEMA sch4;
+
+DROP SCHEMA sch4 CASCADE;
+
DROP PUBLICATION pub1;
DROP SCHEMA sch3 CASCADE;
DROP SERVER fdw_server;
--
2.43.0
On Wed, 29 Jan 2025 at 19:21, Sergey Tatarintsev
<s.tatarintsev@postgrespro.ru> wrote:
29.01.2025 12:16, Shlok Kyal пишет:
Hi,
As part of a discussion in [1], I am starting this thread to address
the issue reported for foreign tables.Logical replication of foreign tables is not supported, and we throw
an error in this case. But when we create a publication on a
partitioned table that has a foreign table as a partition, the initial
sync of such a table is successful. We should also throw an error in
such cases.
With this patch, we will throw an error when we try to create a
publication on (or add to an existing publication) a partitioned table
with a foreign table as its partition or attach such a table to
existing published tables.[1] : /messages/by-id/CAA4eK1Lhh4SgiYQLNiWSNKGdVSzbd53=sr2tQCKooEphDkUtgw@mail.gmail.com
Thanks and Regards,
Shlok KyalHi!
Thanks for patch.
I reviewed it and made some changes.
1. we should check foreign tables (not partitioned)
2. added checking for foreign table creation
3. some little correctionsSee attach
Hi Sergey,
I have added most of the changes in v2-0002 with small changes except
one change.
@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid)
errdetail("foreign table \"%s\" is a partition of
partitioned table \"%s\"",
get_rel_name(foreign_tbl_relid), parent_name)));
}
+ else if (relForm->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ get_rel_name(relForm->oid)),
+ errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE)));
}
We should only throw error when foreign table is part of a partition
table in case of 'FOR TABLES IN SCHEMA' . We should not throw an error
otherwise because in case of 'FOR TABLES IN SCHEMA' foreign tables are
not published by default.
I have added the changes in v3-0001.
Thanks and Regards,
Shlok Kyal
Attachments:
v3-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchapplication/octet-stream; name=v3-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchDownload
From 349750385cd3df12c02654a91997f48f2abd9832 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 29 Jan 2025 10:18:53 +0530
Subject: [PATCH v3] Restrict publishing of partitioned table with a foreign
table as partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign table as partition, the initial sync of such
table is successful. We should also throw an error in such cases.
With this patch we will throw an error when we try create a publication
on (or add to existing publication) a partitioned table with foreign
table as its partition. We will also throw an error when we try to
attach such table to existing published tables.
---
src/backend/catalog/pg_publication.c | 151 ++++++++++++++++++++++
src/backend/commands/foreigncmds.c | 40 ++++++
src/backend/commands/publicationcmds.c | 3 +
src/backend/commands/tablecmds.c | 44 +++++++
src/include/catalog/pg_publication.h | 8 ++
src/test/regress/expected/publication.out | 39 ++++++
src/test/regress/sql/publication.sql | 37 ++++++
7 files changed, 322 insertions(+)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 41ffd494c8..071b428bc0 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -55,6 +55,8 @@ typedef struct
static void
check_publication_add_relation(Relation targetrel)
{
+ Oid foreign_tbl_relid;
+
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -64,6 +66,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * Check if it is a partitioned table and any foreign table is its
+ * partition
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(targetrel), &foreign_tbl_relid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -695,6 +710,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /* check if schema has any foreign table as partition table */
+ check_foreign_tables_in_schema(schemaid);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1324,3 +1342,136 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition*/
+bool
+check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
+{
+ bool has_foreign_tbl = false;
+
+ *foreign_tbl_relid = InvalidOid;
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_LEAF,
+ relform->oid);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ has_foreign_tbl = true;
+ *foreign_tbl_relid = relid;
+ }
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/* Check if a schema has a partitioned table which has a foreign partition */
+void
+check_foreign_tables_in_schema(Oid schemaid)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Oid foreign_tbl_relid;
+
+ if (check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ list_free(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(void)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ NameStr(relForm->relname), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..004edd9515 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,45 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is
+ * published
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ if (puboids)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign partition \"%s\" as partitioned table \"%s\" is published",
+ get_rel_name(relid),
+ RelationGetRelationName(rootrel))));
+ }
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 951ffabb65..7258d6c33d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -855,6 +855,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ check_foreign_tables();
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63..82e07509a8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19222,6 +19222,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
Oid defaultPartOid;
List *partBoundConstraint;
ParseState *pstate = make_parsestate(NULL);
+ Oid foreign_tbl_relid;
pstate->p_sourcetext = context->queryString;
@@ -19347,6 +19348,49 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not published.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ if (puboids)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign partition to a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(attachrel))));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot attach foreign table \"%s\" to a published table",
+ get_rel_name(attachrel->rd_id))));
+
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..ba51f4a721 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,11 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform,
+ Oid *foreign_tbl_name);
+
+extern void check_foreign_tables_in_schema(Oid schemaid);
+
+extern void check_foreign_tables(void);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bc3898fbe5..b973f41e94 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1885,6 +1885,45 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach a partitioned table with a foreign partition to a published table
+DETAIL: foreign table "part2_1" is a partition of partitioned table "part2"
+-- Can't create foreign partition of published table
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign partition "part3_1" as partitioned table "tmain" is published
+-- Can't attach foreign partition to published table
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to a published table
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 47f0329c24..b9811033dc 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1186,6 +1186,43 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
30.01.2025 19:02, Shlok Kyal пишет:
On Wed, 29 Jan 2025 at 19:21, Sergey Tatarintsev
<s.tatarintsev@postgrespro.ru> wrote:29.01.2025 12:16, Shlok Kyal пишет:
Hi,
As part of a discussion in [1], I am starting this thread to address
the issue reported for foreign tables.Logical replication of foreign tables is not supported, and we throw
an error in this case. But when we create a publication on a
partitioned table that has a foreign table as a partition, the initial
sync of such a table is successful. We should also throw an error in
such cases.
With this patch, we will throw an error when we try to create a
publication on (or add to an existing publication) a partitioned table
with a foreign table as its partition or attach such a table to
existing published tables.[1] : /messages/by-id/CAA4eK1Lhh4SgiYQLNiWSNKGdVSzbd53=sr2tQCKooEphDkUtgw@mail.gmail.com
Thanks and Regards,
Shlok KyalHi!
Thanks for patch.
I reviewed it and made some changes.
1. we should check foreign tables (not partitioned)
2. added checking for foreign table creation
3. some little correctionsSee attach
Hi Sergey,
I have added most of the changes in v2-0002 with small changes except
one change.@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid) errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"", get_rel_name(foreign_tbl_relid), parent_name))); } + else if (relForm->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add relation \"%s\" to publication", + get_rel_name(relForm->oid)), + errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE))); }We should only throw error when foreign table is part of a partition
table in case of 'FOR TABLES IN SCHEMA' . We should not throw an error
otherwise because in case of 'FOR TABLES IN SCHEMA' foreign tables are
not published by default.I have added the changes in v3-0001.
Thanks and Regards,
Shlok Kyal
Hello!
Ok, but maybe it will be correct to raise an WARNING (or at least LOG)
that some tables was skipped during publication. What do you think?
And I think we need check tables which was really published in case of
'FOR ALL TABLES' and 'FOR TABLES IN SCHEMA' in our tests. See attach.
Also it looks strange - we raise an ERROR for partitioned tables
containing foreign partitions , but just skip foreign tables as itself.
I think there should be the same behavior in both cases - raise an ERROR
or skip.
Attachments:
v4-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchtext/x-patch; charset=UTF-8; name=v4-0001-Restrict-publishing-of-partitioned-table-with-a-f.patchDownload
From 349750385cd3df12c02654a91997f48f2abd9832 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Wed, 29 Jan 2025 10:18:53 +0530
Subject: [PATCH v3] Restrict publishing of partitioned table with a foreign
table as partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign table as partition, the initial sync of such
table is successful. We should also throw an error in such cases.
With this patch we will throw an error when we try create a publication
on (or add to existing publication) a partitioned table with foreign
table as its partition. We will also throw an error when we try to
attach such table to existing published tables.
---
src/backend/catalog/pg_publication.c | 151 ++++++++++++++++++++++
src/backend/commands/foreigncmds.c | 40 ++++++
src/backend/commands/publicationcmds.c | 3 +
src/backend/commands/tablecmds.c | 44 +++++++
src/include/catalog/pg_publication.h | 8 ++
src/test/regress/expected/publication.out | 39 ++++++
src/test/regress/sql/publication.sql | 37 ++++++
7 files changed, 322 insertions(+)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 41ffd494c8..071b428bc0 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -55,6 +55,8 @@ typedef struct
static void
check_publication_add_relation(Relation targetrel)
{
+ Oid foreign_tbl_relid;
+
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
RelationGetForm(targetrel)->relkind != RELKIND_PARTITIONED_TABLE)
@@ -64,6 +66,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * Check if it is a partitioned table and any foreign table is its
+ * partition
+ */
+ if (check_partrel_has_foreign_table(RelationGetForm(targetrel), &foreign_tbl_relid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ RelationGetRelationName(targetrel)),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -695,6 +710,9 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /* check if schema has any foreign table as partition table */
+ check_foreign_tables_in_schema(schemaid);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1324,3 +1342,136 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition*/
+bool
+check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
+{
+ bool has_foreign_tbl = false;
+
+ *foreign_tbl_relid = InvalidOid;
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_LEAF,
+ relform->oid);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ has_foreign_tbl = true;
+ *foreign_tbl_relid = relid;
+ }
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/* Check if a schema has a partitioned table which has a foreign partition */
+void
+check_foreign_tables_in_schema(Oid schemaid)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Oid foreign_tbl_relid;
+
+ if (check_partrel_has_foreign_table(relForm, &foreign_tbl_relid))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ list_free(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(void)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add relation \"%s\" to publication",
+ parent_name),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ NameStr(relForm->relname), parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..004edd9515 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,45 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is
+ * published
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ if (puboids)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign partition \"%s\" as partitioned table \"%s\" is published",
+ get_rel_name(relid),
+ RelationGetRelationName(rootrel))));
+ }
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 951ffabb65..7258d6c33d 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -855,6 +855,9 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ check_foreign_tables();
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d617c4bc63..82e07509a8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19222,6 +19222,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
Oid defaultPartOid;
List *partBoundConstraint;
ParseState *pstate = make_parsestate(NULL);
+ Oid foreign_tbl_relid;
pstate->p_sourcetext = context->queryString;
@@ -19347,6 +19348,49 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not published.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel), &foreign_tbl_relid))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ if (puboids)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach a partitioned table with a foreign partition to a published table"),
+ errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"",
+ get_rel_name(foreign_tbl_relid),
+ RelationGetRelationName(attachrel))));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot attach foreign table \"%s\" to a published table",
+ get_rel_name(attachrel->rd_id))));
+
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..ba51f4a721 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,11 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform,
+ Oid *foreign_tbl_name);
+
+extern void check_foreign_tables_in_schema(Oid schemaid);
+
+extern void check_foreign_tables(void);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index bc3898fbe5..b973f41e94 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1885,6 +1885,45 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+ERROR: cannot add relation "tmain" to publication
+DETAIL: foreign table "part2_1" is a partition of partitioned table "tmain"
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach a partitioned table with a foreign partition to a published table
+DETAIL: foreign table "part2_1" is a partition of partitioned table "part2"
+-- Can't create foreign partition of published table
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign partition "part3_1" as partitioned table "tmain" is published
+-- Can't attach foreign partition to published table
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to a published table
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 47f0329c24..b9811033dc 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1186,6 +1186,43 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub1 FOR ALL TABLES;
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+DROP PUBLICATION pub1;
+DROP SCHEMA sch3 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
v4-0002-Tests-check-tables-which-was-really-published-via-FO.patchtext/x-patch; charset=UTF-8; name=v4-0002-Tests-check-tables-which-was-really-published-via-FO.patchDownload
From 8a2a4f94e4c1f35e77179e6de0d4f3f2d1c7cf1f Mon Sep 17 00:00:00 2001
From: Sergey Tatarintsev <s.tatarintsev@postgrespro.ru>
Date: Fri, 31 Jan 2025 11:57:18 +0700
Subject: [PATCH] Tests: check tables which was really published via 'FOR ALL
TABLES' and 'FOR TABLES IN SCHEMA'
---
src/backend/catalog/pg_publication.c | 1 +
src/test/regress/expected/publication.out | 26 ++++++++++++++++++++++-
src/test/regress/sql/publication.sql | 21 ++++++++++++++++--
3 files changed, 45 insertions(+), 3 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 071b428bc0..b7be142bd0 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1372,6 +1372,7 @@ check_partrel_has_foreign_table(Form_pg_class relform, Oid *foreign_tbl_relid)
if (has_foreign_tbl)
break;
}
+ list_free(relids);
}
return has_foreign_tbl;
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index b973f41e94..20dfe5c6d0 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1886,6 +1886,11 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
-- ======================================================
+-- We don't want to see previously created tables, so we'll create an empty database
+SELECT current_database() as prev_dbname
+\gset
+CREATE DATABASE publication_test_db;
+\c publication_test_db
-- Test when foreign table is a partition of a partitioned table on which
-- publication is created
SET client_min_messages = 'ERROR';
@@ -1922,8 +1927,27 @@ ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20
ERROR: cannot attach foreign table "part3_2" to a published table
DROP PUBLICATION pub1;
DROP SCHEMA sch3 CASCADE;
-DROP SERVER fdw_server;
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.ltable(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch4.ltable_part PARTITION OF sch4.ltable FOR VALUES FROM (0) TO (5);
+CREATE FOREIGN TABLE sch4.ftable(id int) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+SELECT pubname, tablename FROM pg_publication_tables ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-------------
+ pub1 | ltable_part
+ pub2 | ltable
+ pub3 | ltable_part
+(3 rows)
+
+DROP PUBLICATION pub1, pub2;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server CASCADE;
DROP FOREIGN DATA WRAPPER test_fdw;
+\c :prev_dbname
+DROP DATABASE publication_test_db;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index b9811033dc..b24c1e90c7 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1188,6 +1188,11 @@ DROP TABLE gencols;
RESET client_min_messages;
-- ======================================================
+-- We don't want to see previously created tables, so we'll create an empty database
+SELECT current_database() as prev_dbname
+\gset
+CREATE DATABASE publication_test_db;
+\c publication_test_db
-- Test when foreign table is a partition of a partitioned table on which
-- publication is created
SET client_min_messages = 'ERROR';
@@ -1220,9 +1225,21 @@ ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20
DROP PUBLICATION pub1;
DROP SCHEMA sch3 CASCADE;
-DROP SERVER fdw_server;
-DROP FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.ltable(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch4.ltable_part PARTITION OF sch4.ltable FOR VALUES FROM (0) TO (5);
+CREATE FOREIGN TABLE sch4.ftable(id int) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+SELECT pubname, tablename FROM pg_publication_tables ORDER BY pubname, tablename;
+DROP PUBLICATION pub1, pub2;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server CASCADE;
+DROP FOREIGN DATA WRAPPER test_fdw;
+\c :prev_dbname
+DROP DATABASE publication_test_db;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.43.0
On Fri, 31 Jan 2025 at 10:38, Sergey Tatarintsev
<s.tatarintsev@postgrespro.ru> wrote:
Ok, but maybe it will be correct to raise an WARNING (or at least LOG)
that some tables was skipped during publication. What do you think?And I think we need check tables which was really published in case of
'FOR ALL TABLES' and 'FOR TABLES IN SCHEMA' in our tests. See attach.Also it looks strange - we raise an ERROR for partitioned tables
containing foreign partitions , but just skip foreign tables as itself.
I think there should be the same behavior in both cases - raise an ERROR
or skip.
I believe this behavior is acceptable. We should throw an error for
partitioned tables that contain foreign partitions, as this would
include the data from these foreign tables during the initial sync,
while incremental changes would not be replicated. However, in the
case of foreign tables, neither the initial sync nor the incremental
sync will occur. Additionally, it is documented in [1]https://www.postgresql.org/docs/current/logical-replication-restrictions.html that foreign
tables are not supported, so this behavior is in line with the
documentation and is acceptable.
[1]: https://www.postgresql.org/docs/current/logical-replication-restrictions.html
Regards,
Vignesh
On Thu, 30 Jan 2025 at 17:32, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid) errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"", get_rel_name(foreign_tbl_relid), parent_name))); } + else if (relForm->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add relation \"%s\" to publication", + get_rel_name(relForm->oid)), + errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE))); }We should only throw error when foreign table is part of a partition
table in case of 'FOR TABLES IN SCHEMA' . We should not throw an error
otherwise because in case of 'FOR TABLES IN SCHEMA' foreign tables are
not published by default.I have added the changes in v3-0001.
In case of all tables publication you have retrieved all the foreign
tables and then checked if any of the foreign tables is a partition of
a partitioned table. In case of all tables in schema publication you
have retrieved all the partitioned tables and then check if it
includes foreign tables. I felt you can keep the all tables in schema
publication also similar to all tables publication(as generally the
number of foreign tables will be lesser than the tables) i.e. retrieve
all the foreign tables and then check if any of the foreign tables is
a partition of a partitioned table.
Regards,
Vignesh
On 2025-Feb-04, vignesh C wrote:
We should throw an error for partitioned tables that contain foreign
partitions, as this would include the data from these foreign tables
during the initial sync, while incremental changes would not be
replicated.
Hmm, I would support the idea of allowing partitioned tables containing
foreign partitions into publications, where only the tuples in
non-foreign partitions would be transmitted -- both during initial sync
and during replicated DML. This way, any partitioned tables with mixed
local and foreign partitions would work okay for replication. If the
subscriber wants the data in the foreign partitions, they can create the
foreign table on the subscription side and all is well. From a users
POV this is probably the most useful.
Another aspect is the case where you create the publication first and
the foreign table later. I didn't spot in the proposed patch any code
that would check whether a publication including this schema exists,
which we would have to do if we wanted to be watertight about rejecting
publications with foreign tables. But I think that would be a bad
direction to go it.
I'd rather have the table-listing code for publications skip any foreign
tables. For instance, GetPubPartitionOptionRelations() and
PublicationAddTables() should skip tables with relkind RELKIND_FOREIGN.
The sync code should also silently ignore all foreign tables.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"I am amazed at [the pgsql-sql] mailing list for the wonderful support, and
lack of hesitasion in answering a lost soul's question, I just wished the rest
of the mailing list could be like this." (Fotis)
/messages/by-id/200606261359.k5QDxE2p004593@auth-smtp.hol.gr
On Tue, 4 Feb 2025 at 21:21, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-04, vignesh C wrote:
We should throw an error for partitioned tables that contain foreign
partitions, as this would include the data from these foreign tables
during the initial sync, while incremental changes would not be
replicated.Hmm, I would support the idea of allowing partitioned tables containing
foreign partitions into publications, where only the tuples in
non-foreign partitions would be transmitted -- both during initial sync
and during replicated DML. This way, any partitioned tables with mixed
local and foreign partitions would work okay for replication. If the
subscriber wants the data in the foreign partitions, they can create the
foreign table on the subscription side and all is well. From a users
POV this is probably the most useful.Another aspect is the case where you create the publication first and
the foreign table later. I didn't spot in the proposed patch any code
that would check whether a publication including this schema exists,
which we would have to do if we wanted to be watertight about rejecting
publications with foreign tables. But I think that would be a bad
direction to go it.I'd rather have the table-listing code for publications skip any foreign
tables. For instance, GetPubPartitionOptionRelations() and
PublicationAddTables() should skip tables with relkind RELKIND_FOREIGN.
The sync code should also silently ignore all foreign tables.
We can maintain the behavior you suggested when the
PUBLISH_VIA_PARTITION_ROOT option is set to false. However, when
PUBLISH_VIA_PARTITION_ROOT is true, the table data is copied from the
root table (as intended by the user), which will also include the
foreign table data. In this case, wouldn’t it be better to throw an
error?
Regards,
Vignesh
On 2025-Feb-05, vignesh C wrote:
We can maintain the behavior you suggested when the
PUBLISH_VIA_PARTITION_ROOT option is set to false. However, when
PUBLISH_VIA_PARTITION_ROOT is true, the table data is copied from the
root table (as intended by the user), which will also include the
foreign table data. In this case, wouldn’t it be better to throw an
error?
It sounds to me a reasonable restriction that you can only add a
partitioned table to a publication if publish_via_partition_root=false.
Then the case of hybrid partitioned tables is supported, but it requires
that changes are published directly by partitions, which is sensible
anyway.
In this case, during CREATE FOREIGN TABLE of a partition with this
condition, we must check whether any publications include the schema
that the table is being created on (or attached, for ALTER TABLE ATTACH
PARTITION), and whether there are any publications that are FOR ALL
TABLES that would be affected.
(If we later figure out a way to allow publish_via_partition_root and
skip the tuples in foreign partitions, it's easy to remove the
restriction.)
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"That sort of implies that there are Emacs keystrokes which aren't obscure.
I've been using it daily for 2 years now and have yet to discover any key
sequence which makes any sense." (Paul Thomas)
On Tue, 4 Feb 2025 at 18:31, vignesh C <vignesh21@gmail.com> wrote:
On Thu, 30 Jan 2025 at 17:32, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid) errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"", get_rel_name(foreign_tbl_relid), parent_name))); } + else if (relForm->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add relation \"%s\" to publication", + get_rel_name(relForm->oid)), + errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE))); }We should only throw error when foreign table is part of a partition
table in case of 'FOR TABLES IN SCHEMA' . We should not throw an error
otherwise because in case of 'FOR TABLES IN SCHEMA' foreign tables are
not published by default.I have added the changes in v3-0001.
In case of all tables publication you have retrieved all the foreign
tables and then checked if any of the foreign tables is a partition of
a partitioned table. In case of all tables in schema publication you
have retrieved all the partitioned tables and then check if it
includes foreign tables. I felt you can keep the all tables in schema
publication also similar to all tables publication(as generally the
number of foreign tables will be lesser than the tables) i.e. retrieve
all the foreign tables and then check if any of the foreign tables is
a partition of a partitioned table.
I believe you chose this approach because partitioned tables can have
their partitions as foreign tables in a different schema. As a result,
the current approach works well for the 'TABLES IN SCHEMA' case. It
would be helpful to include a brief comment explaining this reasoning
where you're handling tables in the schema publication.
Regards,
Vignesh
On Wed, 5 Feb 2025 at 14:14, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Feb-05, vignesh C wrote:
We can maintain the behavior you suggested when the
PUBLISH_VIA_PARTITION_ROOT option is set to false. However, when
PUBLISH_VIA_PARTITION_ROOT is true, the table data is copied from the
root table (as intended by the user), which will also include the
foreign table data. In this case, wouldn’t it be better to throw an
error?It sounds to me a reasonable restriction that you can only add a
partitioned table to a publication if publish_via_partition_root=false.
Then the case of hybrid partitioned tables is supported, but it requires
that changes are published directly by partitions, which is sensible
anyway.
I have created a patch with the above approach.
We can create a publication on a partition table with foreign
partition when publish_via_partition_root=false. In this case foreign
partitions will not be published. And if
publish_via_partition_root=true we will throw an error.
Please find the v5 patch.
In this case, during CREATE FOREIGN TABLE of a partition with this
condition, we must check whether any publications include the schema
that the table is being created on (or attached, for ALTER TABLE ATTACH
PARTITION), and whether there are any publications that are FOR ALL
TABLES that would be affected.
I have handled the above cases and added tests for the same.
(If we later figure out a way to allow publish_via_partition_root and
skip the tuples in foreign partitions, it's easy to remove the
restriction.)
Thanks and Regards,
Shlok Kyal
Attachments:
v5-0001-Restrict-publishing-of-partitioned-table-with-for.patchapplication/octet-stream; name=v5-0001-Restrict-publishing-of-partitioned-table-with-for.patchDownload
From 266366aded45b4c7cdc1525c45286ef198f021c3 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Feb 2025 15:32:55 +0530
Subject: [PATCH v5] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
---
src/backend/catalog/pg_publication.c | 181 ++++++++++++++++++++--
src/backend/commands/foreigncmds.c | 47 ++++++
src/backend/commands/publicationcmds.c | 47 ++++++
src/backend/commands/tablecmds.c | 53 +++++++
src/include/catalog/pg_publication.h | 7 +
src/test/regress/expected/publication.out | 89 +++++++++++
src/test/regress/sql/publication.sql | 81 ++++++++++
7 files changed, 488 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d9..d022ff71b5 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -53,7 +53,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +64,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition
+ */
+ if (pub->pubviaroot &&
+ check_partrel_has_foreign_table(RelationGetForm(targetrel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +317,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +326,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +472,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +712,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables_in_schema(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1348,134 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Check if a schema has a partition table which has a foreign partition.
+ * The partition tables in a schema can have partitions in other schema. We also
+ * need to check if such partitions are foreign partition.
+ */
+void
+check_foreign_tables_in_schema(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (check_partrel_has_foreign_table(relForm))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ list_free(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..f41a24aea3 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,52 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 150a768d16..e71d5408c7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ if (publish_via_partition_root)
+ check_foreign_tables(stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1041,6 +1045,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables_in_schema(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ HeapTuple reltup;
+ Form_pg_class relform;
+
+ reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relform = (Form_pg_class) GETSTRUCT(reltup);
+
+ ReleaseSysCache(reltup);
+
+ if (check_partrel_has_foreign_table(relform))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5823fce934..fccc1082f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19440,6 +19440,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel)))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..b1bb864d2f 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,10 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform);
+
+extern void check_foreign_tables_in_schema(Oid schemaid, char *pubname);
+
+extern void check_foreign_tables(char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9..6a0c245bd6 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de400..49c9d98b66 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On Mon, 10 Feb 2025 at 16:11, vignesh C <vignesh21@gmail.com> wrote:
On Tue, 4 Feb 2025 at 18:31, vignesh C <vignesh21@gmail.com> wrote:
On Thu, 30 Jan 2025 at 17:32, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
@@ -1428,6 +1427,12 @@ check_foreign_tables_in_schema(Oid schemaid) errdetail("foreign table \"%s\" is a partition of partitioned table \"%s\"", get_rel_name(foreign_tbl_relid), parent_name))); } + else if (relForm->relkind == RELKIND_FOREIGN_TABLE) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot add relation \"%s\" to publication", + get_rel_name(relForm->oid)), + errdetail_relkind_not_supported(RELKIND_FOREIGN_TABLE))); }We should only throw error when foreign table is part of a partition
table in case of 'FOR TABLES IN SCHEMA' . We should not throw an error
otherwise because in case of 'FOR TABLES IN SCHEMA' foreign tables are
not published by default.I have added the changes in v3-0001.
In case of all tables publication you have retrieved all the foreign
tables and then checked if any of the foreign tables is a partition of
a partitioned table. In case of all tables in schema publication you
have retrieved all the partitioned tables and then check if it
includes foreign tables. I felt you can keep the all tables in schema
publication also similar to all tables publication(as generally the
number of foreign tables will be lesser than the tables) i.e. retrieve
all the foreign tables and then check if any of the foreign tables is
a partition of a partitioned table.I believe you chose this approach because partitioned tables can have
their partitions as foreign tables in a different schema. As a result,
the current approach works well for the 'TABLES IN SCHEMA' case. It
would be helpful to include a brief comment explaining this reasoning
where you're handling tables in the schema publication.
Correct, I used this approach because partitioned tables can have
their foreign partitions in a different schema. I have added a comment
for the same in the v5 patch [1]/messages/by-id/CANhcyEXxjq9U7BXxCba3Njz+eHpNzAsSVY2GzbV=8iy5j=UAUA@mail.gmail.com.
[1]: /messages/by-id/CANhcyEXxjq9U7BXxCba3Njz+eHpNzAsSVY2GzbV=8iy5j=UAUA@mail.gmail.com
Thanks and Regards,
Shlok Kyal
On Tue, 11 Feb 2025 at 16:55, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have handled the above cases and added tests for the same.
There is a concurrency issue with the patch:
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid,
AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind ==
RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, AccessShareLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
In an ideal scenario, the creation of a foreign table should fail if
there is an associated publication, as demonstrated below:
CREATE TABLE t(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE PUBLICATION pub1 FOR TABLE t with (publish_via_partition_root = true);
postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
ERROR: cannot create table foreign partition "part22"
DETAIL: partition table "part2" is published with option
publish_via_partition_root
Consider a scenario where the publication is being created and after
the check_partrel_has_foreign_table execution is done, concurrently
creation of foreign table is executed, then the creation will be
successful.
postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
CREATE FOREIGN TABLE
I felt the problem here is that you have released the lock:
+ if (RelationGetForm(rel)->relkind ==
RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, AccessShareLock);
We should retain the lock to fix this issue:
+ if (RelationGetForm(rel)->relkind ==
RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, NoLock);
Regards,
Vignesh
On Thu, 13 Feb 2025 at 11:25, vignesh C <vignesh21@gmail.com> wrote:
On Tue, 11 Feb 2025 at 16:55, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have handled the above cases and added tests for the same.
There is a concurrency issue with the patch: +check_partrel_has_foreign_table(Form_pg_class relform) +{ + bool has_foreign_tbl = false; + + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + List *relids = NIL; + + relids = find_all_inheritors(relform->oid, NoLock, NULL); + + foreach_oid(relid, relids) + { + Relation rel = table_open(relid, AccessShareLock); + + if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE) + has_foreign_tbl = true; + + table_close(rel, AccessShareLock); + + if (has_foreign_tbl) + break; + } + } + + return has_foreign_tbl; +}In an ideal scenario, the creation of a foreign table should fail if
there is an associated publication, as demonstrated below:
CREATE TABLE t(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE PUBLICATION pub1 FOR TABLE t with (publish_via_partition_root = true);postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
ERROR: cannot create table foreign partition "part22"
DETAIL: partition table "part2" is published with option
publish_via_partition_rootConsider a scenario where the publication is being created and after
the check_partrel_has_foreign_table execution is done, concurrently
creation of foreign table is executed, then the creation will be
successful.
postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
CREATE FOREIGN TABLEI felt the problem here is that you have released the lock: + if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE) + has_foreign_tbl = true; + + table_close(rel, AccessShareLock);We should retain the lock to fix this issue: + if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE) + has_foreign_tbl = true; + + table_close(rel, NoLock);
Hi Vignesh,
I have fixed the issue. Attached the updated v6 patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v6-0001-Restrict-publishing-of-partitioned-table-with-for.patchapplication/octet-stream; name=v6-0001-Restrict-publishing-of-partitioned-table-with-for.patchDownload
From cd882eb524b192e6260d6459c06200229bdd6a7d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Feb 2025 15:32:55 +0530
Subject: [PATCH v6] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
---
src/backend/catalog/pg_publication.c | 181 ++++++++++++++++++++--
src/backend/commands/foreigncmds.c | 47 ++++++
src/backend/commands/publicationcmds.c | 47 ++++++
src/backend/commands/tablecmds.c | 53 +++++++
src/include/catalog/pg_publication.h | 7 +
src/test/regress/expected/publication.out | 89 +++++++++++
src/test/regress/sql/publication.sql | 81 ++++++++++
7 files changed, 488 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d9..520fa2f382 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -53,7 +53,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +64,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition
+ */
+ if (pub->pubviaroot &&
+ check_partrel_has_foreign_table(RelationGetForm(targetrel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +317,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +326,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +472,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +712,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables_in_schema(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1348,134 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Check if a schema has a partition table which has a foreign partition.
+ * The partition tables in a schema can have partitions in other schema. We also
+ * need to check if such partitions are foreign partition.
+ */
+void
+check_foreign_tables_in_schema(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (check_partrel_has_foreign_table(relForm))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ list_free(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
+/* Check if any foreign table is a partition table */
+void
+check_foreign_tables(char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[1];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_FOREIGN_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 1, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (relForm->relispartition)
+ {
+ Oid parent_oid;
+ char *parent_name;
+ List *ancestors = get_partition_ancestors(relForm->oid);
+
+ parent_oid = llast_oid(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..f41a24aea3 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,52 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 150a768d16..e71d5408c7 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ if (publish_via_partition_root)
+ check_foreign_tables(stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1041,6 +1045,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables_in_schema(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ HeapTuple reltup;
+ Form_pg_class relform;
+
+ reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relform = (Form_pg_class) GETSTRUCT(reltup);
+
+ ReleaseSysCache(reltup);
+
+ if (check_partrel_has_foreign_table(relform))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5823fce934..fccc1082f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19440,6 +19440,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel)))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..b1bb864d2f 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,10 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform);
+
+extern void check_foreign_tables_in_schema(Oid schemaid, char *pubname);
+
+extern void check_foreign_tables(char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9..6a0c245bd6 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de400..49c9d98b66 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On Thu, 13 Feb 2025 at 15:50, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have fixed the issue. Attached the updated v6 patch.
There is another concurrency issue:
In case of create publication for all tables with
publish_via_partition_root we will call check_foreign_tables:
@@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate,
CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any foreign table is a part of partitioned table */
+ if (publish_via_partition_root)
+ check_foreign_tables(stmt->pubname);
At the time of check in check_foreign_tables, there are no foreign
tables so this check will be successful:
+check_foreign_tables_in_schema(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[0],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+ ScanKeyInit(&key[1],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ scan = table_beginscan_catalog(classRel, 2, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
Now immediately after execution of this, create a foreign table:
postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
CREATE FOREIGN TABLE
And then continue execution of create publication, it will also be successful:
postgres=# create publication pub1 for all tables with (
publish_via_partition_root =true);
CREATE PUBLICATION
One probable way to fix this is to do the search similar to
check_foreign_tables_in_schema where we can skip including schemaid
key for all tables.
How about something like the attached patch.
Regards,
Vignesh
Attachments:
Concurrency_all_tables_issue.patchtext/x-patch; charset=US-ASCII; name=Concurrency_all_tables_issue.patchDownload
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 520fa2f382..3214104dec 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -1388,21 +1388,24 @@ check_foreign_tables_in_schema(Oid schemaid, char *pubname)
{
Relation classRel;
ScanKeyData key[2];
+ int keycount = 0;
TableScanDesc scan;
HeapTuple tuple;
classRel = table_open(RelationRelationId, AccessShareLock);
- ScanKeyInit(&key[0],
- Anum_pg_class_relnamespace,
- BTEqualStrategyNumber, F_OIDEQ,
- schemaid);
- ScanKeyInit(&key[1],
+ ScanKeyInit(&key[keycount++],
Anum_pg_class_relkind,
BTEqualStrategyNumber, F_CHAREQ,
CharGetDatum(RELKIND_PARTITIONED_TABLE));
-
- scan = table_beginscan_catalog(classRel, 2, key);
+
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
@@ -1436,46 +1439,3 @@ check_foreign_tables_in_schema(Oid schemaid, char *pubname)
table_endscan(scan);
table_close(classRel, AccessShareLock);
}
-
-/* Check if any foreign table is a partition table */
-void
-check_foreign_tables(char *pubname)
-{
- Relation classRel;
- ScanKeyData key[1];
- TableScanDesc scan;
- HeapTuple tuple;
-
- classRel = table_open(RelationRelationId, AccessShareLock);
-
- ScanKeyInit(&key[0],
- Anum_pg_class_relkind,
- BTEqualStrategyNumber, F_CHAREQ,
- CharGetDatum(RELKIND_FOREIGN_TABLE));
-
- scan = table_beginscan_catalog(classRel, 1, key);
- while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
- {
- Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
-
- if (relForm->relispartition)
- {
- Oid parent_oid;
- char *parent_name;
- List *ancestors = get_partition_ancestors(relForm->oid);
-
- parent_oid = llast_oid(ancestors);
- parent_name = get_rel_name(parent_oid);
-
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
- "publish_via_partition_root", pubname),
- errdetail("partition table \"%s\" in publication contains a foreign partition",
- parent_name)));
- }
- }
-
- table_endscan(scan);
- table_close(classRel, AccessShareLock);
-}
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index e71d5408c7..b8da4ed130 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -878,7 +878,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
{
/* Check if any foreign table is a part of partitioned table */
if (publish_via_partition_root)
- check_foreign_tables(stmt->pubname);
+ check_foreign_tables_in_schema(InvalidOid, stmt->pubname);
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
@@ -1057,7 +1057,7 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
char *pubname = stmt->pubname;
if (pubform->puballtables)
- check_foreign_tables(pubname);
+ check_foreign_tables_in_schema(InvalidOid, pubname);
schemaoids = GetPublicationSchemas(pubform->oid);
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index b1bb864d2f..fcd428465e 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -196,6 +196,4 @@ extern bool check_partrel_has_foreign_table(Form_pg_class relform);
extern void check_foreign_tables_in_schema(Oid schemaid, char *pubname);
-extern void check_foreign_tables(char *pubname);
-
#endif /* PG_PUBLICATION_H */
On Thu, 13 Feb 2025 at 20:12, vignesh C <vignesh21@gmail.com> wrote:
On Thu, 13 Feb 2025 at 15:50, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have fixed the issue. Attached the updated v6 patch.
There is another concurrency issue: In case of create publication for all tables with publish_via_partition_root we will call check_foreign_tables: @@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) /* Associate objects with the publication. */ if (stmt->for_all_tables) { + /* Check if any foreign table is a part of partitioned table */ + if (publish_via_partition_root) + check_foreign_tables(stmt->pubname);At the time of check in check_foreign_tables, there are no foreign tables so this check will be successful: +check_foreign_tables_in_schema(Oid schemaid, char *pubname) +{ + Relation classRel; + ScanKeyData key[2]; + TableScanDesc scan; + HeapTuple tuple; + + classRel = table_open(RelationRelationId, AccessShareLock); + + ScanKeyInit(&key[0], + Anum_pg_class_relnamespace, + BTEqualStrategyNumber, F_OIDEQ, + schemaid); + ScanKeyInit(&key[1], + Anum_pg_class_relkind, + BTEqualStrategyNumber, F_CHAREQ, + CharGetDatum(RELKIND_PARTITIONED_TABLE)); + + scan = table_beginscan_catalog(classRel, 2, key); + while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)Now immediately after execution of this, create a foreign table:
postgres=# CREATE FOREIGN TABLE part22 PARTITION OF part2 FOR VALUES
FROM (10) TO (15) SERVER fdw;
CREATE FOREIGN TABLEAnd then continue execution of create publication, it will also be successful:
postgres=# create publication pub1 for all tables with (
publish_via_partition_root =true);
CREATE PUBLICATIONOne probable way to fix this is to do the search similar to
check_foreign_tables_in_schema where we can skip including schemaid
key for all tables.How about something like the attached patch.
Hi Vignesh,
I have used the changes suggested by you. Also I have updated the
comments and the function name.
Thanks and Regards,
Shlok Kyal
Attachments:
v7-0001-Restrict-publishing-of-partitioned-table-with-for.patchapplication/octet-stream; name=v7-0001-Restrict-publishing-of-partitioned-table-with-for.patchDownload
From a7528ba27c7dbd3159c94363c650f9674a63d5e1 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Feb 2025 15:32:55 +0530
Subject: [PATCH v7] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
---
src/backend/catalog/pg_publication.c | 146 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 47 +++++++
src/backend/commands/publicationcmds.c | 47 +++++++
src/backend/commands/tablecmds.c | 53 ++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 +++++++++++++
src/test/regress/sql/publication.sql | 81 ++++++++++++
7 files changed, 451 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d9..2df2b1d94a 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -53,7 +53,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +64,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition
+ */
+ if (pub->pubviaroot &&
+ check_partrel_has_foreign_table(RelationGetForm(targetrel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +317,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +326,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +472,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +712,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1348,99 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[2];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (check_partrel_has_foreign_table(relForm))
+ {
+ List *ancestors = get_partition_ancestors(relForm->oid);
+ Oid parent_oid = relForm->oid;
+ char *parent_name;
+
+ foreach_oid(ancestor, ancestors)
+ {
+ Oid ancestor_schemaid = get_rel_namespace(ancestor);
+
+ if (ancestor_schemaid == schemaid)
+ parent_oid = ancestor;
+ }
+
+ list_free(ancestors);
+ parent_name = get_rel_name(parent_oid);
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ parent_name)));
+ }
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54..f41a24aea3 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,52 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 150a768d16..e1121a94f9 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1041,6 +1045,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ HeapTuple reltup;
+ Form_pg_class relform;
+
+ reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relform = (Form_pg_class) GETSTRUCT(reltup);
+
+ ReleaseSysCache(reltup);
+
+ if (check_partrel_has_foreign_table(relform))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 5823fce934..fccc1082f4 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19440,6 +19440,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel)))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a861..50705d12c9 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9..6a0c245bd6 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de400..49c9d98b66 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On Fri, 14 Feb 2025 at 12:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have used the changes suggested by you. Also I have updated the
comments and the function name.
There is another concurrency issue possible:
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = NIL;
+
+ relids = find_all_inheritors(relform->oid, NoLock, NULL);
Create a publication with publish_via_partition_root as true, hold the
execution after check_partrel_has_foreign_table execution finishes.
Then parallely execute the following:
CREATE TABLE t1(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t1 FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t1 FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE FOREIGN TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (10)
TO (15) SERVER fdw
Now both the partitioned table having foreign table and a publication
will be created.
Regards,
Vignesh
On Mon, 17 Feb 2025 at 20:13, vignesh C <vignesh21@gmail.com> wrote:
On Fri, 14 Feb 2025 at 12:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have used the changes suggested by you. Also I have updated the
comments and the function name.There is another concurrency issue possible: +/* Check if a partitioned table has a foreign partition */ +bool +check_partrel_has_foreign_table(Form_pg_class relform) +{ + bool has_foreign_tbl = false; + + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + List *relids = NIL; + + relids = find_all_inheritors(relform->oid, NoLock, NULL);Create a publication with publish_via_partition_root as true, hold the
execution after check_partrel_has_foreign_table execution finishes.
Then parallely execute the following:
CREATE TABLE t1(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t1 FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t1 FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE FOREIGN TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (10)
TO (15) SERVER fdwNow both the partitioned table having foreign table and a publication
will be created.
Hi Vignesh,
I have addressed the above issue. If we take a ShareLock on the
pg_class, we won't be able to create table concurrently, which may
address the issue. Thoughts?
I have attached the v8 patch here.
Thanks and Regards,
Shlok Kyal
Attachments:
v8-0001-Restrict-publishing-of-partitioned-table-with-for.patchapplication/octet-stream; name=v8-0001-Restrict-publishing-of-partitioned-table-with-for.patchDownload
From 56cf18870d442cc3c23173997dbca8a060c65f43 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Feb 2025 15:32:55 +0530
Subject: [PATCH v8] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
We have introduced two functions 'check_partrel_has_foreign_table' and
'check_foreign_tables'. In 'check_partrel_has_foreign_table' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take a AccessShareLock on each partition table
to avoid creation of foreign partition concurrently.
In 'check_foreign_tables' if schema id is provided we check for each
partitioned table in that schema if it has a foreign partition, or if
schema id is not provided we check for each partitioned table in the
database if it has a foreign partition. While doing so we take a
ShareLock on pg_class so no partition table is created concurrently after
this check.
---
src/backend/catalog/pg_publication.c | 137 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 47 ++++++++
src/backend/commands/publicationcmds.c | 47 ++++++++
src/backend/commands/tablecmds.c | 53 +++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 ++++++++++++++
src/test/regress/sql/publication.sql | 81 +++++++++++++
7 files changed, 442 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..d45577679ec 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -53,7 +53,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +64,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition
+ */
+ if (pub->pubviaroot &&
+ check_partrel_has_foreign_table(RelationGetForm(targetrel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +317,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +326,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +472,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +712,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1348,90 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_class to restrict addition of new partitioned table
+ * which may contain a foreign partition while publication is being created.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (check_partrel_has_foreign_table(relForm))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relForm->oid))));
+ }
+
+ table_endscan(scan);
+ table_close(classRel, NoLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..f41a24aea39 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,52 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 150a768d16f..e1121a94f92 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -876,6 +876,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1041,6 +1045,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ HeapTuple reltup;
+ Form_pg_class relform;
+
+ reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relform = (Form_pg_class) GETSTRUCT(reltup);
+
+ ReleaseSysCache(reltup);
+
+ if (check_partrel_has_foreign_table(relform))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 72a1b64c2a2..e96e58903ec 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19440,6 +19440,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel)))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..50705d12c98 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..6a0c245bd60 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..49c9d98b668 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On Tue, 18 Feb 2025 at 15:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Mon, 17 Feb 2025 at 20:13, vignesh C <vignesh21@gmail.com> wrote:
On Fri, 14 Feb 2025 at 12:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have used the changes suggested by you. Also I have updated the
comments and the function name.There is another concurrency issue possible: +/* Check if a partitioned table has a foreign partition */ +bool +check_partrel_has_foreign_table(Form_pg_class relform) +{ + bool has_foreign_tbl = false; + + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + List *relids = NIL; + + relids = find_all_inheritors(relform->oid, NoLock, NULL);Create a publication with publish_via_partition_root as true, hold the
execution after check_partrel_has_foreign_table execution finishes.
Then parallely execute the following:
CREATE TABLE t1(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t1 FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t1 FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE FOREIGN TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (10)
TO (15) SERVER fdwNow both the partitioned table having foreign table and a publication
will be created.Hi Vignesh,
I have addressed the above issue. If we take a ShareLock on the
pg_class, we won't be able to create table concurrently, which may
address the issue. Thoughts?
I have attached the v8 patch here.
Since pg_class is a very common table it is not a good idea to take
ShareLock on it. Will it be possible to use pg_partitioned_table table
instead?
Regards,
Vignesh
On Thu, 20 Feb 2025 at 21:18, vignesh C <vignesh21@gmail.com> wrote:
On Tue, 18 Feb 2025 at 15:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Mon, 17 Feb 2025 at 20:13, vignesh C <vignesh21@gmail.com> wrote:
On Fri, 14 Feb 2025 at 12:59, Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
I have used the changes suggested by you. Also I have updated the
comments and the function name.There is another concurrency issue possible: +/* Check if a partitioned table has a foreign partition */ +bool +check_partrel_has_foreign_table(Form_pg_class relform) +{ + bool has_foreign_tbl = false; + + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + List *relids = NIL; + + relids = find_all_inheritors(relform->oid, NoLock, NULL);Create a publication with publish_via_partition_root as true, hold the
execution after check_partrel_has_foreign_table execution finishes.
Then parallely execute the following:
CREATE TABLE t1(id int) PARTITION BY RANGE(id);
CREATE TABLE part1 PARTITION OF t1 FOR VALUES FROM (0) TO (5);
CREATE TABLE part2 PARTITION OF t1 FOR VALUES FROM (5) TO (15)
PARTITION BY RANGE(id);
CREATE FOREIGN TABLE part2_1 PARTITION OF part2 FOR VALUES FROM (10)
TO (15) SERVER fdwNow both the partitioned table having foreign table and a publication
will be created.Hi Vignesh,
I have addressed the above issue. If we take a ShareLock on the
pg_class, we won't be able to create table concurrently, which may
address the issue. Thoughts?
I have attached the v8 patch here.Since pg_class is a very common table it is not a good idea to take
ShareLock on it. Will it be possible to use pg_partitioned_table table
instead?
Hi Vignesh,
I have addressed comments and I have modified the patch to take a
ShareLock on pg_partitioned_table table.
Please find the updated v9 patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v9-0001-Restrict-publishing-of-partitioned-table-with-for.patchapplication/octet-stream; name=v9-0001-Restrict-publishing-of-partitioned-table-with-for.patchDownload
From bda55bfffff9f0487730f5ddda34bc0af635faf0 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Tue, 11 Feb 2025 15:32:55 +0530
Subject: [PATCH v9] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
We have introduced two functions 'check_partrel_has_foreign_table' and
'check_foreign_tables'. In 'check_partrel_has_foreign_table' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take a AccessShareLock on each partition table
to avoid creation of foreign partition concurrently.
In 'check_foreign_tables' if schema id is provided we check for each
partitioned table in that schema if it has a foreign partition, or if
schema id is not provided we check for each partitioned table in the
database if it has a foreign partition. While doing so we take a
ShareLock on pg_partitioned_table so no partition table is created
concurrently after this check.
---
src/backend/catalog/pg_publication.c | 141 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 47 ++++++++
src/backend/commands/publicationcmds.c | 47 ++++++++
src/backend/commands/tablecmds.c | 53 ++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 ++++++++++++++
src/test/regress/sql/publication.sql | 81 +++++++++++++
7 files changed, 446 insertions(+), 17 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..d3d8173509e 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,6 +26,7 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
@@ -53,7 +54,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +65,19 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition
+ */
+ if (pub->pubviaroot &&
+ check_partrel_has_foreign_table(RelationGetForm(targetrel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +318,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +327,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +473,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +713,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1349,93 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Form_pg_class relform)
+{
+ bool has_foreign_tbl = false;
+
+ if (relform->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ List *relids = find_all_inheritors(relform->oid, NoLock, NULL);
+
+ foreach_oid(relid, relids)
+ {
+ Relation rel = table_open(relid, AccessShareLock);
+
+ if (RelationGetForm(rel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+
+ table_close(rel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_class to restrict addition of new partitioned table
+ * which may contain a foreign partition while publication is being created.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ Relation partRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+ partRel = table_open(PartitionedRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ if (check_partrel_has_foreign_table(relForm))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relForm->oid))));
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+ table_close(partRel, NoLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..f41a24aea39 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,52 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..f74cbb2abb3 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -915,6 +915,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1084,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ HeapTuple reltup;
+ Form_pg_class relform;
+
+ reltup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relform = (Form_pg_class) GETSTRUCT(reltup);
+
+ ReleaseSysCache(reltup);
+
+ if (check_partrel_has_foreign_table(relform))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1202544ebd0..dac1db2ce50 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19644,6 +19644,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(RelationGetForm(attachrel)))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..50705d12c98 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Form_pg_class relform);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..6a0c245bd60 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..49c9d98b668 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
One thing that bothers me a bit about this is that there's no single
code comment where this restriction it documented in full; in fact it
doesn't seem documented anywhere, only in the commit message.
I think check_foreign_tables() is a good place to add an explanatory
comment; other places can reference that. For instance, add something
like
/*
* Protect against including foreign tables that are partitions of
* partitioned tables published by the given publication. This would
* not work properly, because <!-- explain reason -->, so we disallow
* the case here and in all DDL commands that would end up creating
* such a case indirectly.
*/
Then for instance in check_publication_add_relation() and
ATExecAttachPartition() you comment would say /* if the would-be
partition is a foreign table, verify that the partitioned table is not
in a publication with publish_via_root=false. See check_foreign_tables
for details */
Also, surely we should document this restriction in the SGML docs
somewhere.
Would it be better if check_partrel_has_foreign_table() used
RelationGetPartitionDesc(omit_detached=true) instead of
find_all_inheritors()?
I'm wary of all those accesses of subscription/publication catalogs in
DDL code. Maybe I worry about nothing, but I cannot but feel that we're
missing one layer of abstraction there (including possibly some caching
on top of syscache).
I think this
castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
is better written
linitial_node(RangeVar, stmt->base.inhRelations);
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Tiene valor aquel que admite que es un cobarde" (Fernandel)
On Mon, 24 Mar 2025 at 21:17, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
One thing that bothers me a bit about this is that there's no single
code comment where this restriction it documented in full; in fact it
doesn't seem documented anywhere, only in the commit message.I think check_foreign_tables() is a good place to add an explanatory
comment; other places can reference that. For instance, add something
like/*
* Protect against including foreign tables that are partitions of
* partitioned tables published by the given publication. This would
* not work properly, because <!-- explain reason -->, so we disallow
* the case here and in all DDL commands that would end up creating
* such a case indirectly.
*/Then for instance in check_publication_add_relation() and
ATExecAttachPartition() you comment would say /* if the would-be
partition is a foreign table, verify that the partitioned table is not
in a publication with publish_via_root=false. See check_foreign_tables
for details */
I have added comments as suggested above.
Also, surely we should document this restriction in the SGML docs
somewhere.
I have added comment in create_publication.sgml
Would it be better if check_partrel_has_foreign_table() used
RelationGetPartitionDesc(omit_detached=true) instead of
find_all_inheritors()?I'm wary of all those accesses of subscription/publication catalogs in
DDL code. Maybe I worry about nothing, but I cannot but feel that we're
missing one layer of abstraction there (including possibly some caching
on top of syscache).
I also think using RelationGetPartitionDesc would be better and made the change.
I think this
castNode(RangeVar, lfirst(list_head(stmt->base.inhRelations)));
is better written
linitial_node(RangeVar, stmt->base.inhRelations);
Fixed.
I have attached the updated v10 patch with the above changes.
Thanks and Regards,
Shlok Kyal
Attachments:
v10-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v10-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From f8b13f85faae6b8bd4594ac84504ac8fa0cd77d0 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 28 Mar 2025 11:15:09 +0530
Subject: [PATCH v10] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
We have introduced two functions 'check_partrel_has_foreign_table' and
'check_foreign_tables'. In 'check_partrel_has_foreign_table' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take a AccessShareLock on each partition table
to avoid creation of foreign partition concurrently.
In 'check_foreign_tables' if schema id is provided we check for each
partitioned table in that schema if it has a foreign partition, or if
schema id is not provided we check for each partitioned table in the
database if it has a foreign partition. While doing so we take a
ShareLock on pg_partitioned_table so no partition table is created
concurrently after this check.
---
doc/src/sgml/ref/create_publication.sgml | 5 +
src/backend/catalog/pg_publication.c | 154 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 48 +++++++
src/backend/commands/publicationcmds.c | 43 ++++++
src/backend/commands/tablecmds.c | 57 ++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 +++++++++++++
src/test/regress/sql/publication.sql | 81 ++++++++++++
8 files changed, 465 insertions(+), 17 deletions(-)
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 73f0c8d89fb..c4e993b1c1a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -257,6 +257,11 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
If this is enabled, <literal>TRUNCATE</literal> operations performed
directly on partitions are not replicated.
</para>
+
+ <para>
+ If this is enabled, we cannot include a foreign table or a partitioned
+ table with a foreign partition in the publication.
+ </para>
</listitem>
</varlistentry>
</variablelist></para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..0daa5be3469 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,12 +26,14 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "partitioning/partdesc.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -53,7 +55,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +66,18 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition. See check_foreign_tables for details.
+ */
+ if (pub->pubviaroot && check_partrel_has_foreign_table(targetrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +318,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +327,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +473,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +713,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1349,106 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Relation rel)
+{
+ bool has_foreign_tbl = false;
+
+ if (RelationGetForm(rel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ Relation childrel = table_open(pd->oids[i], AccessShareLock);
+
+ if (RelationGetForm(childrel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+ else
+ has_foreign_tbl = check_partrel_has_foreign_table(childrel);
+
+ table_close(childrel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Protect against including foreign tables that are partitions of partitioned
+ * tables published by the given publication when publish_via_root_partition is
+ * true. This will not work correctly as the initial data from the foreign
+ * table can be replicated by the tablesync worker even though replication of
+ * foreign table is not supported. So we disallow the case here and in all DDL
+ * commands that would end up creating such a case indirectly.
+ *
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_partitioned_table to restrict addition of new
+ * partitioned table which may contain a foreign partition while publication is
+ * being created.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ Relation partRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+ partRel = table_open(PartitionedRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Relation pubrel = table_open(relForm->oid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relForm->oid))));
+
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+ table_close(partRel, NoLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..abc23baa59e 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,53 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ * See check_foreign_tables for details.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = linitial_node(RangeVar, stmt->base.inhRelations);
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..e5fa1bf5b09 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -915,6 +915,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1084,45 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition. See check_foreign_tables for details.
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel = table_open(relid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+
+ table_close(pubrel, NoLock);
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..fc4db92bde7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19570,6 +19570,10 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
* ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
*
* Return the address of the newly attached partition.
+ *
+ * If the would-be partition is a foreign table or a partitioned table with
+ * foreign partition, verify that the partitioned table is not in a publication
+ * with publish_via_partition_root=true. See check_foreign_tables for details.
*/
static ObjectAddress
ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
@@ -19714,6 +19718,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(attachrel))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..e2919da2541 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Relation rel);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..6a0c245bd60 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..49c9d98b668 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On 2025-Mar-28, Shlok Kyal wrote:
On Mon, 24 Mar 2025 at 21:17, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Also, surely we should document this restriction in the SGML docs
somewhere.I have added comment in create_publication.sgml
Hmm, okay, but "We cannot" is not the style used in the documentation.
In addition, I think this mechanism should be mentioned in
logical-replication.sgml; currently there's a note in the Restrictions
section about foreign tables, which should be expanded to explain this.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
On Fri, 28 Mar 2025 at 16:35, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-28, Shlok Kyal wrote:
On Mon, 24 Mar 2025 at 21:17, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Also, surely we should document this restriction in the SGML docs
somewhere.I have added comment in create_publication.sgml
Hmm, okay, but "We cannot" is not the style used in the documentation.
In addition, I think this mechanism should be mentioned in
logical-replication.sgml; currently there's a note in the Restrictions
section about foreign tables, which should be expanded to explain this.
I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'
I have attached the updated v11 patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v11-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v11-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From 6fc197f6263db9c08af39db340ef7d7dd955c957 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 28 Mar 2025 11:15:09 +0530
Subject: [PATCH v11] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
We have introduced two functions 'check_partrel_has_foreign_table' and
'check_foreign_tables'. In 'check_partrel_has_foreign_table' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take a AccessShareLock on each partition table
to avoid creation of foreign partition concurrently.
In 'check_foreign_tables' if schema id is provided we check for each
partitioned table in that schema if it has a foreign partition, or if
schema id is not provided we check for each partitioned table in the
database if it has a foreign partition. While doing so we take a
ShareLock on pg_partitioned_table so no partition table is created
concurrently after this check.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_publication.sgml | 5 +
src/backend/catalog/pg_publication.c | 166 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 48 +++++++
src/backend/commands/publicationcmds.c | 43 ++++++
src/backend/commands/tablecmds.c | 57 ++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 ++++++++++++
src/test/regress/sql/publication.sql | 81 +++++++++++
9 files changed, 486 insertions(+), 18 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f288c049a5c..ce60a1b391c 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,7 +2154,15 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is supported only for tables, including partitioned tables,
+ except when they contain foreign partitions. If
+ <literal>publish_via_partition_root</literal> is set to
+ <literal>true</literal> for a publication table with foreign partitions, or
+ if an attempt is made to replicate such a table, an error is thrown.
+ Additionally, when replicating a partitioned table where
+ <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal> and foreign partitions are present, all partitions
+ are replicated except the foreign partitions.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 73f0c8d89fb..47216fc4789 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -257,6 +257,11 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
If this is enabled, <literal>TRUNCATE</literal> operations performed
directly on partitions are not replicated.
</para>
+
+ <para>
+ If this is enabled, a foreign table or a partitioned table with a
+ foreign partition is not allowed in the publication.
+ </para>
</listitem>
</varlistentry>
</variablelist></para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..77299b75cde 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,12 +26,14 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "partitioning/partdesc.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -53,7 +55,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +66,18 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition. See check_foreign_tables for details.
+ */
+ if (pub->pubviaroot && check_partrel_has_foreign_table(targetrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +318,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +327,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +473,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +713,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1349,118 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Relation rel)
+{
+ bool has_foreign_tbl = false;
+
+ if (RelationGetForm(rel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ Relation childrel = table_open(pd->oids[i], AccessShareLock);
+
+ if (RelationGetForm(childrel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+ else
+ has_foreign_tbl = check_partrel_has_foreign_table(childrel);
+
+ table_close(childrel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Protect against including foreign tables that are partitions of partitioned
+ * tables published by the given publication when publish_via_root_partition is
+ * true. This will not work correctly as the initial data from the foreign
+ * table can be replicated by the tablesync worker even though replication of
+ * foreign table is not supported because when publish_via_root_partition is
+ * true, the root table is included in pg_subscription_rel catalog table and
+ * tablesync worker cannot distinguish data from foreign partition. So we
+ * disallow the case here and in all DDL commands that would end up creating
+ * such a case indirectly.
+ *
+ * When publish_via_root_partition is set to false, leaf partitions are included
+ * in pg_subscription_rel catalog table. So, when we include a partition table
+ * with foreign partition in a publication, we skip including foreign partitions
+ * to pg_subscription_rel catalog table. So, the foreign partitions are not
+ * replicated.
+ *
+ *
+ * check_foreign_tables
+ *
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_partitioned_table to restrict addition of new
+ * partitioned table which may contain a foreign partition while publication is
+ * being created.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ Relation partRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+ partRel = table_open(PartitionedRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Relation pubrel = table_open(relForm->oid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relForm->oid))));
+
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+ table_close(partRel, NoLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..abc23baa59e 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,53 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ * See check_foreign_tables for details.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = linitial_node(RangeVar, stmt->base.inhRelations);
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..e5fa1bf5b09 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -915,6 +915,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1084,45 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition. See check_foreign_tables for details.
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel = table_open(relid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+
+ table_close(pubrel, NoLock);
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 10624353b0a..beee7010481 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19619,6 +19619,10 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
* ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
*
* Return the address of the newly attached partition.
+ *
+ * If the would-be partition is a foreign table or a partitioned table with
+ * foreign partition, verify that the partitioned table is not in a publication
+ * with publish_via_partition_root=true. See check_foreign_tables for details.
*/
static ObjectAddress
ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
@@ -19763,6 +19767,59 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(attachrel))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..e2919da2541 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Relation rel);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..6a0c245bd60 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..49c9d98b668 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
01.04.2025 21:48, Shlok Kyal пишет:
On Fri, 28 Mar 2025 at 16:35, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-28, Shlok Kyal wrote:
On Mon, 24 Mar 2025 at 21:17, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Also, surely we should document this restriction in the SGML docs
somewhere.I have added comment in create_publication.sgml
Hmm, okay, but "We cannot" is not the style used in the documentation.
In addition, I think this mechanism should be mentioned in
logical-replication.sgml; currently there's a note in the Restrictions
section about foreign tables, which should be expanded to explain this.I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'I have attached the updated v11 patch.
Thanks and Regards,
Shlok Kyal
Hi!
I looked at the latest version of the patch, and think that we should
free puboids list here:
diff --git a/src/backend/commands/tablecmds.c
b/src/backend/commands/tablecmds.c
index 6a128f7bd4e..4254654cc24 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -20122,6 +20122,7 @@ ATExecAttachPartition(List **wqueue, Relation
rel, PartitionCmd *cmd,
relname)));
}
}
+ list_free(puboids);
}
/*
--
With best regards,
Sergey Tatarintsev,
PostgresPro
On Fri, 4 Apr 2025 at 10:36, Sergey Tatarintsev
<s.tatarintsev@postgrespro.ru> wrote:
01.04.2025 21:48, Shlok Kyal пишет:
On Fri, 28 Mar 2025 at 16:35, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Mar-28, Shlok Kyal wrote:
On Mon, 24 Mar 2025 at 21:17, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Also, surely we should document this restriction in the SGML docs
somewhere.I have added comment in create_publication.sgml
Hmm, okay, but "We cannot" is not the style used in the documentation.
In addition, I think this mechanism should be mentioned in
logical-replication.sgml; currently there's a note in the Restrictions
section about foreign tables, which should be expanded to explain this.I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'I have attached the updated v11 patch.
Thanks and Regards,
Shlok KyalHi!
I looked at the latest version of the patch, and think that we should
free puboids list here:diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 6a128f7bd4e..4254654cc24 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -20122,6 +20122,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd, relname))); } } + list_free(puboids); }/*
Hi Sergey,
Thanks for reviewing the patch.
I have fixed the comment. I also found other places where we should
free the relids, schemaoids, and puboids list. I have added changes
for those as well.
I have attached the updated patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v12-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v12-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From 7ef4b3095c9a0b308192898302e8415ba27d60e6 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 28 Mar 2025 11:15:09 +0530
Subject: [PATCH v12] Restrict publishing of partitioned table with foreign
partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when create a publication on a partitioned
table that has a foreign partition, the initial sync of such table is
successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
Root table is published and the initial data of foreign partitions
are replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except foreign partitions are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published. Initial data of foreign partitions are
replicated.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a foreign partititon. Error
is thrown when we try to create a publication on (or add to existing
publication) a partitioned table with foreign partition, when try to
create a foreign partition and when we try to attach foreign table (or
a table with foreign partition) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing foreign partition which are part of the publication.
This is done by avoid adding foreign partitions in pg_subscription_rel
catalog table.
We have introduced two functions 'check_partrel_has_foreign_table' and
'check_foreign_tables'. In 'check_partrel_has_foreign_table' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take a AccessShareLock on each partition table
to avoid creation of foreign partition concurrently.
In 'check_foreign_tables' if schema id is provided we check for each
partitioned table in that schema if it has a foreign partition, or if
schema id is not provided we check for each partitioned table in the
database if it has a foreign partition. While doing so we take a
ShareLock on pg_partitioned_table so no partition table is created
concurrently after this check.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_publication.sgml | 5 +
src/backend/catalog/pg_publication.c | 166 +++++++++++++++++++---
src/backend/commands/foreigncmds.c | 50 +++++++
src/backend/commands/publicationcmds.c | 47 ++++++
src/backend/commands/tablecmds.c | 59 ++++++++
src/include/catalog/pg_publication.h | 5 +
src/test/regress/expected/publication.out | 89 ++++++++++++
src/test/regress/sql/publication.sql | 81 +++++++++++
9 files changed, 494 insertions(+), 18 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f288c049a5c..ce60a1b391c 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,7 +2154,15 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is supported only for tables, including partitioned tables,
+ except when they contain foreign partitions. If
+ <literal>publish_via_partition_root</literal> is set to
+ <literal>true</literal> for a publication table with foreign partitions, or
+ if an attempt is made to replicate such a table, an error is thrown.
+ Additionally, when replicating a partitioned table where
+ <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal> and foreign partitions are present, all partitions
+ are replicated except the foreign partitions.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 73f0c8d89fb..47216fc4789 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -257,6 +257,11 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
If this is enabled, <literal>TRUNCATE</literal> operations performed
directly on partitions are not replicated.
</para>
+
+ <para>
+ If this is enabled, a foreign table or a partitioned table with a
+ foreign partition is not allowed in the publication.
+ </para>
</listitem>
</varlistentry>
</variablelist></para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..77299b75cde 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,12 +26,14 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "partitioning/partdesc.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -53,7 +55,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Relation targetrel, Publication *pub)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +66,18 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any foreign partition. See check_foreign_tables for details.
+ */
+ if (pub->pubviaroot && check_partrel_has_foreign_table(targetrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pub->name),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ RelationGetRelationName(targetrel))));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -304,7 +318,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +327,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +473,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(targetrel, pub);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +713,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ check_foreign_tables(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
@@ -1332,3 +1349,118 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
+
+/* Check if a partitioned table has a foreign partition */
+bool
+check_partrel_has_foreign_table(Relation rel)
+{
+ bool has_foreign_tbl = false;
+
+ if (RelationGetForm(rel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ Relation childrel = table_open(pd->oids[i], AccessShareLock);
+
+ if (RelationGetForm(childrel)->relkind == RELKIND_FOREIGN_TABLE)
+ has_foreign_tbl = true;
+ else
+ has_foreign_tbl = check_partrel_has_foreign_table(childrel);
+
+ table_close(childrel, NoLock);
+
+ if (has_foreign_tbl)
+ break;
+ }
+ }
+
+ return has_foreign_tbl;
+}
+
+/*
+ * Protect against including foreign tables that are partitions of partitioned
+ * tables published by the given publication when publish_via_root_partition is
+ * true. This will not work correctly as the initial data from the foreign
+ * table can be replicated by the tablesync worker even though replication of
+ * foreign table is not supported because when publish_via_root_partition is
+ * true, the root table is included in pg_subscription_rel catalog table and
+ * tablesync worker cannot distinguish data from foreign partition. So we
+ * disallow the case here and in all DDL commands that would end up creating
+ * such a case indirectly.
+ *
+ * When publish_via_root_partition is set to false, leaf partitions are included
+ * in pg_subscription_rel catalog table. So, when we include a partition table
+ * with foreign partition in a publication, we skip including foreign partitions
+ * to pg_subscription_rel catalog table. So, the foreign partitions are not
+ * replicated.
+ *
+ *
+ * check_foreign_tables
+ *
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_partitioned_table to restrict addition of new
+ * partitioned table which may contain a foreign partition while publication is
+ * being created.
+ */
+void
+check_foreign_tables(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ Relation partRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+ partRel = table_open(PartitionedRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Relation pubrel = table_open(relForm->oid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relForm->oid))));
+
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+ table_close(partRel, NoLock);
+}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index c14e038d54f..98c1c82db61 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -21,6 +21,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
+#include "catalog/partition.h"
#include "catalog/pg_foreign_data_wrapper.h"
#include "catalog/pg_foreign_server.h"
#include "catalog/pg_foreign_table.h"
@@ -1423,6 +1424,55 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
+ /*
+ * Check if it is a foreign partition and the partitioned table is not
+ * published or published with publish_via_partition_root option as false.
+ * See check_foreign_tables for details.
+ */
+ if (stmt->base.partbound != NULL)
+ {
+ RangeVar *root = linitial_node(RangeVar, stmt->base.inhRelations);
+ Relation rootrel = table_openrv(root, AccessShareLock);
+
+ if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ Oid schemaid = RelationGetNamespace(rootrel);
+ List *puboids = GetRelationPublications(rootrel->rd_id);
+ List *ancestors;
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rootrel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create table foreign partition \"%s\"",
+ get_rel_name(relid)),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ RelationGetRelationName(rootrel))));
+ }
+
+ list_free(puboids);
+ }
+
+ table_close(rootrel, AccessShareLock);
+ }
+
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..b06960e8e22 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -915,6 +915,10 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /* Check if any partitioned table has foreign partition */
+ if (publish_via_partition_root)
+ check_foreign_tables(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1084,49 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * already have any foreign partition. See check_foreign_tables for details.
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ List *schemaoids = NIL;
+ List *relids = NIL;
+
+ char *pubname = stmt->pubname;
+
+ if (pubform->puballtables)
+ check_foreign_tables(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+
+ foreach_oid(schemaoid, schemaoids)
+ check_foreign_tables(schemaoid, pubname);
+
+ list_free(schemaoids);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel = table_open(relid, AccessShareLock);
+
+ if (check_partrel_has_foreign_table(pubrel))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
+ "publish_via_partition_root", pubname),
+ errdetail("partition table \"%s\" in publication contains a foreign partition",
+ get_rel_name(relid))));
+ }
+
+ table_close(pubrel, NoLock);
+ }
+
+ list_free(relids);
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 11fcb51a165..5c4007dbeee 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -19923,6 +19923,10 @@ QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
* ALTER TABLE <name> ATTACH PARTITION <partition-name> FOR VALUES
*
* Return the address of the newly attached partition.
+ *
+ * If the would-be partition is a foreign table or a partitioned table with
+ * foreign partition, verify that the partitioned table is not in a publication
+ * with publish_via_partition_root=true. See check_foreign_tables for details.
*/
static ObjectAddress
ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
@@ -20067,6 +20071,61 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * Check if attachrel is a foreign table or a partitioned table with
+ * foreign partition and rel is not part of publication with option
+ * publish_via_partition_root as true.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ check_partrel_has_foreign_table(attachrel))
+ {
+ Oid schemaid = RelationGetNamespace(rel);
+ List *puboids = GetRelationPublications(rel->rd_id);
+ List *ancestors;
+ char *relname = get_rel_name(rel->rd_id);
+ char *attachrelname = get_rel_name(attachrel->rd_id);
+
+ puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
+ puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ ancestors = get_partition_ancestors(rel->rd_id);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat_unique_oid(puboids,
+ GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat_unique_oid(puboids,
+ GetSchemaPublications(schemaid));
+ }
+
+ list_free(ancestors);
+
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
+ attachrelname, relname),
+ errdetail("partition table \"%s\" is published with option publish_via_partition_root",
+ relname)));
+ }
+ }
+
+ list_free(puboids);
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..e2919da2541 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -191,4 +192,8 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
+extern bool check_partrel_has_foreign_table(Relation rel);
+
+extern void check_foreign_tables(Oid schemaid, char *pubname);
+
#endif /* PG_PUBLICATION_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..6a0c245bd60 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,95 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create table foreign partition "part3_1"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DROP PUBLICATION pub1;
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
+DETAIL: partition table "tmain" in publication contains a foreign partition
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..49c9d98b668 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,87 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a foreign partition
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create foreign partition of published table with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach foreign partition to published table with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Test with publish_via_partition_root = false
+-- Foreign partitions are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create foreign partition of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach foreign partition to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- foreign partition
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On 2025-Apr-01, Shlok Kyal wrote:
I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'I have attached the updated v11 patch.
Sadly I don't have time to describe the changes proposed here right now,
but I'll do that early tomorrow. (Some minor changes are still needed,
particularly the comments to publication_check_foreign_parts which are
mostly unchanged from what your patch has. I'll do another review round
tomorrow.)
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
Attachments:
0001-some-changes.patch.txttext/plain; charset=utf-8Download
From c6433548f3f47b23b81384f3cc80c0df307f6415 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Sun, 6 Apr 2025 21:27:39 +0200
Subject: [PATCH] some changes
---
doc/src/sgml/logical-replication.sgml | 18 +-
doc/src/sgml/ref/create_publication.sgml | 24 ++-
src/backend/catalog/pg_publication.c | 235 ++++++++++------------
src/backend/commands/foreigncmds.c | 49 -----
src/backend/commands/publicationcmds.c | 42 ++--
src/backend/commands/tablecmds.c | 109 +++++++---
src/backend/partitioning/partdesc.c | 32 +++
src/include/catalog/pg_publication.h | 6 +-
src/include/partitioning/partdesc.h | 1 +
src/test/regress/expected/publication.out | 30 +--
10 files changed, 287 insertions(+), 259 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index ce60a1b391c..67503aec871 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,18 +2154,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is supported only for tables, including partitioned tables,
- except when they contain foreign partitions. If
- <literal>publish_via_partition_root</literal> is set to
- <literal>true</literal> for a publication table with foreign partitions, or
- if an attempt is made to replicate such a table, an error is thrown.
- Additionally, when replicating a partitioned table where
- <literal>publish_via_partition_root</literal> is set to
- <literal>false</literal> and foreign partitions are present, all partitions
- are replicated except the foreign partitions.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>. In this mode, changes to foreign partitions are
+ ignored for the purposes of replication, and data contained in them
+ is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 47216fc4789..9f412e6693a 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -248,20 +248,26 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
that of the individual partitions.
</para>
+ <para>
+ If this parameter is enabled, <literal>TRUNCATE</literal>
+ operations performed directly on partitions are not replicated.
+ </para>
+
+ <para>
+ If this parameter is enabled, foreign tables and partitioned tables
+ containing partitions that are foreign tables may not be
+ added to the publication. Conversely, foreign tables may not be
+ attached to a partitioned table that is included in a publication
+ with this parameter enabled. Lastly, this parameter may not be
+ changed on publications that include partitioned tables with foreign
+ tables as partitions.
+ </para>
+
<para>
This parameter also affects how row filters and column lists are
chosen for partitions; see below for details.
</para>
- <para>
- If this is enabled, <literal>TRUNCATE</literal> operations performed
- directly on partitions are not replicated.
- </para>
-
- <para>
- If this is enabled, a foreign table or a partitioned table with a
- foreign partition is not allowed in the publication.
- </para>
</listitem>
</varlistentry>
</variablelist></para>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 77299b75cde..51e463c112b 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -55,7 +55,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel, Publication *pub)
+check_publication_add_relation(Publication *pub, Relation targetrel)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -68,15 +68,18 @@ check_publication_add_relation(Relation targetrel, Publication *pub)
/*
* publish_via_root_partition cannot be true if it is a partitioned table
- * and has any foreign partition. See check_foreign_tables for details.
+ * and has any foreign partition. See publication_check_foreign_parts for
+ * details.
*/
- if (pub->pubviaroot && check_partrel_has_foreign_table(targetrel))
+ if (pub->pubviaroot &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(targetrel))
ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
- "publish_via_partition_root", pub->name),
- errdetail("partition table \"%s\" in publication contains a foreign partition",
- RelationGetRelationName(targetrel))));
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add partitioned table \"%s\" to publication \"%s\", which has \"%s\" set to \"%s\"",
+ RelationGetRelationName(targetrel), pub->name,
+ "publish_via_partition_root", "true"),
+ errdetail("The table contains a partition that's a foreign table."));
/* Can't be system table */
if (IsCatalogRelation(targetrel))
@@ -125,6 +128,103 @@ check_publication_add_schema(Oid schemaid)
errdetail("Temporary schemas cannot be replicated.")));
}
+/*
+ * publication_check_foreign_parts
+ * Helper function to ensure we don't publish foreign tables
+ *
+ * Foreign tables may not be published.
+ *
+ * Protect against including foreign tables that are partitions of partitioned
+ * tables published by the given publication when publish_via_root_partition is
+ * true. This will not work correctly as the initial data from the foreign
+ * table can be replicated by the tablesync worker even though replication of
+ * foreign table is not supported because when publish_via_root_partition is
+ * true, the root table is included in pg_subscription_rel catalog table and
+ * tablesync worker cannot distinguish data from foreign partition. So we
+ * disallow the case here and in all DDL commands that would end up creating
+ * such a case indirectly.
+ *
+ * When publish_via_root_partition is set to false, leaf partitions are included
+ * in pg_subscription_rel catalog table. So, when we include a partition table
+ * with foreign partition in a publication, we skip including foreign partitions
+ * to pg_subscription_rel catalog table. So, the foreign partitions are not
+ * replicated.
+ *
+ *
+ *
+ * If valid schemaid provided, check if the schema has a partition table which
+ * has a foreign partition. The partition tables in a schema can have partitions
+ * in other schema. We also need to check if such partitions are foreign
+ * partition.
+ *
+ * If valid schemaid is not provided, we get all partition tables and check if
+ * it has any foreign partition. We take a lock on partition tables so no new
+ * foreign partitions are added concurrently.
+ *
+ * We take a ShareLock on pg_partitioned_table to restrict addition of new
+ * partitioned table which may contain a foreign partition while publication is
+ * being created.
+ */
+void
+publication_check_foreign_parts(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ Relation partRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+ partRel = table_open(PartitionedRelationId, ShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+ Relation pubrel = table_open(relForm->oid, AccessShareLock);
+
+ if (pubrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errtable(pubrel),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ get_rel_name(relForm->oid))));
+
+ /*
+ * Keep lock till end of transaction: must prevent this table from
+ * being attached a foreign table until we're done. XXX does this
+ * prevent addition of a partition in a partitioned child?
+ */
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+ table_close(partRel, NoLock);
+}
+
/*
* Returns if relation represented by oid and Form_pg_class entry
* is publishable.
@@ -473,7 +573,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel, pub);
+ check_publication_add_relation(pub, targetrel);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -718,7 +818,7 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
* partition
*/
if (pub->pubviaroot)
- check_foreign_tables(schemaid, pub->name);
+ publication_check_foreign_parts(schemaid, pub->name);
/* Form a tuple */
memset(values, 0, sizeof(values));
@@ -1349,118 +1449,3 @@ pg_get_publication_tables(PG_FUNCTION_ARGS)
SRF_RETURN_DONE(funcctx);
}
-
-/* Check if a partitioned table has a foreign partition */
-bool
-check_partrel_has_foreign_table(Relation rel)
-{
- bool has_foreign_tbl = false;
-
- if (RelationGetForm(rel)->relkind == RELKIND_PARTITIONED_TABLE)
- {
- PartitionDesc pd = RelationGetPartitionDesc(rel, true);
-
- for (int i = 0; i < pd->nparts; i++)
- {
- Relation childrel = table_open(pd->oids[i], AccessShareLock);
-
- if (RelationGetForm(childrel)->relkind == RELKIND_FOREIGN_TABLE)
- has_foreign_tbl = true;
- else
- has_foreign_tbl = check_partrel_has_foreign_table(childrel);
-
- table_close(childrel, NoLock);
-
- if (has_foreign_tbl)
- break;
- }
- }
-
- return has_foreign_tbl;
-}
-
-/*
- * Protect against including foreign tables that are partitions of partitioned
- * tables published by the given publication when publish_via_root_partition is
- * true. This will not work correctly as the initial data from the foreign
- * table can be replicated by the tablesync worker even though replication of
- * foreign table is not supported because when publish_via_root_partition is
- * true, the root table is included in pg_subscription_rel catalog table and
- * tablesync worker cannot distinguish data from foreign partition. So we
- * disallow the case here and in all DDL commands that would end up creating
- * such a case indirectly.
- *
- * When publish_via_root_partition is set to false, leaf partitions are included
- * in pg_subscription_rel catalog table. So, when we include a partition table
- * with foreign partition in a publication, we skip including foreign partitions
- * to pg_subscription_rel catalog table. So, the foreign partitions are not
- * replicated.
- *
- *
- * check_foreign_tables
- *
- * If valid schemaid provided, check if the schema has a partition table which
- * has a foreign partition. The partition tables in a schema can have partitions
- * in other schema. We also need to check if such partitions are foreign
- * partition.
- *
- * If valid schemaid is not provided, we get all partition tables and check if
- * it has any foreign partition. We take a lock on partition tables so no new
- * foreign partitions are added concurrently.
- *
- * We take a ShareLock on pg_partitioned_table to restrict addition of new
- * partitioned table which may contain a foreign partition while publication is
- * being created.
- */
-void
-check_foreign_tables(Oid schemaid, char *pubname)
-{
- Relation classRel;
- Relation partRel;
- ScanKeyData key[3];
- int keycount = 0;
- TableScanDesc scan;
- HeapTuple tuple;
-
- classRel = table_open(RelationRelationId, AccessShareLock);
- partRel = table_open(PartitionedRelationId, ShareLock);
-
- /* Get the root nodes of partitioned table */
- ScanKeyInit(&key[keycount++],
- Anum_pg_class_relkind,
- BTEqualStrategyNumber, F_CHAREQ,
- CharGetDatum(RELKIND_PARTITIONED_TABLE));
-
- ScanKeyInit(&key[keycount++],
- Anum_pg_class_relispartition,
- BTEqualStrategyNumber, F_BOOLEQ,
- BoolGetDatum(false));
-
- /* If schema id is provided check partitioned table in that schema */
- if (OidIsValid(schemaid))
- ScanKeyInit(&key[keycount++],
- Anum_pg_class_relnamespace,
- BTEqualStrategyNumber, F_OIDEQ,
- schemaid);
-
- scan = table_beginscan_catalog(classRel, keycount, key);
- while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
- {
- Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
- Relation pubrel = table_open(relForm->oid, AccessShareLock);
-
- if (check_partrel_has_foreign_table(pubrel))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
- "publish_via_partition_root", pubname),
- errdetail("partition table \"%s\" in publication contains a foreign partition",
- get_rel_name(relForm->oid))));
-
- table_close(pubrel, NoLock);
- }
-
- table_endscan(scan);
- table_close(classRel, AccessShareLock);
- table_close(partRel, NoLock);
-}
diff --git a/src/backend/commands/foreigncmds.c b/src/backend/commands/foreigncmds.c
index 98c1c82db61..c6843a9c30a 100644
--- a/src/backend/commands/foreigncmds.c
+++ b/src/backend/commands/foreigncmds.c
@@ -1424,55 +1424,6 @@ CreateForeignTable(CreateForeignTableStmt *stmt, Oid relid)
ftrel = table_open(ForeignTableRelationId, RowExclusiveLock);
- /*
- * Check if it is a foreign partition and the partitioned table is not
- * published or published with publish_via_partition_root option as false.
- * See check_foreign_tables for details.
- */
- if (stmt->base.partbound != NULL)
- {
- RangeVar *root = linitial_node(RangeVar, stmt->base.inhRelations);
- Relation rootrel = table_openrv(root, AccessShareLock);
-
- if (RelationGetForm(rootrel)->relkind == RELKIND_PARTITIONED_TABLE)
- {
- Oid schemaid = RelationGetNamespace(rootrel);
- List *puboids = GetRelationPublications(rootrel->rd_id);
- List *ancestors;
-
- puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
- puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
- ancestors = get_partition_ancestors(rootrel->rd_id);
-
- foreach_oid(ancestor, ancestors)
- {
- puboids = list_concat_unique_oid(puboids,
- GetRelationPublications(ancestor));
- schemaid = get_rel_namespace(ancestor);
- puboids = list_concat_unique_oid(puboids,
- GetSchemaPublications(schemaid));
- }
- list_free(ancestors);
-
- foreach_oid(puboid, puboids)
- {
- Publication *pub = GetPublication(puboid);
-
- if (pub->pubviaroot)
- ereport(ERROR,
- (errcode(ERRCODE_WRONG_OBJECT_TYPE),
- errmsg("cannot create table foreign partition \"%s\"",
- get_rel_name(relid)),
- errdetail("partition table \"%s\" is published with option publish_via_partition_root",
- RelationGetRelationName(rootrel))));
- }
-
- list_free(puboids);
- }
-
- table_close(rootrel, AccessShareLock);
- }
-
/*
* For now the owner cannot be specified on create. Use effective user ID.
*/
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index b06960e8e22..54f2b505e3b 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -35,6 +35,7 @@
#include "commands/publicationcmds.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "partitioning/partdesc.h"
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
#include "parser/parse_relation.h"
@@ -917,7 +918,7 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
{
/* Check if any partitioned table has foreign partition */
if (publish_via_partition_root)
- check_foreign_tables(InvalidOid, stmt->pubname);
+ publication_check_foreign_parts(InvalidOid, stmt->pubname);
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
@@ -1086,45 +1087,42 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
/*
* If publish_via_partition_root is set to true, check if the publication
- * already have any foreign partition. See check_foreign_tables for details.
+ * has any foreign partition. See publication_check_foreign_parts for
+ * details.
*/
if (publish_via_partition_root_given && publish_via_partition_root)
{
- List *schemaoids = NIL;
- List *relids = NIL;
-
char *pubname = stmt->pubname;
+ List *schemaoids;
+ List *relids;
if (pubform->puballtables)
- check_foreign_tables(InvalidOid, pubname);
+ publication_check_foreign_parts(InvalidOid, pubname);
schemaoids = GetPublicationSchemas(pubform->oid);
-
foreach_oid(schemaoid, schemaoids)
- check_foreign_tables(schemaoid, pubname);
-
- list_free(schemaoids);
+ publication_check_foreign_parts(schemaoid, pubname);
relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
-
foreach_oid(relid, relids)
{
- Relation pubrel = table_open(relid, AccessShareLock);
+ Relation pubrel;
- if (check_partrel_has_foreign_table(pubrel))
- {
+ if (get_rel_relkind(relid) != RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ pubrel = table_open(relid, AccessShareLock);
+
+ if (RelationHasForeignPartition(pubrel))
ereport(ERROR,
- (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
- errmsg("cannot set parameter \"%s\" to true for publication \"%s\"",
- "publish_via_partition_root", pubname),
- errdetail("partition table \"%s\" in publication contains a foreign partition",
- get_rel_name(relid))));
- }
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ RelationGetRelationName(pubrel)));
table_close(pubrel, NoLock);
}
-
- list_free(relids);
}
/* Everything ok, form a new tuple. */
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index eed4487e2d9..b594b01e774 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1129,6 +1129,53 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled. For details, see
+ * publication_check_foreign_parts.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = GetRelationPublications(parent->rd_id);
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20093,58 +20140,68 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
errmsg("cannot attach temporary relation of another session as partition")));
/*
- * Check if attachrel is a foreign table or a partitioned table with
- * foreign partition and rel is not part of publication with option
- * publish_via_partition_root as true.
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the
+ * parent table is not in a publication with publish_via_partition_root
+ * enabled. See publication_check_foreign_parts.
*/
if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
- check_partrel_has_foreign_table(attachrel))
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
{
- Oid schemaid = RelationGetNamespace(rel);
- List *puboids = GetRelationPublications(rel->rd_id);
+ Oid schemaid;
+ List *puboids;
List *ancestors;
- char *relname = get_rel_name(rel->rd_id);
- char *attachrelname = get_rel_name(attachrel->rd_id);
- puboids = list_concat_unique_oid(puboids, GetAllTablesPublications());
- puboids = list_concat_unique_oid(puboids, GetSchemaPublications(schemaid));
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
ancestors = get_partition_ancestors(rel->rd_id);
-
foreach_oid(ancestor, ancestors)
{
- puboids = list_concat_unique_oid(puboids,
- GetRelationPublications(ancestor));
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
schemaid = get_rel_namespace(ancestor);
- puboids = list_concat_unique_oid(puboids,
- GetSchemaPublications(schemaid));
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
}
- list_free(ancestors);
-
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
foreach_oid(puboid, puboids)
{
- Publication *pub = GetPublication(puboid);
+ Publication *pub;
+ pub = GetPublication(puboid);
if (pub->pubviaroot)
{
if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
- attachrelname, relname),
- errdetail("partition table \"%s\" is published with option publish_via_partition_root",
- relname)));
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
else
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach table \"%s\" with foreign partition to partition table \"%s\"",
- attachrelname, relname),
- errdetail("partition table \"%s\" is published with option publish_via_partition_root",
- relname)));
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
}
}
-
- list_free(puboids);
}
/*
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..b53139bafdd 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], AccessShareLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index e2919da2541..71ad5a6f846 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -179,6 +179,8 @@ extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
extern bool is_publishable_relation(Relation rel);
extern bool is_schema_publication(Oid pubid);
+extern void publication_check_foreign_parts(Oid schemaid, char *pubname);
+
extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
MemoryContext mcxt, Bitmapset **cols);
extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
@@ -192,8 +194,4 @@ extern Bitmapset *pub_collist_to_bitmapset(Bitmapset *columns, Datum pubcols,
extern Bitmapset *pub_form_cols_map(Relation relation,
PublishGencolsType include_gencols_type);
-extern bool check_partrel_has_foreign_table(Relation rel);
-
-extern void check_foreign_tables(Oid schemaid, char *pubname);
-
#endif /* PG_PUBLICATION_H */
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 6a0c245bd60..4395563fcd5 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1939,32 +1939,32 @@ ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
-- Can't create publications with publish_via_partition_root = true, if table
-- has a foreign partition
CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
-ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
-DETAIL: partition table "tmain" in publication contains a foreign partition
+ERROR: cannot add partitioned table "tmain" to publication "pub1", which has "publish_via_partition_root" set to "true"
+DETAIL: The table contains a partition that's a foreign table.
CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
-ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
-DETAIL: partition table "tmain" in publication contains a foreign partition
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
-ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
-DETAIL: partition table "tmain" in publication contains a foreign partition
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
-- Test when a partitioned table with foreign table as a partition is attached
-- to partitioned table which is already published
ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
ERROR: cannot attach table "part2" with foreign partition to partition table "tmain"
-DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
-- Can't create foreign partition of published table with
-- publish_via_partition_root = true
CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
-ERROR: cannot create table foreign partition "part3_1"
-DETAIL: partition table "tmain" is published with option publish_via_partition_root
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
-- Can't attach foreign partition to published table with
-- publish_via_partition_root = true
CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
-DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
CREATE SCHEMA sch4;
CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
-- publication created with FOR TABLES IN SCHEMA
@@ -1973,13 +1973,13 @@ CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_ro
-- Can't create foreign partition of published table with
-- publish_via_partition_root = true
CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
-ERROR: cannot create table foreign partition "part3_1"
-DETAIL: partition table "tmain" is published with option publish_via_partition_root
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
-- Can't attach foreign partition to published table with
-- publish_via_partition_root = true
ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
-DETAIL: partition table "tmain" is published with option publish_via_partition_root
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
DROP PUBLICATION pub1;
-- Test with publish_via_partition_root = false
-- Foreign partitions are skipped by default
@@ -2004,8 +2004,8 @@ SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3'
-- Can't alter publish_via_partition_root to true, if publication already have
-- foreign partition
ALTER PUBLICATION pub1 SET (publish_via_partition_root);
-ERROR: cannot set parameter "publish_via_partition_root" to true for publication "pub1"
-DETAIL: partition table "tmain" in publication contains a foreign partition
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP PUBLICATION pub3;
--
2.39.5
07.04.2025 03:27, Álvaro Herrera пишет:
On 2025-Apr-01, Shlok Kyal wrote:
I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'I have attached the updated v11 patch.
Sadly I don't have time to describe the changes proposed here right now,
but I'll do that early tomorrow. (Some minor changes are still needed,
particularly the comments to publication_check_foreign_parts which are
mostly unchanged from what your patch has. I'll do another review round
tomorrow.)
Hello!
I looked at the latest patch again and found one more place for
list_free(). Also look at additional test case:
diff --git a/src/backend/catalog/pg_publication.c
b/src/backend/catalog/pg_publication.c index 51e463c112b..7fcc191feb9
100644 --- a/src/backend/catalog/pg_publication.c +++
b/src/backend/catalog/pg_publication.c @@ -442,6 +442,7 @@
GetPubPartitionOptionRelations(List *result, PublicationPartOpt
pub_partopt, result = lappend_oid(result,
partOid); } + list_free(all_parts);
} else result = lappend_oid(result,
relid); diff --git a/src/test/regress/sql/publication.sql
b/src/test/regress/sql/publication.sql index 49c9d98b668..e56aebc397a
100644 --- a/src/test/regress/sql/publication.sql +++
b/src/test/regress/sql/publication.sql @@ -1296,6 +1296,14 @@ SELECT
pubname, tablename FROM pg_publication_tables WHERE schemaname in
('sch3' -- foreign partition ALTER PUBLICATION pub1 SET
(publish_via_partition_root); +CREATE SCHEMA sch5; +CREATE SCHEMA sch6;
+CREATE TABLE sch6.tmain(id int) PARTITION BY RANGE(id); +CREATE TABLE
sch5.part1 PARTITION OF sch6.tmain FOR VALUES FROM (0) TO (10) PARTITION
BY RANGE(id); +CREATE FOREIGN TABLE sch6.part2 PARTITION OF sch5.part1
FOR VALUES FROM (0) TO (5) SERVER fdw_server; +CREATE PUBLICATION pub4
FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root); +SELECT
pubname, tablename FROM pg_publication_tables WHERE pubname = 'pub4'
ORDER BY pubname, tablename; + DROP PUBLICATION pub1; DROP PUBLICATION
pub2; DROP PUBLICATION pub3;
I think this is a wrong assumption:
ScanKeyInit(&key[keycount++], Anum_pg_class_relispartition,
BTEqualStrategyNumber, F_BOOLEQ, BoolGetDatum(false));
In this case sch5.part1 is partitioned table, but it also partition of
table in different schema
--
With best regards,
Sergey Tatarintsev,
PostgresPro
As promised, here's a rundown of the changes I did, mostly in order the
patch shows them:
- I reworded the documentation changes to read more coherent with the
surrounding text.
- It seemed wrong to have check_publication_add_relation() have the
relation first as argument and publication later, so I turned them
around.
- I rewrote almost all of the error messages. The errdetails failed
style (no initial capitals and no end period); one of the errmsg() was
completely wrong (it said "cannot set parameter" instead of "cannot add
table to publication"). I also moved the parameter name out of the
translatable part of the message.
- I removed the novel use of the term "foreign partition". We don't
seem to use that anywhere that's user-visible, only in code comments.
Instead I used "a partition that's a foreign table". I just did a new
grep and I think I missed a couple, which I'm going to fix momentarily.
- I renamed check_foreign_tables to publication_check_foreign_parts and
moved to a different place in the file. The original name wasn't very
good for an extern function.
- I renamed check_partrel_has_foreign_table to RelationHasForeignPartition
and moved it to partdesc.c. There's nothing that ties this function to
pg_publication.c, so in my mind it made no sense for it to be there.
I'm not 100% certain that partdesc.c is the best place for it, but if
there's another, then I didn't see it. Also, I made it Assert() that
it's not given a relation other than a partitioned table, which forces
all callers to check the relkind before calling it. This is good
because we don't need to open tables before ensuring the relkind is one
that interests us. Also, I made it use the PartitionDesc->is_leaf flag
array to determine whether to recurse or not; and use get_rel_relkind to
get the relkind for leaf partitions, which is much cheaper than doing
table_open. The original coding was kind of wasteful for no reason.
- The check to prohibit CREATE FOREIGN TABLE as a partition in
CreateForeignTable, was entirely in the wrong place. This is proven by
the fact that the original coding had to do table_openrv() in order to
know what the parent table was. This is terrible coding, because it
means we resolve the table name to OID twice. (This is probably not so
terrible because the first location that does it already acquires a good
enough lock that it'd stay put; still, it's a bad coding pattern). I
moved it to DefineRelation, in the block were we learn that we're
creating a partition; it's easy to check there that the new relation is
a foreign table. We don't even need to open the parent table at all,
since it's already open.
- In ATExecAttachPartition (and in the check that was in
CreateForeignTable), there were a bunch of list_concat_unique_oids().
It's unlikely that this would bother anyone unless there are large
numbers of publications, but the performance characteristics of doing
this uniquification were unclear, and list_concat_unique() comments warn
against it anyway. It seemed safer to just do list_concat() and a
single sort/uniq step at the end, just before walking the list.
- There were a bunch of list_frees() in there. I removed them all.
They are pointless, unnecessary, and give a wrong sense of cleanliness.
In reality, you will be leaking the second argument of each
list_concat() anyway; to make this watertight, you'd have to read each
of those lists into a variable, concat, then free the variable. The
code would become much noisier coding. However, DDL code is normally
leaky and that causes no major problems, because the memory context in
use is going to be released or deleted soon afterwards. Freeing a few
bytes ahead of time would only require more memory context bookkeeping
to be executed, to very little gain.
I think that's all I had ...
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
On 2025-Apr-07, Sergey Tatarintsev wrote:
I think this is a wrong assumption:
ScanKeyInit(&key[keycount++], Anum_pg_class_relispartition,
BTEqualStrategyNumber, F_BOOLEQ, BoolGetDatum(false));In this case sch5.part1 is partitioned table, but it also partition of table
in different schema
Yeah, this looks ill-considered.
After looking at this code some more, I'm withdrawing myself from here.
(I don't mean just this patch in particular, I mean the logical
replication code as a whole.)
Thanks,
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"La persona que no quería pecar / estaba obligada a sentarse
en duras y empinadas sillas / desprovistas, por cierto
de blandos atenuantes" (Patricio Vogel)
Here's the additional changes I made here before giving up on this.
I think it needs some additional rethinking, not going to happen for 18.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"The Gord often wonders why people threaten never to come back after they've
been told never to return" (www.actsofgord.com)
Attachments:
0001-some-more-changes.patch.txttext/x-diff; charset=utf-8Download
From cc7b4021d5661aa8dbb8bd3a8c8569824dbbbc36 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@alvh.no-ip.org>
Date: Mon, 7 Apr 2025 14:31:44 +0200
Subject: [PATCH] some more changes
---
src/backend/catalog/pg_publication.c | 63 ++++++++++++-----------
src/test/regress/expected/publication.out | 16 ++++++
src/test/regress/sql/publication.sql | 10 ++++
3 files changed, 59 insertions(+), 30 deletions(-)
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index 51e463c112b..ec76c0c8eb2 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -34,6 +34,7 @@
#include "commands/publicationcmds.h"
#include "funcapi.h"
#include "partitioning/partdesc.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -130,53 +131,56 @@ check_publication_add_schema(Oid schemaid)
/*
* publication_check_foreign_parts
- * Helper function to ensure we don't publish foreign tables
+ * Helper function to ensure we don't indirectly publish foreign tables
*
- * Foreign tables may not be published.
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables. To
+ * avoid the inconsistencies that would result, we disallow foreign
+ * tables from being published generally. However, it's possible for
+ * partitioned tables to have foreign tables as partitions, and we would
+ * like to allow publishing those partitioned tables so that the other
+ * partitions are replicated.
*
- * Protect against including foreign tables that are partitions of partitioned
- * tables published by the given publication when publish_via_root_partition is
- * true. This will not work correctly as the initial data from the foreign
- * table can be replicated by the tablesync worker even though replication of
- * foreign table is not supported because when publish_via_root_partition is
- * true, the root table is included in pg_subscription_rel catalog table and
- * tablesync worker cannot distinguish data from foreign partition. So we
- * disallow the case here and in all DDL commands that would end up creating
- * such a case indirectly.
+ * This function is in charge of detecting if a partitioned table has a
+ * foreign table as a partition -- either in the whole database (useful
+ * for FOR ALL TABLES publications) or in a particular schema (useful
+ * for FOR TABLES IN SCHEMA publications). This function must be called
+ * only for publications with publish_via_partition_root=true.
*
- * When publish_via_root_partition is set to false, leaf partitions are included
- * in pg_subscription_rel catalog table. So, when we include a partition table
- * with foreign partition in a publication, we skip including foreign partitions
- * to pg_subscription_rel catalog table. So, the foreign partitions are not
- * replicated.
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_replication_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
*
+ * If a valid schemaid is provided, check if that schema has any
+ * partitioned table with a foreign table as partition.
*
+ * If no valid schemaid is provided, check all partitioned tables.
*
- * If valid schemaid provided, check if the schema has a partition table which
- * has a foreign partition. The partition tables in a schema can have partitions
- * in other schema. We also need to check if such partitions are foreign
- * partition.
+ * We take a lock on partition tables so no new foreign partitions are
+ * added concurrently.
*
- * If valid schemaid is not provided, we get all partition tables and check if
- * it has any foreign partition. We take a lock on partition tables so no new
- * foreign partitions are added concurrently.
- *
- * We take a ShareLock on pg_partitioned_table to restrict addition of new
- * partitioned table which may contain a foreign partition while publication is
- * being created.
+ * We also take a ShareLock on pg_partitioned_table to restrict addition
+ * of new partitioned table which may contain a foreign partition while
+ * publication is being created. XXX this is quite weird actually.
*/
void
publication_check_foreign_parts(Oid schemaid, char *pubname)
{
Relation classRel;
- Relation partRel;
ScanKeyData key[3];
int keycount = 0;
TableScanDesc scan;
HeapTuple tuple;
+ /*
+ * Take lock on pg_partitioned_rel. This prevents new publications
+ * from being created.
+ */
+ LockRelationOid(PartitionedRelationId, ShareLock);
+
classRel = table_open(RelationRelationId, AccessShareLock);
- partRel = table_open(PartitionedRelationId, ShareLock);
/* Get the root nodes of partitioned table */
ScanKeyInit(&key[keycount++],
@@ -222,7 +226,6 @@ publication_check_foreign_parts(Oid schemaid, char *pubname)
table_endscan(scan);
table_close(classRel, AccessShareLock);
- table_close(partRel, NoLock);
}
/*
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4395563fcd5..678f8b52e5b 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -2006,6 +2006,22 @@ SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3'
ALTER PUBLICATION pub1 SET (publish_via_partition_root);
ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+CREATE SCHEMA sch5;
+CREATE SCHEMA sch6;
+CREATE TABLE sch6.tmain(id int) PARTITION BY RANGE (id);
+CREATE TABLE sch5.part1 PARTITION OF sch6.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch6.part2 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub4 FOR TABLES IN SCHEMA sch5
+ WITH (publish_via_partition_root); -- should fail
+ERROR: cannot add table ...
+SELECT pubname, tablename FROM pg_publication_tables WHERE pubname = 'pub4' ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub4 | part1
+(1 row)
+
+DROP SCHEMA sch5, sch6 CASCADE;
+DROP PUBLICATION pub4;
DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP PUBLICATION pub3;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 49c9d98b668..43755b973cb 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1296,6 +1296,16 @@ SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3'
-- foreign partition
ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+CREATE SCHEMA sch5;
+CREATE SCHEMA sch6;
+CREATE TABLE sch6.tmain(id int) PARTITION BY RANGE (id);
+CREATE TABLE sch5.part1 PARTITION OF sch6.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch6.part2 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub4 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root);
+SELECT pubname, tablename FROM pg_publication_tables WHERE pubname = 'pub4' ORDER BY pubname, tablename;
+DROP SCHEMA sch5, sch6 CASCADE;
+DROP PUBLICATION pub4;
+
DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP PUBLICATION pub3;
--
2.39.5
On Mon, 7 Apr 2025 at 18:09, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
Here's the additional changes I made here before giving up on this.
I think it needs some additional rethinking, not going to happen for 18.
Hi Alvaro,
Thanks for reviewing the patch.
The changes shared by you in [1]/messages/by-id/202504062027.tqmabk2h353o@alvherre.pgsql, look good to me and I have added it
to the latest patch.
I have also included the changes shared by you in [2]/messages/by-id/202504071239.kuj6m5a5wqxg@alvherre.pgsql.
I have also addressed the issue reported by Sergey in [3]/messages/by-id/c64352fa-9a30-4e0a-853a-a6b5b6d07f4e@postgrespro.ru.
Additionally, I made some changes to the comments, commit message and
documentation to reflect the changes in the function name and to
reduce the usage of 'foreign partition' term.
I saw some comments by you in the patches:
1.
+ /*
+ * Keep lock till end of transaction: must prevent this table from
+ * being attached a foreign table until we're done. XXX does this
+ * prevent addition of a partition in a partitioned child?
+ */
This is same as the issue reported by Sergey in [3]/messages/by-id/c64352fa-9a30-4e0a-853a-a6b5b6d07f4e@postgrespro.ru. I have addressed
the issue in the latest patch. So, I have modified this comment.
2.
+ * We also take a ShareLock on pg_partitioned_table to restrict addition
+ * of new partitioned table which may contain a foreign partition while
+ * publication is being created. XXX this is quite weird actually.
This change was added to resolve the concurrency issue shared by
Vignesh in [4]/messages/by-id/CALDaNm2+eL22Sbvj74uS37xvt=haQWcOwP15QnDuVeYsjHiffw@mail.gmail.com. I tried with different locks and found that lock with
severity >= ShareLock was needed to avoid the concurrency issue.
Initially I added ShareLock to pg_class, but to reduce the scope I
added it to pg_partitioned_table instead. I cannot think of an
alternate approach. Do you have any suggestions for this?
[1]: /messages/by-id/202504062027.tqmabk2h353o@alvherre.pgsql
[2]: /messages/by-id/202504071239.kuj6m5a5wqxg@alvherre.pgsql
[3]: /messages/by-id/c64352fa-9a30-4e0a-853a-a6b5b6d07f4e@postgrespro.ru
[4]: /messages/by-id/CALDaNm2+eL22Sbvj74uS37xvt=haQWcOwP15QnDuVeYsjHiffw@mail.gmail.com
Thanks and Regards,
Shlok Kyal
Attachments:
v13-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v13-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From 534eee027b02e5cda9b502f9f624a5fb426d7f84 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 28 Mar 2025 11:15:09 +0530
Subject: [PATCH v13] Restrict publishing of partitioned table with foreign
table as its partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when we create a publication on a partitioned
table that has a foreign table as its partition, the initial sync of
such table is successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
The root table is published, and initial data from partitions that are
foreign tables is replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except partitions that are foreign tables are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published, including initial data from partitions
that are foreign tables.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a partition that is a foreign
table. Error is thrown when we try to create a publication on (or add to
existing publication) a partitioned table that has foreign tables as its
partitions, when try to create a partition that is a foreign table and
when we try to attach foreign table (or a partitioned table with
foreign tables as partitions) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing partitions that are foreign tables. This is done by
avoid adding such partitions in pg_subscription_rel catalog table.
We have introduced two functions 'RelationHasForeignPartition' and
'publication_check_foreign_parts'. In 'RelationHasForeignPartition' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take an AccessShareLock on each partition table
to prevent concurrent creation of a foreign table as a partition. In
'publication_check_foreign_parts' if schema id is provided we check for
each partitioned table in that schema if it has a foreign table as its
partition, or if schema id is not provided we check for each
partitioned table in the database if it has a partition that's a foreign
table. While doing so we take a ShareLock on pg_partitioned_table so no
partition table is created concurrently after this check.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_publication.sgml | 18 +-
src/backend/catalog/pg_publication.c | 221 ++++++++++++++++++++--
src/backend/commands/publicationcmds.c | 47 +++++
src/backend/commands/tablecmds.c | 112 +++++++++++
src/backend/partitioning/partdesc.c | 32 ++++
src/include/catalog/pg_publication.h | 3 +
src/include/partitioning/partdesc.h | 1 +
src/test/regress/expected/publication.out | 99 ++++++++++
src/test/regress/sql/publication.sql | 90 +++++++++
10 files changed, 611 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f288c049a5c..57236a452e9 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,10 +2154,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>. In this mode, changes to a partition that is a
+ foreign table are ignored for the purposes of replication, and data
+ contained in them is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 73f0c8d89fb..ad2c3b69d6b 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -249,13 +249,23 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
</para>
<para>
- This parameter also affects how row filters and column lists are
- chosen for partitions; see below for details.
+ If this parameter is enabled, <literal>TRUNCATE</literal>
+ operations performed directly on partitions are not replicated.
</para>
<para>
- If this is enabled, <literal>TRUNCATE</literal> operations performed
- directly on partitions are not replicated.
+ If this parameter is enabled, foreign tables and partitioned tables
+ containing partitions that are foreign tables may not be
+ added to the publication. Conversely, foreign tables may not be
+ attached to a partitioned table that is included in a publication
+ with this parameter enabled. Lastly, this parameter may not be
+ changed on publications that include partitioned tables with foreign
+ tables as partitions.
+ </para>
+
+ <para>
+ This parameter also affects how row filters and column lists are
+ chosen for partitions; see below for details.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..73a9bf22470 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,12 +26,15 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "partitioning/partdesc.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -53,7 +56,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Publication *pub, Relation targetrel)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +67,21 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_root_partition cannot be true if it is a partitioned table
+ * and has any partition that's a foreign table. See
+ * publication_check_foreign_parts for details.
+ */
+ if (pub->pubviaroot &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(targetrel))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add partitioned table \"%s\" to publication \"%s\", which has \"%s\" set to \"%s\"",
+ RelationGetRelationName(targetrel), pub->name,
+ "publish_via_partition_root", "true"),
+ errdetail("The table contains a partition that's a foreign table."));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -111,6 +129,172 @@ check_publication_add_schema(Oid schemaid)
errdetail("Temporary schemas cannot be replicated.")));
}
+/*
+ * Returns true if the ancestor is in the list of relations
+ * Otherwise, returns false.
+ */
+static bool
+is_ancestor_member_relids(Oid ancestor, List *relids)
+{
+ foreach_oid(relid, relids)
+ {
+ if (relid == ancestor)
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Filter out the partitions whose parent tables are also present in the list
+ * of relations.
+ */
+static void
+filter_partition_rels(List *relids)
+{
+ foreach_oid(relid, relids)
+ {
+ bool skip = false;
+ List *ancestors = NIL;
+ ListCell *lc2;
+
+ if (get_rel_relispartition(relid))
+ ancestors = get_partition_ancestors(relid);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ if (is_ancestor_member_relids(ancestor, relids))
+ {
+ skip = true;
+ break;
+ }
+ }
+
+ if (skip)
+ relids = list_delete_oid(relids, relid);
+ }
+}
+
+/*
+ * publication_check_foreign_parts
+ * Helper function to ensure we don't indirectly publish foreign tables
+ *
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables. To
+ * avoid the inconsistencies that would result, we disallow foreign
+ * tables from being published generally. However, it's possible for
+ * partitioned tables to have foreign tables as partitions, and we would
+ * like to allow publishing those partitioned tables so that the other
+ * partitions are replicated.
+ *
+ * This function is in charge of detecting if a partitioned table has a
+ * foreign table as a partition -- either in the whole database (useful
+ * for FOR ALL TABLES publications) or in a particular schema (useful
+ * for FOR TABLES IN SCHEMA publications). This function must be called
+ * only for publications with publish_via_partition_root=true.
+ *
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
+ *
+ * If a valid schemaid is provided, check if that schema has any
+ * partitioned table with a foreign table as partition.
+ *
+ * If no valid schemaid is provided, check all partitioned tables.
+ *
+ * We take a lock on partition tables so no new foreign table are added
+ * concurrently as a partition.
+ *
+ * We also take a ShareLock on pg_partitioned_table to restrict addition
+ * of new partitioned table which may include a foreign table as partition
+ * while publication is being created. XXX this is quite weird actually.
+ */
+void
+publication_check_foreign_parts(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+ List *relids = NIL;
+
+ /*
+ * Take lock on pg_partitioned_rel. This prevents new publications from
+ * being created.
+ */
+ LockRelationOid(PartitionedRelationId, ShareLock);
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ /*
+ * If schema id is not provided, take relations for which relispartition
+ * is false. This will give only the root partitioned tables. We donot
+ * include this for the case when schema id is specified because there can
+ * be cases when root partitioned tables are not part of schema and one of
+ * the child partition can still have a foreign table as its partition.
+ */
+ else
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ relids = lappend_oid(relids, relForm->oid);
+ }
+
+ /*
+ * If schema id is provided filter partitions list to have only topmost
+ * partitioned tables in that schema, to avoid repeated check.
+ */
+ if (OidIsValid(schemaid))
+ filter_partition_rels(relids);
+
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel = table_open(relid, AccessShareLock);
+
+ if (pubrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errtable(pubrel),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ get_rel_name(relid))));
+
+ /*
+ * Keep lock till end of transaction: must prevent this table from
+ * being attached a foreign table until we're done.
+ */
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
/*
* Returns if relation represented by oid and Form_pg_class entry
* is publishable.
@@ -304,7 +488,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +497,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +643,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(pub, targetrel);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +883,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ publication_check_foreign_parts(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..a2411fbebda 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -35,6 +35,7 @@
#include "commands/publicationcmds.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "partitioning/partdesc.h"
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
#include "parser/parse_relation.h"
@@ -915,6 +916,12 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /*
+ * Check if any partitioned table has a foreign table as its partition
+ */
+ if (publish_via_partition_root)
+ publication_check_foreign_parts(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1087,46 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * has any partition that's a foreign table. See
+ * publication_check_foreign_parts for details.
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ char *pubname = stmt->pubname;
+ List *schemaoids;
+ List *relids;
+
+ if (pubform->puballtables)
+ publication_check_foreign_parts(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+ foreach_oid(schemaoid, schemaoids)
+ publication_check_foreign_parts(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel;
+
+ if (get_rel_relkind(relid) != RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ pubrel = table_open(relid, AccessShareLock);
+
+ if (RelationHasForeignPartition(pubrel))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ RelationGetRelationName(pubrel)));
+
+ table_close(pubrel, NoLock);
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2705cf11330..3587e9960d8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1133,6 +1133,53 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled. For details, see
+ * publication_check_foreign_parts.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = GetRelationPublications(parent->rd_id);
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20304,6 +20351,71 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the parent
+ * table is not in a publication with publish_via_partition_root enabled.
+ * See publication_check_foreign_parts.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub;
+
+ pub = GetPublication(puboid);
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with a partition that's a foreign table to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..b53139bafdd 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], AccessShareLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..71ad5a6f846 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -178,6 +179,8 @@ extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
extern bool is_publishable_relation(Relation rel);
extern bool is_schema_publication(Oid pubid);
+extern void publication_check_foreign_parts(Oid schemaid, char *pubname);
+
extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
MemoryContext mcxt, Bitmapset **cols);
extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..f57addab23d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,105 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a partition that's a foreign table
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot add partitioned table "tmain" to publication "pub1", which has "publish_via_partition_root" set to "true"
+DETAIL: The table contains a partition that's a foreign table.
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with a partition that's a foreign table to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+DROP PUBLICATION pub1;
+-- Can't create publication with publish_via_partition_root = true on
+-- partitioned table(which is not root) with a partition that's a foreign table
+-- on other schema
+CREATE SCHEMA sch5;
+CREATE TABLE sch5.part1 PARTITION OF sch4.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch4.part1_1 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "part1" contains a partition that is a foreign table.
+-- Test with publish_via_partition_root = false
+-- Partition that are foreign tables are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create a partition that's a foreign table of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach partition that's a foreign table to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- a partition that's a foreign table
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SCHEMA sch5 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..55c0dff3601 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,96 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a partition that's a foreign table
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Can't create publication with publish_via_partition_root = true on
+-- partitioned table(which is not root) with a partition that's a foreign table
+-- on other schema
+CREATE SCHEMA sch5;
+CREATE TABLE sch5.part1 PARTITION OF sch4.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch4.part1_1 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root);
+
+-- Test with publish_via_partition_root = false
+-- Partition that are foreign tables are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create a partition that's a foreign table of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach partition that's a foreign table to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- a partition that's a foreign table
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SCHEMA sch5 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
On Mon, 7 Apr 2025 at 09:43, Sergey Tatarintsev
<s.tatarintsev@postgrespro.ru> wrote:
07.04.2025 03:27, Álvaro Herrera пишет:
On 2025-Apr-01, Shlok Kyal wrote:
I have modified the comment in create_publication.sgml and also added
comment in the restrictions section of logical-replication.sgml.
I have also added a more detailed explanation in comment of
'check_foreign_tables'I have attached the updated v11 patch.
Sadly I don't have time to describe the changes proposed here right now,
but I'll do that early tomorrow. (Some minor changes are still needed,
particularly the comments to publication_check_foreign_parts which are
mostly unchanged from what your patch has. I'll do another review round
tomorrow.)Hello!
I looked at the latest patch again and found one more place for list_free(). Also look at additional test case:
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 51e463c112b..7fcc191feb9 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -442,6 +442,7 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt, result = lappend_oid(result, partOid); } + list_free(all_parts); } else result = lappend_oid(result, relid); diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index 49c9d98b668..e56aebc397a 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -1296,6 +1296,14 @@ SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3' -- foreign partition ALTER PUBLICATION pub1 SET (publish_via_partition_root); +CREATE SCHEMA sch5; +CREATE SCHEMA sch6; +CREATE TABLE sch6.tmain(id int) PARTITION BY RANGE(id); +CREATE TABLE sch5.part1 PARTITION OF sch6.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id); +CREATE FOREIGN TABLE sch6.part2 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server; +CREATE PUBLICATION pub4 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root); +SELECT pubname, tablename FROM pg_publication_tables WHERE pubname = 'pub4' ORDER BY pubname, tablename; + DROP PUBLICATION pub1; DROP PUBLICATION pub2; DROP PUBLICATION pub3;I think this is a wrong assumption:
ScanKeyInit(&key[keycount++], Anum_pg_class_relispartition, BTEqualStrategyNumber, F_BOOLEQ, BoolGetDatum(false));
In this case sch5.part1 is partitioned table, but it also partition of table in different schema
Hi Sergey,
Thanks for reviewing the patch. I have addressed the comment in the
latest patch shared in [1]/messages/by-id/CANhcyEW0QMiJXMqpPFRHni-q0Rm4R0hpZ0LdaqA=F3wvDUU6sQ@mail.gmail.com.
[1]: /messages/by-id/CANhcyEW0QMiJXMqpPFRHni-q0Rm4R0hpZ0LdaqA=F3wvDUU6sQ@mail.gmail.com
Thanks and Regards,
Shlok Kyal
On 2025-Apr-28, Shlok Kyal wrote:
2. + * We also take a ShareLock on pg_partitioned_table to restrict addition + * of new partitioned table which may contain a foreign partition while + * publication is being created. XXX this is quite weird actually.This change was added to resolve the concurrency issue shared by
Vignesh in [4]. I tried with different locks and found that lock with
severity >= ShareLock was needed to avoid the concurrency issue.
Initially I added ShareLock to pg_class, but to reduce the scope I
added it to pg_partitioned_table instead. I cannot think of an
alternate approach. Do you have any suggestions for this?
[4]: /messages/by-id/CALDaNm2+eL22Sbvj74uS37xvt=haQWcOwP15QnDuVeYsjHiffw@mail.gmail.com
I think this is the sticky point in this patch. I think you need a
clearer explanation (in code comments) of why you need this lock, and
whether a weaker lock would be enough in some cases (see below); also I
suspect that these locking considerations are going to be important for
users so they're going to need to be documented in the SGML docs. What
operations are blocked when you hold this lock? Is replication going to
block altogether until the transaction that runs
publication_check_foreign_parts() commits/aborts? This is important
because it might mean that that users need to keep such transactions
short.
If your publication is FOR TABLES IN SCHEMA, can you do with blocking
creation of partitions only in that schema, or only partitions of
partitioned tables in that schema?
Another point that just occurred to me is that pg_upgrade --check may
need to alert users that if they have an incompatible setup in 18 or
earlier, then an upgrade to 19 does not work until they have fixed the
publications, detached the partitions, or some other remediation has
been applied.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"Linux transformó mi computadora, de una `máquina para hacer cosas',
en un aparato realmente entretenido, sobre el cual cada día aprendo
algo nuevo" (Jaime Salinas)
On Mon, 28 Apr 2025 at 19:57, Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
On 2025-Apr-28, Shlok Kyal wrote:
2. + * We also take a ShareLock on pg_partitioned_table to restrict addition + * of new partitioned table which may contain a foreign partition while + * publication is being created. XXX this is quite weird actually.This change was added to resolve the concurrency issue shared by
Vignesh in [4]. I tried with different locks and found that lock with
severity >= ShareLock was needed to avoid the concurrency issue.
Initially I added ShareLock to pg_class, but to reduce the scope I
added it to pg_partitioned_table instead. I cannot think of an
alternate approach. Do you have any suggestions for this?[4]: /messages/by-id/CALDaNm2+eL22Sbvj74uS37xvt=haQWcOwP15QnDuVeYsjHiffw@mail.gmail.com
I think this is the sticky point in this patch. I think you need a
clearer explanation (in code comments) of why you need this lock, and
whether a weaker lock would be enough in some cases (see below); also I
suspect that these locking considerations are going to be important for
users so they're going to need to be documented in the SGML docs. What
operations are blocked when you hold this lock? Is replication going to
block altogether until the transaction that runs
publication_check_foreign_parts() commits/aborts? This is important
because it might mean that that users need to keep such transactions
short.
ShareLock don't have any impact on replication.
For a partitioned table here is the observation when a ShareLock is
taken on the pg_partitioned table in a concurrent session:
1. CREATE TABLE (for partitioned table only) : blocked until
sharelock is released
2. INSERT : No impact
3 .UPDATE : No impact
4. TRUNCATE : No impact
5. DELETE : No impact
6. VACUUM : blocked until sharelock is released
7. DROP TABLE : blocked until sharelock is released
But I found one issue with the patch:
Suppose we have only a partitioned table t1 in a database. Now we
create a publication for all tables and add a breakpoint just after
the 'ShareLock'.
Now I run DROP TABLE t1; in a concurrent session. Now continue the
CREATE PUBLICATION command.
Now we hit a deadlock.
logs:
2025-05-09 13:35:57.199 IST [3567096] ERROR: deadlock detected
2025-05-09 13:35:57.199 IST [3567096] DETAIL: Process 3567096 waits
for AccessShareLock on relation 16411 of database 5; blocked by
process 3567751.
Process 3567751 waits for RowExclusiveLock on relation 3350 of
database 5; blocked by process 3567096.
Process 3567096: CREATE PUBLICATION pub1 FOR ALL TABLES WITH
(publish_via_partition_root);
Process 3567751: drop table t1;
This happens because the DROP TABLE command takes an
AccessExclusiveLock on the table t1 and is waiting to take
RowExclusiveLock on pg_partitioned_table. And
CREATE PUBLICATION command has taken ShareLock on pg_partitioned_table
and is waiting to take an AccessShareLock on table t1.
One thing I thought about is if we can change the ordering of the
locks while checking during creating publication, but it is not
possible as we must take lock on pg_partitoned_table first to avoid
creation of any partitioned table while we are fetching the table
list.
Do you have suggestion what should we do in above case?
If your publication is FOR TABLES IN SCHEMA, can you do with blocking
creation of partitions only in that schema, or only partitions of
partitioned tables in that schema?
We should not block only a particular schema because we can create a
partition of a partitioned table in a different schema as well.
Another point that just occurred to me is that pg_upgrade --check may
need to alert users that if they have an incompatible setup in 18 or
earlier, then an upgrade to 19 does not work until they have fixed the
publications, detached the partitions, or some other remediation has
been applied.
I have added a check in pg_upgrade and attached the same here.
Thanks and Regards,
Shlok Kyal
Attachments:
v14-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v14-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From 9d7831fc9f160d3a6042f9a78d0cedbd3fedd546 Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Fri, 28 Mar 2025 11:15:09 +0530
Subject: [PATCH v14] Restrict publishing of partitioned table with foreign
table as its partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when we create a publication on a partitioned
table that has a foreign table as its partition, the initial sync of
such table is successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
The root table is published, and initial data from partitions that are
foreign tables is replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except partitions that are foreign tables are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published, including initial data from partitions
that are foreign tables.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
We throw an error when we try to publish a partition that is a foreign
table. Error is thrown when we try to create a publication on (or add to
existing publication) a partitioned table that has foreign tables as its
partitions, when try to create a partition that is a foreign table and
when we try to attach foreign table (or a partitioned table with
foreign tables as partitions) to existing published tables.
2. with publish_via_partition_root = false
We skip publishing partitions that are foreign tables. This is done by
avoid adding such partitions in pg_subscription_rel catalog table.
We have introduced two functions 'RelationHasForeignPartition' and
'publication_check_foreign_parts'. In 'RelationHasForeignPartition' we go
through the child nodes of a partition and check if it has a foreign
table. While doing so, we take an AccessShareLock on each partition table
to prevent concurrent creation of a foreign table as a partition. In
'publication_check_foreign_parts' if schema id is provided we check for
each partitioned table in that schema if it has a foreign table as its
partition, or if schema id is not provided we check for each
partitioned table in the database if it has a partition that's a foreign
table. While doing so we take a ShareLock on pg_partitioned_table so no
partition table is created concurrently after this check.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_publication.sgml | 18 +-
src/backend/catalog/pg_publication.c | 220 ++++++++++++++++++++--
src/backend/commands/publicationcmds.c | 47 +++++
src/backend/commands/tablecmds.c | 112 +++++++++++
src/backend/partitioning/partdesc.c | 32 ++++
src/bin/pg_upgrade/check.c | 89 +++++++++
src/include/catalog/pg_publication.h | 3 +
src/include/partitioning/partdesc.h | 1 +
src/test/regress/expected/publication.out | 99 ++++++++++
src/test/regress/sql/publication.sql | 90 +++++++++
11 files changed, 699 insertions(+), 22 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f288c049a5c..57236a452e9 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,10 +2154,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>. In this mode, changes to a partition that is a
+ foreign table are ignored for the purposes of replication, and data
+ contained in them is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_publication.sgml b/doc/src/sgml/ref/create_publication.sgml
index 802630f2df1..1b07d57844e 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -251,13 +251,23 @@ CREATE PUBLICATION <replaceable class="parameter">name</replaceable>
</para>
<para>
- This parameter also affects how row filters and column lists are
- chosen for partitions; see below for details.
+ If this parameter is enabled, <literal>TRUNCATE</literal>
+ operations performed directly on partitions are not replicated.
</para>
<para>
- If this is enabled, <literal>TRUNCATE</literal> operations performed
- directly on partitions are not replicated.
+ If this parameter is enabled, foreign tables and partitioned tables
+ containing partitions that are foreign tables may not be
+ added to the publication. Conversely, foreign tables may not be
+ attached to a partitioned table that is included in a publication
+ with this parameter enabled. Lastly, this parameter may not be
+ changed on publications that include partitioned tables with foreign
+ tables as partitions.
+ </para>
+
+ <para>
+ This parameter also affects how row filters and column lists are
+ chosen for partitions; see below for details.
</para>
</listitem>
</varlistentry>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..e6e453b097c 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -26,12 +26,15 @@
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "catalog/pg_namespace.h"
+#include "catalog/pg_partitioned_table.h"
#include "catalog/pg_publication.h"
#include "catalog/pg_publication_namespace.h"
#include "catalog/pg_publication_rel.h"
#include "catalog/pg_type.h"
#include "commands/publicationcmds.h"
#include "funcapi.h"
+#include "partitioning/partdesc.h"
+#include "storage/lmgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/catcache.h"
@@ -53,7 +56,7 @@ typedef struct
* error if not.
*/
static void
-check_publication_add_relation(Relation targetrel)
+check_publication_add_relation(Publication *pub, Relation targetrel)
{
/* Must be a regular or partitioned table */
if (RelationGetForm(targetrel)->relkind != RELKIND_RELATION &&
@@ -64,6 +67,21 @@ check_publication_add_relation(Relation targetrel)
RelationGetRelationName(targetrel)),
errdetail_relkind_not_supported(RelationGetForm(targetrel)->relkind)));
+ /*
+ * publish_via_partition_root cannot be true if it is a partitioned table
+ * and has any partition that's a foreign table. See
+ * publication_check_foreign_parts for details.
+ */
+ if (pub->pubviaroot &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(targetrel))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot add partitioned table \"%s\" to publication \"%s\", which has \"%s\" set to \"%s\"",
+ RelationGetRelationName(targetrel), pub->name,
+ "publish_via_partition_root", "true"),
+ errdetail("The table contains a partition that's a foreign table."));
+
/* Can't be system table */
if (IsCatalogRelation(targetrel))
ereport(ERROR,
@@ -111,6 +129,171 @@ check_publication_add_schema(Oid schemaid)
errdetail("Temporary schemas cannot be replicated.")));
}
+/*
+ * Returns true if the ancestor is in the list of relations
+ * Otherwise, returns false.
+ */
+static bool
+is_ancestor_member_relids(Oid ancestor, List *relids)
+{
+ foreach_oid(relid, relids)
+ {
+ if (relid == ancestor)
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * Filter out the partitions whose parent tables are also present in the list
+ * of relations.
+ */
+static void
+filter_partition_rels(List *relids)
+{
+ foreach_oid(relid, relids)
+ {
+ bool skip = false;
+ List *ancestors = NIL;
+
+ if (get_rel_relispartition(relid))
+ ancestors = get_partition_ancestors(relid);
+
+ foreach_oid(ancestor, ancestors)
+ {
+ if (is_ancestor_member_relids(ancestor, relids))
+ {
+ skip = true;
+ break;
+ }
+ }
+
+ if (skip)
+ relids = list_delete_oid(relids, relid);
+ }
+}
+
+/*
+ * publication_check_foreign_parts
+ * Helper function to ensure we don't indirectly publish foreign tables
+ *
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables. To
+ * avoid the inconsistencies that would result, we disallow foreign
+ * tables from being published generally. However, it's possible for
+ * partitioned tables to have foreign tables as partitions, and we would
+ * like to allow publishing those partitioned tables so that the other
+ * partitions are replicated.
+ *
+ * This function is in charge of detecting if a partitioned table has a
+ * foreign table as a partition -- either in the whole database (useful
+ * for FOR ALL TABLES publications) or in a particular schema (useful
+ * for FOR TABLES IN SCHEMA publications). This function must be called
+ * only for publications with publish_via_partition_root=true.
+ *
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
+ *
+ * If a valid schemaid is provided, check if that schema has any
+ * partitioned table with a foreign table as partition.
+ *
+ * If no valid schemaid is provided, check all partitioned tables.
+ *
+ * We take a lock on partition tables so no new foreign table are added
+ * concurrently as a partition.
+ *
+ * We also take a ShareLock on pg_partitioned_table to restrict addition
+ * of new partitioned table which may include a foreign table as partition
+ * while publication is being created. XXX this is quite weird actually.
+ */
+void
+publication_check_foreign_parts(Oid schemaid, char *pubname)
+{
+ Relation classRel;
+ ScanKeyData key[3];
+ int keycount = 0;
+ TableScanDesc scan;
+ HeapTuple tuple;
+ List *relids = NIL;
+
+ /*
+ * Take lock on pg_partitioned_rel. This prevents new publications from
+ * being created.
+ */
+ LockRelationOid(PartitionedRelationId, ShareLock);
+
+ classRel = table_open(RelationRelationId, AccessShareLock);
+
+ /* Get the root nodes of partitioned table */
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relkind,
+ BTEqualStrategyNumber, F_CHAREQ,
+ CharGetDatum(RELKIND_PARTITIONED_TABLE));
+
+ /* If schema id is provided check partitioned table in that schema */
+ if (OidIsValid(schemaid))
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relnamespace,
+ BTEqualStrategyNumber, F_OIDEQ,
+ schemaid);
+
+ /*
+ * If schema id is not provided, take relations for which relispartition
+ * is false. This will give only the root partitioned tables. We donot
+ * include this for the case when schema id is specified because there can
+ * be cases when root partitioned tables are not part of schema and one of
+ * the child partition can still have a foreign table as its partition.
+ */
+ else
+ ScanKeyInit(&key[keycount++],
+ Anum_pg_class_relispartition,
+ BTEqualStrategyNumber, F_BOOLEQ,
+ BoolGetDatum(false));
+
+ scan = table_beginscan_catalog(classRel, keycount, key);
+ while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class relForm = (Form_pg_class) GETSTRUCT(tuple);
+
+ relids = lappend_oid(relids, relForm->oid);
+ }
+
+ /*
+ * If schema id is provided filter partitions list to have only topmost
+ * partitioned tables in that schema, to avoid repeated check.
+ */
+ if (OidIsValid(schemaid))
+ filter_partition_rels(relids);
+
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel = table_open(relid, AccessShareLock);
+
+ if (pubrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(pubrel))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errtable(pubrel),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ get_rel_name(relid))));
+
+ /*
+ * Keep lock till end of transaction: must prevent this table from
+ * being attached a foreign table until we're done.
+ */
+ table_close(pubrel, NoLock);
+ }
+
+ table_endscan(scan);
+ table_close(classRel, AccessShareLock);
+}
+
/*
* Returns if relation represented by oid and Form_pg_class entry
* is publishable.
@@ -304,7 +487,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +496,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
@@ -463,7 +642,7 @@ publication_add_relation(Oid pubid, PublicationRelInfo *pri,
RelationGetRelationName(targetrel), pub->name)));
}
- check_publication_add_relation(targetrel);
+ check_publication_add_relation(pub, targetrel);
/* Validate and translate column names into a Bitmapset of attnums. */
attnums = pub_collist_validate(pri->relation, pri->columns);
@@ -703,6 +882,13 @@ publication_add_schema(Oid pubid, Oid schemaid, bool if_not_exists)
check_publication_add_schema(schemaid);
+ /*
+ * If publish_via_partition_root is true, check if schema has any foreign
+ * partition
+ */
+ if (pub->pubviaroot)
+ publication_check_foreign_parts(schemaid, pub->name);
+
/* Form a tuple */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 0b23d94c38e..a2411fbebda 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -35,6 +35,7 @@
#include "commands/publicationcmds.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
+#include "partitioning/partdesc.h"
#include "parser/parse_clause.h"
#include "parser/parse_collate.h"
#include "parser/parse_relation.h"
@@ -915,6 +916,12 @@ CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt)
/* Associate objects with the publication. */
if (stmt->for_all_tables)
{
+ /*
+ * Check if any partitioned table has a foreign table as its partition
+ */
+ if (publish_via_partition_root)
+ publication_check_foreign_parts(InvalidOid, stmt->pubname);
+
/* Invalidate relcache so that publication info is rebuilt. */
CacheInvalidateRelcacheAll();
}
@@ -1080,6 +1087,46 @@ AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt,
}
}
+ /*
+ * If publish_via_partition_root is set to true, check if the publication
+ * has any partition that's a foreign table. See
+ * publication_check_foreign_parts for details.
+ */
+ if (publish_via_partition_root_given && publish_via_partition_root)
+ {
+ char *pubname = stmt->pubname;
+ List *schemaoids;
+ List *relids;
+
+ if (pubform->puballtables)
+ publication_check_foreign_parts(InvalidOid, pubname);
+
+ schemaoids = GetPublicationSchemas(pubform->oid);
+ foreach_oid(schemaoid, schemaoids)
+ publication_check_foreign_parts(schemaoid, pubname);
+
+ relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT);
+ foreach_oid(relid, relids)
+ {
+ Relation pubrel;
+
+ if (get_rel_relkind(relid) != RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ pubrel = table_open(relid, AccessShareLock);
+
+ if (RelationHasForeignPartition(pubrel))
+ ereport(ERROR,
+ errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("cannot set parameter \"%s\" to \"%s\" for publication \"%s\"",
+ "publish_via_partition_root", "true", pubname),
+ errdetail("Published partitioned table \"%s\" contains a partition that is a foreign table.",
+ RelationGetRelationName(pubrel)));
+
+ table_close(pubrel, NoLock);
+ }
+ }
+
/* Everything ok, form a new tuple. */
memset(values, 0, sizeof(values));
memset(nulls, false, sizeof(nulls));
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 54ad38247aa..9e94a489cd6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1133,6 +1133,53 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled. For details, see
+ * publication_check_foreign_parts.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = GetRelationPublications(parent->rd_id);
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20291,6 +20338,71 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the parent
+ * table is not in a publication with publish_via_partition_root enabled.
+ * See publication_check_foreign_parts.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub;
+
+ pub = GetPublication(puboid);
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with a partition that's a foreign table to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..b53139bafdd 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], AccessShareLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 940fc77fc2e..2cf407a3169 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -31,6 +31,7 @@ static void check_new_cluster_logical_replication_slots(void);
static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
+static void check_for_valid_publication(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -633,6 +634,8 @@ check_and_dump_old_cluster(void)
check_old_cluster_subscription_state();
}
+ check_for_valid_publication(&old_cluster);
+
check_for_data_types_usage(&old_cluster);
/*
@@ -2272,3 +2275,89 @@ check_old_cluster_subscription_state(void)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_valid_publication()'s UpgradeTask. If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_pub_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_puboid = PQfnumber(res, "oid");
+ int i_pubviaroot = PQfnumber(res, "pubviaroot");
+ int i_puballtables = PQfnumber(res, "puballtables");
+ int i_ftrelid = PQfnumber(res, "ftrelid");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, "The publication with oid: %s, has foreign table with oid: %s as partition of a published table. "
+ "Publication is published with publish_via_partition_root = %s and it is published %s.\n",
+ PQgetvalue(res, rowno, i_puboid),
+ PQgetvalue(res, rowno, i_ftrelid),
+ strcmp(PQgetvalue(res, rowno, i_pubviaroot), "f") ? "true" : "false",
+ strcmp(PQgetvalue(res, rowno, i_puballtables), "f") ? "FOR ALL TABLES" : "FOR TABLE/FOR TABLES IN SCHEMA");
+}
+
+/*
+ * check_for_valid_publication
+ *
+ * Verify if the publication has any published foreign table which are
+ * partitions of a table.
+ *
+ * For PG 18 and below foreign tables can be indirectly published when it is
+ * partition of a partitioned table. Avoid upgrading the cluster if it has a
+ * publication with publish_via_partition_root = true and has foreign table
+ * which is partition of a published table.
+ */
+static void
+check_for_valid_publication(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task;
+ const char *query;
+
+ if (GET_MAJOR_VERSION(cluster->major_version) >= 1900)
+ return;
+
+ prep_status("Checking for invalid publications");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "pubs_invalid.txt");
+
+ query = "SELECT p.oid, p.pubviaroot, p.puballtables, ft.ftrelid "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true;";
+
+ task = upgrade_task_create();
+ upgrade_task_add_step(task, query,
+ process_pub_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains publications, which has foreign table which are partitions of published table.\n"
+ "A list of potentially-affected publications is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h
index 48c7d1a8615..71ad5a6f846 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -19,6 +19,7 @@
#include "catalog/genbki.h"
#include "catalog/objectaddress.h"
+#include "catalog/pg_class.h"
#include "catalog/pg_publication_d.h" /* IWYU pragma: export */
/* ----------------
@@ -178,6 +179,8 @@ extern Oid GetTopMostAncestorInPublication(Oid puboid, List *ancestors,
extern bool is_publishable_relation(Relation rel);
extern bool is_schema_publication(Oid pubid);
+extern void publication_check_foreign_parts(Oid schemaid, char *pubname);
+
extern bool check_and_fetch_column_list(Publication *pub, Oid relid,
MemoryContext mcxt, Bitmapset **cols);
extern ObjectAddress publication_add_relation(Oid pubid, PublicationRelInfo *pri,
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out
index 4de96c04f9d..f57addab23d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -1924,6 +1924,105 @@ DROP PUBLICATION pub1;
DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a partition that's a foreign table
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ERROR: cannot add partitioned table "tmain" to publication "pub1", which has "publish_via_partition_root" set to "true"
+DETAIL: The table contains a partition that's a foreign table.
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+ERROR: cannot attach table "part2" with a partition that's a foreign table to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+ERROR: cannot create foreign table "part3_1" as a partition of "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+ERROR: cannot attach foreign table "part3_2" to partition table "tmain"
+DETAIL: Partitioned table "tmain" is published with option "publish_via_partition_root" in publication "pub1".
+DROP PUBLICATION pub1;
+-- Can't create publication with publish_via_partition_root = true on
+-- partitioned table(which is not root) with a partition that's a foreign table
+-- on other schema
+CREATE SCHEMA sch5;
+CREATE TABLE sch5.part1 PARTITION OF sch4.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch4.part1_1 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "part1" contains a partition that is a foreign table.
+-- Test with publish_via_partition_root = false
+-- Partition that are foreign tables are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+-- Create a partition that's a foreign table of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+-- Attach partition that's a foreign table to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+ pubname | tablename
+---------+-----------
+ pub1 | part1
+ pub2 | part1
+ pub3 | part1
+(3 rows)
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- a partition that's a foreign table
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
+DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SCHEMA sch5 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql
index 68001de4000..55c0dff3601 100644
--- a/src/test/regress/sql/publication.sql
+++ b/src/test/regress/sql/publication.sql
@@ -1223,6 +1223,96 @@ DROP PUBLICATION pub2;
DROP TABLE gencols;
RESET client_min_messages;
+-- ======================================================
+
+-- Test when foreign table is a partition of a partitioned table on which
+-- publication is created
+SET client_min_messages = 'ERROR';
+CREATE FOREIGN DATA WRAPPER test_fdw;
+CREATE SERVER fdw_server FOREIGN DATA WRAPPER test_fdw;
+
+CREATE SCHEMA sch3;
+CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
+CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
+CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create publications with publish_via_partition_root = true, if table
+-- has a partition that's a foreign table
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch3 WITH (publish_via_partition_root);
+CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
+
+-- Test when a partitioned table with foreign table as a partition is attached
+-- to partitioned table which is already published
+ALTER TABLE sch3.tmain DETACH PARTITION sch3.part2;
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
+
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_2(id int) SERVER fdw_server;
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+CREATE SCHEMA sch4;
+CREATE TABLE sch4.tmain(id int) PARTITION BY RANGE(id);
+
+-- publication created with FOR TABLES IN SCHEMA
+DROP PUBLICATION pub1;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch4 WITH (publish_via_partition_root);
+
+-- Can't create a foreign table that is partition of table published with
+-- publish_via_partition_root = true
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch4.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Can't attach a foreign table as partition to table published with
+-- publish_via_partition_root = true
+ALTER TABLE sch4.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+DROP PUBLICATION pub1;
+
+-- Can't create publication with publish_via_partition_root = true on
+-- partitioned table(which is not root) with a partition that's a foreign table
+-- on other schema
+CREATE SCHEMA sch5;
+CREATE TABLE sch5.part1 PARTITION OF sch4.tmain FOR VALUES FROM (0) TO (10) PARTITION BY RANGE(id);
+CREATE FOREIGN TABLE sch4.part1_1 PARTITION OF sch5.part1 FOR VALUES FROM (0) TO (5) SERVER fdw_server;
+CREATE PUBLICATION pub1 FOR TABLES IN SCHEMA sch5 WITH (publish_via_partition_root);
+
+-- Test with publish_via_partition_root = false
+-- Partition that are foreign tables are skipped by default
+CREATE PUBLICATION pub1 FOR TABLE sch3.tmain;
+CREATE PUBLICATION pub2 FOR TABLES IN SCHEMA sch3;
+CREATE PUBLICATION pub3 FOR ALL TABLES;
+
+-- Create a partition that's a foreign table of published table with
+-- publish_via_partition_root = false
+CREATE FOREIGN TABLE sch3.part3_1 PARTITION OF sch3.tmain FOR VALUES FROM (10) TO (15) SERVER fdw_server;
+
+-- Attach partition that's a foreign table to published table
+-- publish_via_partition_root = false
+ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part3_2 FOR VALUES FROM (15) TO (20);
+
+-- Check the published tables
+SELECT pubname, tablename FROM pg_publication_tables WHERE schemaname in ('sch3', 'sch4') ORDER BY pubname, tablename;
+
+-- Can't alter publish_via_partition_root to true, if publication already have
+-- a partition that's a foreign table
+ALTER PUBLICATION pub1 SET (publish_via_partition_root);
+
+DROP PUBLICATION pub1;
+DROP PUBLICATION pub2;
+DROP PUBLICATION pub3;
+DROP SCHEMA sch3 CASCADE;
+DROP SCHEMA sch4 CASCADE;
+DROP SCHEMA sch5 CASCADE;
+DROP SERVER fdw_server;
+DROP FOREIGN DATA WRAPPER test_fdw;
+
RESET SESSION AUTHORIZATION;
DROP ROLE regress_publication_user, regress_publication_user2;
DROP ROLE regress_publication_user_dummy;
--
2.34.1
Hello,
I think reimplementing list_member_oid() under a different name
(is_ancestor_member_relids) is pointless and should not be done.
It also appears to me that we haven't nailed the error messages just
yet. I tried to fix it upthread, but didn't really get it correct. For
instance, consider this case taken from the new regression tests:
/* test setup */
CREATE SCHEMA sch3;
CREATE TABLE sch3.tmain(id int) PARTITION BY RANGE(id);
CREATE TABLE sch3.part1 PARTITION OF sch3.tmain FOR VALUES FROM (0) TO (5);
CREATE TABLE sch3.part2(id int) PARTITION BY RANGE(id);
CREATE FOREIGN TABLE sch3.part2_1 PARTITION OF sch3.part2 FOR VALUES FROM (5) TO (10) SERVER fdw_server;
ALTER TABLE sch3.tmain ATTACH PARTITION sch3.part2 FOR VALUES FROM (5) TO (10);
The issue occurs in the next command:
CREATE PUBLICATION pub1 FOR TABLE sch3.tmain WITH (publish_via_partition_root);
ERROR: cannot add partitioned table "tmain" to publication "pub1", which has "publish_via_partition_root" set to "true"
DETAIL: The table contains a partition that's a foreign table.
The user is trying to create a publication, so an error saying that it's
not possible to add a partition is a bit off-point. (If you squint hard
enough it sort of makes sense, but it seems inconsistent enough.)
We could replace the word "add" with the word "include" there and this
particular problem would be fixed, perhaps. However, the next one is
worse:
CREATE PUBLICATION pub1 FOR ALL TABLES WITH (publish_via_partition_root);
ERROR: cannot set parameter "publish_via_partition_root" to "true" for publication "pub1"
DETAIL: Published partitioned table "tmain" contains a partition that is a foreign table.
Here we again try to create a publication, but we get an error that we
cannot set the parameter, which is even more detached from reality.
Maybe what we need is to add another parameter to that function to
indicate which operation is being executed (create publication, add
schema, change the parameter), which is used to indicate the error
message to throw.
With that thought in mind, I think it would make things simpler for us
to remove the duplicate copies of the same error messages by adding yet
another function, after publication_check_foreign_parts(), which receives
the operation being executed and whatever additional parameter it needs,
and throws the error. If publication_check_foreign_parts() also
receives the operation being executed, it can simply pass it through to
that new function when an error needs to be thrown; other places that
hardcode error situations would pass a constant for the operation.
But the non-idiomatic locking of pg_partitioned_table appears to
continue to be the pain point of this patch. My impression is that
using a lock is the wrong approach to solve the concurrency problem.
Maybe we can use a ConditionVariable instead somehow. (The real trick
here is to figure out exactly _how_ to use the CV, I mean what exactly
is the condition that the CV sleeps on?)
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
"Industry suffers from the managerial dogma that for the sake of stability
and continuity, the company should be independent of the competence of
individual employees." (E. Dijkstra)
On Sun, May 11, 2025 at 6:53 AM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
But the non-idiomatic locking of pg_partitioned_table appears to
continue to be the pain point of this patch. My impression is that
using a lock is the wrong approach to solve the concurrency problem.
Maybe we can use a ConditionVariable instead somehow. (The real trick
here is to figure out exactly _how_ to use the CV, I mean what exactly
is the condition that the CV sleeps on?)
Can we fix this problem by having a check at the time of
CREATESUBSCRIPTION such that, if copy-data is true, then we ensure
that the specified publishers don't have a foreign table? We have a
somewhat similar check for publications in the function
check_publications_origin(), though for a different purpose. Along
with it, we can still have a check during foreign table
creation/attach that it doesn't become part of some publication, as
the patch may have it now.
--
With Regards,
Amit Kapila.
On Thu, 15 May 2025 at 18:19, Amit Kapila <amit.kapila16@gmail.com> wrote:
On Sun, May 11, 2025 at 6:53 AM Álvaro Herrera <alvherre@alvh.no-ip.org> wrote:
But the non-idiomatic locking of pg_partitioned_table appears to
continue to be the pain point of this patch. My impression is that
using a lock is the wrong approach to solve the concurrency problem.
Maybe we can use a ConditionVariable instead somehow. (The real trick
here is to figure out exactly _how_ to use the CV, I mean what exactly
is the condition that the CV sleeps on?)Can we fix this problem by having a check at the time of
CREATESUBSCRIPTION such that, if copy-data is true, then we ensure
that the specified publishers don't have a foreign table? We have a
somewhat similar check for publications in the function
check_publications_origin(), though for a different purpose. Along
with it, we can still have a check during foreign table
creation/attach that it doesn't become part of some publication, as
the patch may have it now.
This approach seems better to me. I have created a patch with the
above approach.
Thanks and Regards,
Shlok Kyal
Attachments:
v15-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v15-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From f13809f9a46d18eec02407cf6ea4a104627e9a9c Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 19 May 2025 19:47:04 +0530
Subject: [PATCH v15] Restrict publishing of partitioned table with foreign
table as its partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when we create a publication on a partitioned
table that has a foreign table as its partition, the initial sync of
such table is successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
The root table is published, and initial data from partitions that are
foreign tables is replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except partitions that are foreign tables are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published, including initial data from partitions
that are foreign tables.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
An error is thrown when we try to create subscription (with copy_data = true)
on publication with partitioned table with foreign partition.
When copy_data = false, we donot need this check as initial sync is not
performed.
Also we restrict creating/attaching a foreign table as partition of a
partitioned table which is part of any publication.
2. with publish_via_partition_root = false
We skip publishing partitions that are foreign tables. This is done by
avoid adding such partitions in pg_subscription_rel catalog table.
We have introduced two functions 'RelationHasForeignPartition' and
'check_publications_foreign_parts'. In 'RelationHasForeignPartition' we go
through the child nodes of a partition and check if it has a foreign
table. In 'check_publications_foreign_parts' we check if a subscription
with copy_data = true is subscribing to a publication which has partitioned
table with foreign table as its partition.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/catalog/pg_publication.c | 26 ++--
src/backend/commands/subscriptioncmds.c | 101 ++++++++++++++++
src/backend/commands/tablecmds.c | 110 +++++++++++++++++
src/backend/partitioning/partdesc.c | 32 +++++
src/bin/pg_upgrade/check.c | 89 ++++++++++++++
src/include/partitioning/partdesc.h | 1 +
src/test/subscription/Makefile | 1 +
src/test/subscription/t/013_partition.pl | 139 ++++++++++++++++++++++
10 files changed, 500 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index f288c049a5c..57236a452e9 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2154,10 +2154,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>. In this mode, changes to a partition that is a
+ foreign table are ignored for the purposes of replication, and data
+ contained in them is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 57dec28a5df..3d8992e27e3 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -545,6 +545,13 @@ WHERE C.relnamespace = N.oid AND
PT.pubname IN (<pub-names>);
</programlisting></para>
+ <para>
+ When using a subscription parameter copy_data = true, corresponding
+ publications are checked if it has publish_via_partition_root = true and
+ has partitioned table with foreign table as partition. If this scenario is
+ detected we ERROR is logged to the user.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..56255dcd5cc 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -304,7 +304,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +313,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 4aec73bcc6b..a81b334d127 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -112,6 +112,9 @@ static List *merge_publications(List *oldpublist, List *newpublist, bool addpub,
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
static void CheckAlterSubOption(Subscription *sub, const char *option,
bool slot_needs_update, bool isTopLevel);
+static void check_publications_foreign_parts(WalReceiverConn *wrconn,
+ List *publications, bool copydata,
+ char *subname);
/*
@@ -723,6 +726,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
check_publications(wrconn, publications);
check_publications_origin(wrconn, publications, opts.copy_data,
opts.origin, NULL, 0, stmt->subname);
+ check_publications_foreign_parts(wrconn, publications,
+ opts.copy_data, stmt->subname);
/*
* Set sync state based on if we were asked to do data copy or
@@ -883,6 +888,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
check_publications_origin(wrconn, sub->publications, copy_data,
sub->origin, subrel_local_oids,
subrel_count, sub->name);
+ check_publications_foreign_parts(wrconn, sub->publications, copy_data,
+ sub->name);
/*
* Rels that we want to remove from subscription and drop any slots
@@ -2507,3 +2514,97 @@ defGetStreamingMode(DefElem *def)
def->defname)));
return LOGICALREP_STREAM_OFF; /* keep compiler quiet */
}
+
+/*
+ * check_publications_foreign_parts
+ * Check if the publications, on which subscriber is subscribing, publishes any
+ * partitioned table that has an foreign table as its partition and has
+ * publish_via_partition_root set as true. The check is performed only if
+ * copy_data is set as true for the subscription.
+ *
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables. To
+ * avoid the inconsistencies that would result, we disallow foreign
+ * tables from being published generally. However, it's possible for
+ * partitioned tables to have foreign tables as partitions, and we would
+ * like to allow publishing those partitioned tables so that the other
+ * partitions are replicated.
+ *
+ * This function is in charge of detecting if publisher with
+ * publish_via_partition_root=true publishes a partitioned table that has a
+ * foreign table as a partition.
+ *
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
+ */
+static void
+check_publications_foreign_parts(WalReceiverConn *wrconn, List *publications,
+ bool copydata, char *subname)
+{
+ WalRcvExecResult *res;
+ StringInfoData cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[1] = {TEXTOID};
+ List *publist = NIL;
+ int i;
+
+ if (!copydata)
+ return;
+
+ initStringInfo(&cmd);
+ appendStringInfoString(&cmd,
+ "SELECT DISTINCT P.pubname AS pubname "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");
+
+ GetPublicationsStr(publications, &cmd, true);
+ appendStringInfoString(&cmd, ")\n");
+
+ res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
+ pfree(cmd.data);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not receive list of replicated tables from the publisher: %s",
+ res->err)));
+
+ /* Process tables. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ ExecClearTuple(slot);
+ publist = list_append_unique(publist, makeString(pubname));
+ }
+
+ if (publist)
+ {
+ StringInfo pubnames = makeStringInfo();
+
+ /* Prepare the list of publication(s) for warning message. */
+ GetPublicationsStr(publist, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("subscription \"%s\" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition",
+ subname),
+ errdetail_plural("The subscription being created on a publication (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ",
+ "The subscription being created on publications (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ",
+ list_length(publist), pubnames->data),
+ errhint("Drop the foreign table from the publication or set publish_via_partition_root = false on publication or set copy_data = false."));
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 54ad38247aa..345e0e8b279 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1133,6 +1133,52 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(parent->rd_id));
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20291,6 +20337,70 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the parent
+ * table is not in a publication with publish_via_partition_root enabled.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub;
+
+ pub = GetPublication(puboid);
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with a partition that's a foreign table to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..56d176625e6 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], NoLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 940fc77fc2e..2cf407a3169 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -31,6 +31,7 @@ static void check_new_cluster_logical_replication_slots(void);
static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
+static void check_for_valid_publication(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -633,6 +634,8 @@ check_and_dump_old_cluster(void)
check_old_cluster_subscription_state();
}
+ check_for_valid_publication(&old_cluster);
+
check_for_data_types_usage(&old_cluster);
/*
@@ -2272,3 +2275,89 @@ check_old_cluster_subscription_state(void)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_valid_publication()'s UpgradeTask. If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_pub_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_puboid = PQfnumber(res, "oid");
+ int i_pubviaroot = PQfnumber(res, "pubviaroot");
+ int i_puballtables = PQfnumber(res, "puballtables");
+ int i_ftrelid = PQfnumber(res, "ftrelid");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, "The publication with oid: %s, has foreign table with oid: %s as partition of a published table. "
+ "Publication is published with publish_via_partition_root = %s and it is published %s.\n",
+ PQgetvalue(res, rowno, i_puboid),
+ PQgetvalue(res, rowno, i_ftrelid),
+ strcmp(PQgetvalue(res, rowno, i_pubviaroot), "f") ? "true" : "false",
+ strcmp(PQgetvalue(res, rowno, i_puballtables), "f") ? "FOR ALL TABLES" : "FOR TABLE/FOR TABLES IN SCHEMA");
+}
+
+/*
+ * check_for_valid_publication
+ *
+ * Verify if the publication has any published foreign table which are
+ * partitions of a table.
+ *
+ * For PG 18 and below foreign tables can be indirectly published when it is
+ * partition of a partitioned table. Avoid upgrading the cluster if it has a
+ * publication with publish_via_partition_root = true and has foreign table
+ * which is partition of a published table.
+ */
+static void
+check_for_valid_publication(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task;
+ const char *query;
+
+ if (GET_MAJOR_VERSION(cluster->major_version) >= 1900)
+ return;
+
+ prep_status("Checking for invalid publications");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "pubs_invalid.txt");
+
+ query = "SELECT p.oid, p.pubviaroot, p.puballtables, ft.ftrelid "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true;";
+
+ task = upgrade_task_create();
+ upgrade_task_add_step(task, query,
+ process_pub_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains publications, which has foreign table which are partitions of published table.\n"
+ "A list of potentially-affected publications is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/subscription/Makefile b/src/test/subscription/Makefile
index 50b65d8f6ea..11041f79667 100644
--- a/src/test/subscription/Makefile
+++ b/src/test/subscription/Makefile
@@ -14,6 +14,7 @@ top_builddir = ../../..
include $(top_builddir)/src/Makefile.global
EXTRA_INSTALL = contrib/hstore
+EXTRA_INSTALL += contrib/postgres_fdw
export with_icu
diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl
index 61b0cb4aa1a..41cfdc86580 100644
--- a/src/test/subscription/t/013_partition.pl
+++ b/src/test/subscription/t/013_partition.pl
@@ -8,6 +8,9 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $stdout;
+my $stderr;
+
# setup
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
@@ -891,4 +894,140 @@ $result = $node_subscriber2->safe_psql('postgres',
"SELECT a, b, c FROM tab5_1 ORDER BY 1");
is($result, qq(4||1), 'updates of tab5 replicated correctly');
+# Test with partitioned table with foreign table as its partition.
+$node_publisher->safe_psql('postgres', "CREATE DATABASE db1");
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE EXTENSION IF NOT EXISTS postgres_fdw;
+ CREATE SERVER fdw FOREIGN DATA WRAPPER postgres_fdw;
+ CREATE USER MAPPING FOR CURRENT_USER SERVER fdw;});
+
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;}
+);
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE PUBLICATION pub_foreign_1 FOR TABLE sc1.tab6 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_2 FOR TABLES IN SCHEMA sc1 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_3 FOR ALL TABLES with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_4 FOR TABLES IN SCHEMA sc2 with (publish_via_partition_root);
+ });
+
+$node_subscriber1->safe_psql('postgres', "CREATE DATABASE db1");
+$node_subscriber1->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15);}
+);
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=db1';
+
+# Create subscription on publication which has partitioned table with foreign
+# table as its partition.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLE on partitioned table with foreign table as its partition"
+);
+
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLES IN SCHEMA on partitioned table with foreign table as its partition"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_3;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR ALL TABLES on partitioned table with foreign table as its partition"
+);
+
+# If publication is created with publish_via_partition_root = false, we can
+# create subscription on it. But replication of partitioned table with foreign
+# is skipped
+$node_publisher->safe_psql('db1',
+ "ALTER PUBLICATION pub_foreign_1 SET (publish_via_partition_root = false)"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# if subscription is created with copy_data=false, we can create subscription
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2 WITH (copy_data=false)"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# Alter subscription to include publisher which has partitioned table with
+# foreign table as its partition.
+$node_subscriber1->safe_psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_4;"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 ADD PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Add publication which has partitioned table with foreign table as its partition to subscription"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 SET PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "set publication which has partitioned table with foreign table as its partition for subscription"
+);
+
+# Create foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;"
+);
+$node_publisher->safe_psql('db1', "DROP FOREIGN TABLE sc1.tab6_2_1");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot create foreign table "tab6_2_1" as a partition of "tab6_2"/,
+ "Create foreign table as a partition of partitioned table already part of publication"
+);
+
+# Attach foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+$node_publisher->safe_psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot attach foreign table "tab6_2_1" to partition table "tab6_2"/,
+ "Attach foreign table as a partition of already published partitioned table"
+);
+
done_testing();
--
2.34.1
On Tue, May 20, 2025 at 2:33 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
This approach seems better to me. I have created a patch with the
above approach.Thanks and Regards,
Shlok Kyal
Some quick comments on the patch:
1. In doc/src/sgml/ref/create_subscription.sgml:
+ has partitioned table with foreign table as partition. If this scenario is
+ detected we ERROR is logged to the user.
+ </para>
+
Should be: "If this scenario is detected an ERROR is logged to the
user." (remove "we").
In src/backend/commands/subscriptioncmds.c:
2. The comment header:
+ * This function is in charge of detecting if publisher with
+ * publish_via_partition_root=true publishes a partitioned table that has a
+ * foreign table as a partition.
Add "and throw an error if found" at the end of that sentence to
correctly describe what the function does.
3.
+ appendStringInfoString(&cmd,
+ "SELECT DISTINCT P.pubname AS pubname "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN
pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot
= true AND p.pubname IN (");
use FROM rather than from to maintain SQL style consistency.
4.
+ errdetail_plural("The subscription being created on a
publication (%s) with publish_via_root_partition = true and contains
partitioned tables with foreign table as partition ",
+ "The subscription being created on
publications (%s) with publish_via_root_partition = true and contains
partitioned tables with foreign table as partition ",
+ list_length(publist), pubnames->data),
I think you meant "publish_via_partition_root" here and not
"publish_via_root_partition ".
regards,
Ajin Cherian
Fujitsu Australia
On Wed, 4 Jun 2025 at 16:12, Ajin Cherian <itsajin@gmail.com> wrote:
On Tue, May 20, 2025 at 2:33 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
This approach seems better to me. I have created a patch with the
above approach.Thanks and Regards,
Shlok KyalSome quick comments on the patch: 1. In doc/src/sgml/ref/create_subscription.sgml: + has partitioned table with foreign table as partition. If this scenario is + detected we ERROR is logged to the user. + </para> +Should be: "If this scenario is detected an ERROR is logged to the
user." (remove "we").In src/backend/commands/subscriptioncmds.c: 2. The comment header: + * This function is in charge of detecting if publisher with + * publish_via_partition_root=true publishes a partitioned table that has a + * foreign table as a partition.Add "and throw an error if found" at the end of that sentence to
correctly describe what the function does.3. + appendStringInfoString(&cmd, + "SELECT DISTINCT P.pubname AS pubname " + "from pg_catalog.pg_publication p, LATERAL " + "pg_get_publication_tables(p.pubname) gpt, LATERAL " + "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON " + "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");use FROM rather than from to maintain SQL style consistency.
4. + errdetail_plural("The subscription being created on a publication (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + "The subscription being created on publications (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + list_length(publist), pubnames->data),I think you meant "publish_via_partition_root" here and not
"publish_via_root_partition ".
I have addressed all the comments and attached the updated patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v16-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v16-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From e3dad3aa33ae8116b9b62ae4c11a9e8a143d8cca Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 19 May 2025 19:47:04 +0530
Subject: [PATCH v16] Restrict publishing of partitioned table with foreign
table as its partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when we create a publication on a partitioned
table that has a foreign table as its partition, the initial sync of
such table is successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
The root table is published, and initial data from partitions that are
foreign tables is replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except partitions that are foreign tables are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published, including initial data from partitions
that are foreign tables.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
An error is thrown when we try to create subscription (with copy_data = true)
on publication with partitioned table with foreign partition.
When copy_data = false, we donot need this check as initial sync is not
performed.
Also we restrict creating/attaching a foreign table as partition of a
partitioned table which is part of any publication.
2. with publish_via_partition_root = false
We skip publishing partitions that are foreign tables. This is done by
avoid adding such partitions in pg_subscription_rel catalog table.
We have introduced two functions 'RelationHasForeignPartition' and
'check_publications_foreign_parts'. In 'RelationHasForeignPartition' we go
through the child nodes of a partition and check if it has a foreign
table. In 'check_publications_foreign_parts' we check if a subscription
with copy_data = true is subscribing to a publication which has partitioned
table with foreign table as its partition.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/catalog/pg_publication.c | 26 ++--
src/backend/commands/subscriptioncmds.c | 101 ++++++++++++++++
src/backend/commands/tablecmds.c | 110 +++++++++++++++++
src/backend/partitioning/partdesc.c | 32 +++++
src/bin/pg_upgrade/check.c | 89 ++++++++++++++
src/include/partitioning/partdesc.h | 1 +
src/test/subscription/Makefile | 1 +
src/test/subscription/t/013_partition.pl | 139 ++++++++++++++++++++++
10 files changed, 500 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index 686dd441d02..460aa929b73 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2079,10 +2079,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>. In this mode, changes to a partition that is a
+ foreign table are ignored for the purposes of replication, and data
+ contained in them is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 57dec28a5df..8963123725b 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -545,6 +545,13 @@ WHERE C.relnamespace = N.oid AND
PT.pubname IN (<pub-names>);
</programlisting></para>
+ <para>
+ When using a subscription parameter copy_data = true, corresponding
+ publications are checked if it has publish_via_partition_root = true and
+ has partitioned table with foreign table as partition. If this scenario is
+ detected an ERROR is logged to the user.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..56255dcd5cc 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -304,7 +304,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +313,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 4aec73bcc6b..2557b06313d 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -112,6 +112,9 @@ static List *merge_publications(List *oldpublist, List *newpublist, bool addpub,
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
static void CheckAlterSubOption(Subscription *sub, const char *option,
bool slot_needs_update, bool isTopLevel);
+static void check_publications_foreign_parts(WalReceiverConn *wrconn,
+ List *publications, bool copydata,
+ char *subname);
/*
@@ -723,6 +726,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
check_publications(wrconn, publications);
check_publications_origin(wrconn, publications, opts.copy_data,
opts.origin, NULL, 0, stmt->subname);
+ check_publications_foreign_parts(wrconn, publications,
+ opts.copy_data, stmt->subname);
/*
* Set sync state based on if we were asked to do data copy or
@@ -883,6 +888,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
check_publications_origin(wrconn, sub->publications, copy_data,
sub->origin, subrel_local_oids,
subrel_count, sub->name);
+ check_publications_foreign_parts(wrconn, sub->publications, copy_data,
+ sub->name);
/*
* Rels that we want to remove from subscription and drop any slots
@@ -2507,3 +2514,97 @@ defGetStreamingMode(DefElem *def)
def->defname)));
return LOGICALREP_STREAM_OFF; /* keep compiler quiet */
}
+
+/*
+ * check_publications_foreign_parts
+ * Check if the publications, on which subscriber is subscribing, publishes any
+ * partitioned table that has an foreign table as its partition and has
+ * publish_via_partition_root set as true. The check is performed only if
+ * copy_data is set as true for the subscription.
+ *
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables. To
+ * avoid the inconsistencies that would result, we disallow foreign
+ * tables from being published generally. However, it's possible for
+ * partitioned tables to have foreign tables as partitions, and we would
+ * like to allow publishing those partitioned tables so that the other
+ * partitions are replicated.
+ *
+ * This function is in charge of detecting if publisher with
+ * publish_via_partition_root=true publishes a partitioned table that has a
+ * foreign table as a partition and throw an error if found.
+ *
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
+ */
+static void
+check_publications_foreign_parts(WalReceiverConn *wrconn, List *publications,
+ bool copydata, char *subname)
+{
+ WalRcvExecResult *res;
+ StringInfoData cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[1] = {TEXTOID};
+ List *publist = NIL;
+ int i;
+
+ if (!copydata)
+ return;
+
+ initStringInfo(&cmd);
+ appendStringInfoString(&cmd,
+ "SELECT DISTINCT P.pubname AS pubname "
+ "FROM pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");
+
+ GetPublicationsStr(publications, &cmd, true);
+ appendStringInfoString(&cmd, ")\n");
+
+ res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
+ pfree(cmd.data);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not receive list of replicated tables from the publisher: %s",
+ res->err)));
+
+ /* Process tables. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ ExecClearTuple(slot);
+ publist = list_append_unique(publist, makeString(pubname));
+ }
+
+ if (publist)
+ {
+ StringInfo pubnames = makeStringInfo();
+
+ /* Prepare the list of publication(s) for warning message. */
+ GetPublicationsStr(publist, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("subscription \"%s\" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition",
+ subname),
+ errdetail_plural("The subscription being created on a publication (%s) with publish_via_partition_root = true and contains partitioned tables with foreign table as partition ",
+ "The subscription being created on publications (%s) with publish_via_partition_root = true and contains partitioned tables with foreign table as partition ",
+ list_length(publist), pubnames->data),
+ errhint("Drop the foreign table from the publication or set publish_via_partition_root = false on publication or set copy_data = false."));
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index ea96947d813..0f7f1bd4b42 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1133,6 +1133,52 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(parent->rd_id));
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20317,6 +20363,70 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the parent
+ * table is not in a publication with publish_via_partition_root enabled.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub;
+
+ pub = GetPublication(puboid);
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with a partition that's a foreign table to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..56d176625e6 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], NoLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index 940fc77fc2e..2cf407a3169 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -31,6 +31,7 @@ static void check_new_cluster_logical_replication_slots(void);
static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
+static void check_for_valid_publication(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -633,6 +634,8 @@ check_and_dump_old_cluster(void)
check_old_cluster_subscription_state();
}
+ check_for_valid_publication(&old_cluster);
+
check_for_data_types_usage(&old_cluster);
/*
@@ -2272,3 +2275,89 @@ check_old_cluster_subscription_state(void)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_valid_publication()'s UpgradeTask. If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_pub_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_puboid = PQfnumber(res, "oid");
+ int i_pubviaroot = PQfnumber(res, "pubviaroot");
+ int i_puballtables = PQfnumber(res, "puballtables");
+ int i_ftrelid = PQfnumber(res, "ftrelid");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, "The publication with oid: %s, has foreign table with oid: %s as partition of a published table. "
+ "Publication is published with publish_via_partition_root = %s and it is published %s.\n",
+ PQgetvalue(res, rowno, i_puboid),
+ PQgetvalue(res, rowno, i_ftrelid),
+ strcmp(PQgetvalue(res, rowno, i_pubviaroot), "f") ? "true" : "false",
+ strcmp(PQgetvalue(res, rowno, i_puballtables), "f") ? "FOR ALL TABLES" : "FOR TABLE/FOR TABLES IN SCHEMA");
+}
+
+/*
+ * check_for_valid_publication
+ *
+ * Verify if the publication has any published foreign table which are
+ * partitions of a table.
+ *
+ * For PG 18 and below foreign tables can be indirectly published when it is
+ * partition of a partitioned table. Avoid upgrading the cluster if it has a
+ * publication with publish_via_partition_root = true and has foreign table
+ * which is partition of a published table.
+ */
+static void
+check_for_valid_publication(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task;
+ const char *query;
+
+ if (GET_MAJOR_VERSION(cluster->major_version) >= 1900)
+ return;
+
+ prep_status("Checking for invalid publications");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "pubs_invalid.txt");
+
+ query = "SELECT p.oid, p.pubviaroot, p.puballtables, ft.ftrelid "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true;";
+
+ task = upgrade_task_create();
+ upgrade_task_add_step(task, query,
+ process_pub_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains publications, which has foreign table which are partitions of published table.\n"
+ "A list of potentially-affected publications is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/subscription/Makefile b/src/test/subscription/Makefile
index 50b65d8f6ea..11041f79667 100644
--- a/src/test/subscription/Makefile
+++ b/src/test/subscription/Makefile
@@ -14,6 +14,7 @@ top_builddir = ../../..
include $(top_builddir)/src/Makefile.global
EXTRA_INSTALL = contrib/hstore
+EXTRA_INSTALL += contrib/postgres_fdw
export with_icu
diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl
index 61b0cb4aa1a..41cfdc86580 100644
--- a/src/test/subscription/t/013_partition.pl
+++ b/src/test/subscription/t/013_partition.pl
@@ -8,6 +8,9 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $stdout;
+my $stderr;
+
# setup
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
@@ -891,4 +894,140 @@ $result = $node_subscriber2->safe_psql('postgres',
"SELECT a, b, c FROM tab5_1 ORDER BY 1");
is($result, qq(4||1), 'updates of tab5 replicated correctly');
+# Test with partitioned table with foreign table as its partition.
+$node_publisher->safe_psql('postgres', "CREATE DATABASE db1");
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE EXTENSION IF NOT EXISTS postgres_fdw;
+ CREATE SERVER fdw FOREIGN DATA WRAPPER postgres_fdw;
+ CREATE USER MAPPING FOR CURRENT_USER SERVER fdw;});
+
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;}
+);
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE PUBLICATION pub_foreign_1 FOR TABLE sc1.tab6 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_2 FOR TABLES IN SCHEMA sc1 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_3 FOR ALL TABLES with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_4 FOR TABLES IN SCHEMA sc2 with (publish_via_partition_root);
+ });
+
+$node_subscriber1->safe_psql('postgres', "CREATE DATABASE db1");
+$node_subscriber1->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15);}
+);
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=db1';
+
+# Create subscription on publication which has partitioned table with foreign
+# table as its partition.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLE on partitioned table with foreign table as its partition"
+);
+
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLES IN SCHEMA on partitioned table with foreign table as its partition"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_3;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR ALL TABLES on partitioned table with foreign table as its partition"
+);
+
+# If publication is created with publish_via_partition_root = false, we can
+# create subscription on it. But replication of partitioned table with foreign
+# is skipped
+$node_publisher->safe_psql('db1',
+ "ALTER PUBLICATION pub_foreign_1 SET (publish_via_partition_root = false)"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# if subscription is created with copy_data=false, we can create subscription
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2 WITH (copy_data=false)"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# Alter subscription to include publisher which has partitioned table with
+# foreign table as its partition.
+$node_subscriber1->safe_psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_4;"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 ADD PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Add publication which has partitioned table with foreign table as its partition to subscription"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 SET PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "set publication which has partitioned table with foreign table as its partition for subscription"
+);
+
+# Create foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;"
+);
+$node_publisher->safe_psql('db1', "DROP FOREIGN TABLE sc1.tab6_2_1");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot create foreign table "tab6_2_1" as a partition of "tab6_2"/,
+ "Create foreign table as a partition of partitioned table already part of publication"
+);
+
+# Attach foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+$node_publisher->safe_psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot attach foreign table "tab6_2_1" to partition table "tab6_2"/,
+ "Attach foreign table as a partition of already published partitioned table"
+);
+
done_testing();
--
2.34.1
On Mon, Jun 9, 2025 at 3:58 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 4 Jun 2025 at 16:12, Ajin Cherian <itsajin@gmail.com> wrote:
On Tue, May 20, 2025 at 2:33 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
This approach seems better to me. I have created a patch with the
above approach.Thanks and Regards,
Shlok KyalSome quick comments on the patch: 1. In doc/src/sgml/ref/create_subscription.sgml: + has partitioned table with foreign table as partition. If this scenario is + detected we ERROR is logged to the user. + </para> +Should be: "If this scenario is detected an ERROR is logged to the
user." (remove "we").In src/backend/commands/subscriptioncmds.c: 2. The comment header: + * This function is in charge of detecting if publisher with + * publish_via_partition_root=true publishes a partitioned table that has a + * foreign table as a partition.Add "and throw an error if found" at the end of that sentence to
correctly describe what the function does.3. + appendStringInfoString(&cmd, + "SELECT DISTINCT P.pubname AS pubname " + "from pg_catalog.pg_publication p, LATERAL " + "pg_get_publication_tables(p.pubname) gpt, LATERAL " + "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON " + "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");use FROM rather than from to maintain SQL style consistency.
4. + errdetail_plural("The subscription being created on a publication (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + "The subscription being created on publications (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + list_length(publist), pubnames->data),I think you meant "publish_via_partition_root" here and not
"publish_via_root_partition ".I have addressed all the comments and attached the updated patch.
Hi Shlok,
Some more comments:
1.
+ Replication is not supported for foreign tables. When used as partitions
+ of partitioned tables, publishing of the partitioned table is only allowed
+ if the <literal>publish_via_partition_root</literal> is set to
+ <literal>false</literal>.
In patch: "When used as partitions of partitioned tables, publishing
of the partitioned table is only allowed if the
<literal>publish_via_partition_root</literal> is set to
<literal>false</literal>.
Change to: When foreign tables are used as partitions of partitioned
tables, publishing of the partitioned table is only allowed if the
<literal>publish_via_partition_root</literal> is set to
<literal>false</literal>.
2.
+ <para>
+ When using a subscription parameter copy_data = true, corresponding
+ publications are checked if it has publish_via_partition_root = true and
+ has partitioned table with foreign table as partition. If this scenario is
+ detected an ERROR is logged to the user.
+ </para>
In patch: "When using a subscription parameter copy_data = true, ..."
Change to: "When using the subscription parameter copy_data = true, ..."
In patch: "and has partitioned table with foreign table as partition."
Change to: "and has a partitioned table with a foreign table as its partition"
3.
+ * check_publications_foreign_parts
+ * Check if the publications, on which subscriber is subscribing, publishes any
+ * partitioned table that has an foreign table as its partition and has
+ * publish_via_partition_root set as true. The check is performed only if
+ * copy_data is set as true for the subscription.
In patch: "publishes any partitioned table that has an foreign table
as its partition"
Change to: "publishes any partitioned table that has a foreign table
as its partition"
4.
+ * DML data changes are not published for data in foreign tables,
+ * and yet the tablesync worker is not smart enough to omit data from
+ * foreign tables when they are partitions of partitioned tables.
change to:"Although DML changes to foreign tables are excluded from
publication, the tablesync worker will still attempt to copy data from
foreign table partitions during initial table synchronization."
5.
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this check is not
+ * needed.
In patch: "so this check is not needed"
Change to: "so this function is not called for such tables"
6.
+ errdetail_plural("The subscription being created on a
publication (%s) with publish_via_partition_root = true and contains
partitioned tables with foreign table as partition ",
+ "The subscription being created on
publications (%s) with publish_via_partition_root = true and contains
partitioned tables with foreign table as partition ",
+ list_length(publist), pubnames->data),
Change to: "The subscription is for a publication (%s) with
publish_via_partition_root = true but one of the partitioned tables
has a foreign table as a partition"
"The subscription is for publications (%s) with
publish_via_partition_root = true but one of the partitioned tables
has a foreign table as a partition"
7.
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
Why do we need to do all this logic for non partition foreign tables?
Just directly throw an error for those tables and do this extra logic
only for partitioned tables.
8.
+ pg_fatal("Your installation contains publications, which has
foreign table which are partitions of published table.\n"
+ "A list of potentially-affected publications is in
the file:\n"
Change to: "Your installation contains publications, where one of the
partitioned tables has a foreign table as a partition.\n"
regards,
Ajin Cherian
Fujitsu Australia
On Tue, 1 Jul 2025 at 08:20, Ajin Cherian <itsajin@gmail.com> wrote:
On Mon, Jun 9, 2025 at 3:58 PM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
On Wed, 4 Jun 2025 at 16:12, Ajin Cherian <itsajin@gmail.com> wrote:
On Tue, May 20, 2025 at 2:33 AM Shlok Kyal <shlok.kyal.oss@gmail.com> wrote:
This approach seems better to me. I have created a patch with the
above approach.Thanks and Regards,
Shlok KyalSome quick comments on the patch: 1. In doc/src/sgml/ref/create_subscription.sgml: + has partitioned table with foreign table as partition. If this scenario is + detected we ERROR is logged to the user. + </para> +Should be: "If this scenario is detected an ERROR is logged to the
user." (remove "we").In src/backend/commands/subscriptioncmds.c: 2. The comment header: + * This function is in charge of detecting if publisher with + * publish_via_partition_root=true publishes a partitioned table that has a + * foreign table as a partition.Add "and throw an error if found" at the end of that sentence to
correctly describe what the function does.3. + appendStringInfoString(&cmd, + "SELECT DISTINCT P.pubname AS pubname " + "from pg_catalog.pg_publication p, LATERAL " + "pg_get_publication_tables(p.pubname) gpt, LATERAL " + "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON " + "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");use FROM rather than from to maintain SQL style consistency.
4. + errdetail_plural("The subscription being created on a publication (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + "The subscription being created on publications (%s) with publish_via_root_partition = true and contains partitioned tables with foreign table as partition ", + list_length(publist), pubnames->data),I think you meant "publish_via_partition_root" here and not
"publish_via_root_partition ".I have addressed all the comments and attached the updated patch.
Hi Shlok,
Some more comments: 1. + Replication is not supported for foreign tables. When used as partitions + of partitioned tables, publishing of the partitioned table is only allowed + if the <literal>publish_via_partition_root</literal> is set to + <literal>false</literal>.In patch: "When used as partitions of partitioned tables, publishing
of the partitioned table is only allowed if the
<literal>publish_via_partition_root</literal> is set to
<literal>false</literal>.Change to: When foreign tables are used as partitions of partitioned
tables, publishing of the partitioned table is only allowed if the
<literal>publish_via_partition_root</literal> is set to
<literal>false</literal>.
Fixed
2. + <para> + When using a subscription parameter copy_data = true, corresponding + publications are checked if it has publish_via_partition_root = true and + has partitioned table with foreign table as partition. If this scenario is + detected an ERROR is logged to the user. + </para>In patch: "When using a subscription parameter copy_data = true, ..."
Change to: "When using the subscription parameter copy_data = true, ..."In patch: "and has partitioned table with foreign table as partition."
Change to: "and has a partitioned table with a foreign table as its partition"
Fixed
3. + * check_publications_foreign_parts + * Check if the publications, on which subscriber is subscribing, publishes any + * partitioned table that has an foreign table as its partition and has + * publish_via_partition_root set as true. The check is performed only if + * copy_data is set as true for the subscription.In patch: "publishes any partitioned table that has an foreign table
as its partition"
Change to: "publishes any partitioned table that has a foreign table
as its partition"
Fixed
4. + * DML data changes are not published for data in foreign tables, + * and yet the tablesync worker is not smart enough to omit data from + * foreign tables when they are partitions of partitioned tables.change to:"Although DML changes to foreign tables are excluded from
publication, the tablesync worker will still attempt to copy data from
foreign table partitions during initial table synchronization."
Fixed
5. + * When publish_via_partition_root is false, each partition published for + * replication is listed individually in pg_subscription_rel, and we + * don't add partitions that are foreign tables, so this check is not + * needed.In patch: "so this check is not needed"
Change to: "so this function is not called for such tables"
Fixed
6. + errdetail_plural("The subscription being created on a publication (%s) with publish_via_partition_root = true and contains partitioned tables with foreign table as partition ", + "The subscription being created on publications (%s) with publish_via_partition_root = true and contains partitioned tables with foreign table as partition ", + list_length(publist), pubnames->data),Change to: "The subscription is for a publication (%s) with
publish_via_partition_root = true but one of the partitioned tables
has a foreign table as a partition"
"The subscription is for publications (%s) with
publish_via_partition_root = true but one of the partitioned tables
has a foreign table as a partition"
"one of the partitioned tables" this would be misleading as
publication can have multiple partitioned table which have foreign
table as its partition.
I have reworded it like:
"The subscription is for a publication (%s) with
publish_via_partition_root = true, but one or more partitioned tables
have foreign tables as partitions.",
"The subscription is for publications (%s) with
publish_via_partition_root = true but one or more partitioned tables
have foreign tables as partitions."
7. + /* capture all publications that include this relation directly */ + puboids = list_concat(puboids, GetRelationPublications(rel->rd_id)); + schemaid = RelationGetNamespace(rel); + puboids = list_concat(puboids, GetSchemaPublications(schemaid)); + + /* and do the same for its ancestors, if any */ + ancestors = get_partition_ancestors(rel->rd_id); + foreach_oid(ancestor, ancestors) + { + puboids = list_concat(puboids, GetRelationPublications(ancestor)); + schemaid = get_rel_namespace(ancestor); + puboids = list_concat(puboids, GetSchemaPublications(schemaid)); + } + + /* Now check the publish_via_partition_root bit for each of those */ + list_sort(puboids, list_oid_cmp); + list_deduplicate_oid(puboids); + foreach_oid(puboid, puboids)Why do we need to do all this logic for non partition foreign tables?
Just directly throw an error for those tables and do this extra logic
only for partitioned tables.
We need to throw an error if foreign table is being attached to a
partitioned table which is already published (or its parent is already
published) with publish_via_partition_root is true.
So, this logic is required to check if the table to which it is being
attached is published with publish_via_partition_root or not.
8. + pg_fatal("Your installation contains publications, which has foreign table which are partitions of published table.\n" + "A list of potentially-affected publications is in the file:\n"Change to: "Your installation contains publications, where one of the
partitioned tables has a foreign table as a partition.\n"
There can be mutiple partitioned table published. Reworded the error as:
"Your installation contains publications where one or more partitioned
tables have foreign tables as partitions.\n"
Thanks for reviewing the patch. I have addressed the comments and
attached the latest patch.
Thanks and Regards,
Shlok Kyal
Attachments:
v17-0001-Restrict-publishing-of-partitioned-table-with-fo.patchapplication/octet-stream; name=v17-0001-Restrict-publishing-of-partitioned-table-with-fo.patchDownload
From 9339ce3b0ec511f30e21613de12076b35cfb196d Mon Sep 17 00:00:00 2001
From: Shlok Kyal <shlok.kyal.oss@gmail.com>
Date: Mon, 19 May 2025 19:47:04 +0530
Subject: [PATCH v17] Restrict publishing of partitioned table with foreign
table as its partition
Logical replication of foreign table is not supported and we throw an
error in this case. But when we create a publication on a partitioned
table that has a foreign table as its partition, the initial sync of
such table is successful and we should avoid such cases.
Current Behaviour in HEAD, when publication is created:
1. with publish_via_partition_root = true
The root table is published, and initial data from partitions that are
foreign tables is replicated.
2. with publish_via_partition_root = false and FOR ALL TABLES
All leaf tables except partitions that are foreign tables are published.
3. with publish_via_partition_root = false and
FOR TABLE/ FOR TABLES IN SCHEMA
All leaf tables are published, including initial data from partitions
that are foreign tables.
With this patch we have following behaviour:
1. with publish_via_partition_root = true
An error is thrown when we try to create subscription (with copy_data = true)
on publication with partitioned table with foreign partition.
When copy_data = false, we donot need this check as initial sync is not
performed.
Also we restrict creating/attaching a foreign table as partition of a
partitioned table which is part of any publication.
2. with publish_via_partition_root = false
We skip publishing partitions that are foreign tables. This is done by
avoid adding such partitions in pg_subscription_rel catalog table.
We have introduced two functions 'RelationHasForeignPartition' and
'check_publications_foreign_parts'. In 'RelationHasForeignPartition' we go
through the child nodes of a partition and check if it has a foreign
table. In 'check_publications_foreign_parts' we check if a subscription
with copy_data = true is subscribing to a publication which has partitioned
table with foreign table as its partition.
---
doc/src/sgml/logical-replication.sgml | 10 +-
doc/src/sgml/ref/create_subscription.sgml | 7 ++
src/backend/catalog/pg_publication.c | 26 ++--
src/backend/commands/subscriptioncmds.c | 100 ++++++++++++++++
src/backend/commands/tablecmds.c | 110 +++++++++++++++++
src/backend/partitioning/partdesc.c | 32 +++++
src/bin/pg_upgrade/check.c | 89 ++++++++++++++
src/include/partitioning/partdesc.h | 1 +
src/test/subscription/Makefile | 1 +
src/test/subscription/t/013_partition.pl | 139 ++++++++++++++++++++++
10 files changed, 499 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml
index c32e6bc000d..275dd296f18 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -2079,10 +2079,18 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
<listitem>
<para>
- Replication is only supported by tables, including partitioned tables.
+ Replication is only supported for tables, including partitioned tables.
Attempts to replicate other types of relations, such as views, materialized
views, or foreign tables, will result in an error.
</para>
+ <para>
+ Replication is not supported for foreign tables. When foreign tables are
+ used as partitions of partitioned tables, publishing of the partitioned
+ table is only allowed if the <literal>publish_via_partition_root</literal>
+ is set to <literal>false</literal>. In this mode, changes to a partition
+ that is a foreign table are ignored for the purposes of replication, and
+ data contained in them is not included during initial synchronization.
+ </para>
</listitem>
<listitem>
diff --git a/doc/src/sgml/ref/create_subscription.sgml b/doc/src/sgml/ref/create_subscription.sgml
index 57dec28a5df..9019fe1e384 100644
--- a/doc/src/sgml/ref/create_subscription.sgml
+++ b/doc/src/sgml/ref/create_subscription.sgml
@@ -545,6 +545,13 @@ WHERE C.relnamespace = N.oid AND
PT.pubname IN (<pub-names>);
</programlisting></para>
+ <para>
+ When using the subscription parameter copy_data = true, corresponding
+ publications are checked if it has publish_via_partition_root = true and
+ has a partitioned table with a foreign table as its partition. If this
+ scenario is detected an ERROR is logged to the user.
+ </para>
+
</refsect1>
<refsect1>
diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c
index d6f94db5d99..56255dcd5cc 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -304,7 +304,7 @@ check_and_fetch_column_list(Publication *pub, Oid relid, MemoryContext mcxt,
/*
* Gets the relations based on the publication partition option for a specified
- * relation.
+ * relation. Foreign tables are not included.
*/
List *
GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
@@ -313,25 +313,21 @@ GetPubPartitionOptionRelations(List *result, PublicationPartOpt pub_partopt,
if (get_rel_relkind(relid) == RELKIND_PARTITIONED_TABLE &&
pub_partopt != PUBLICATION_PART_ROOT)
{
- List *all_parts = find_all_inheritors(relid, NoLock,
- NULL);
+ List *all_parts = find_all_inheritors(relid, NoLock, NULL);
- if (pub_partopt == PUBLICATION_PART_ALL)
- result = list_concat(result, all_parts);
- else if (pub_partopt == PUBLICATION_PART_LEAF)
+ foreach_oid(partOid, all_parts)
{
- ListCell *lc;
+ char relkind = get_rel_relkind(partOid);
- foreach(lc, all_parts)
- {
- Oid partOid = lfirst_oid(lc);
+ if (relkind == RELKIND_FOREIGN_TABLE)
+ continue;
- if (get_rel_relkind(partOid) != RELKIND_PARTITIONED_TABLE)
- result = lappend_oid(result, partOid);
- }
+ if (pub_partopt == PUBLICATION_PART_LEAF &&
+ relkind == RELKIND_PARTITIONED_TABLE)
+ continue;
+
+ result = lappend_oid(result, partOid);
}
- else
- Assert(false);
}
else
result = lappend_oid(result, relid);
diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c
index 4ff246cd943..83978b77d0c 100644
--- a/src/backend/commands/subscriptioncmds.c
+++ b/src/backend/commands/subscriptioncmds.c
@@ -112,6 +112,9 @@ static List *merge_publications(List *oldpublist, List *newpublist, bool addpub,
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
static void CheckAlterSubOption(Subscription *sub, const char *option,
bool slot_needs_update, bool isTopLevel);
+static void check_publications_foreign_parts(WalReceiverConn *wrconn,
+ List *publications, bool copydata,
+ char *subname);
/*
@@ -723,6 +726,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
check_publications(wrconn, publications);
check_publications_origin(wrconn, publications, opts.copy_data,
opts.origin, NULL, 0, stmt->subname);
+ check_publications_foreign_parts(wrconn, publications,
+ opts.copy_data, stmt->subname);
/*
* Set sync state based on if we were asked to do data copy or
@@ -883,6 +888,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
check_publications_origin(wrconn, sub->publications, copy_data,
sub->origin, subrel_local_oids,
subrel_count, sub->name);
+ check_publications_foreign_parts(wrconn, sub->publications, copy_data,
+ sub->name);
/*
* Rels that we want to remove from subscription and drop any slots
@@ -2507,3 +2514,96 @@ defGetStreamingMode(DefElem *def)
def->defname)));
return LOGICALREP_STREAM_OFF; /* keep compiler quiet */
}
+
+/*
+ * check_publications_foreign_parts
+ * Check if the publications, on which subscriber is subscribing, publishes any
+ * partitioned table that has a foreign table as its partition and has
+ * publish_via_partition_root set as true. The check is performed only if
+ * copy_data is set as true for the subscription.
+ *
+ * Although DML changes to foreign tables are excluded from publication, the
+ * tablesync worker will still attempt to copy data from foreign table
+ * partitions during initial table synchronization. To avoid the
+ * inconsistencies that would result, we disallow foreign tables from being
+ * published generally. However, it's possible for partitioned tables to have
+ * foreign tables as partitions, and we would like to allow publishing those
+ * partitioned tables so that the other partitions are replicated.
+ *
+ * This function is in charge of detecting if publisher with
+ * publish_via_partition_root=true publishes a partitioned table that has a
+ * foreign table as a partition and throw an error if found.
+ *
+ * When publish_via_partition_root is false, each partition published for
+ * replication is listed individually in pg_subscription_rel, and we
+ * don't add partitions that are foreign tables, so this function is not called
+ * for such tables.
+ */
+static void
+check_publications_foreign_parts(WalReceiverConn *wrconn, List *publications,
+ bool copydata, char *subname)
+{
+ WalRcvExecResult *res;
+ StringInfoData cmd;
+ TupleTableSlot *slot;
+ Oid tableRow[1] = {TEXTOID};
+ List *publist = NIL;
+ int i;
+
+ if (!copydata)
+ return;
+
+ initStringInfo(&cmd);
+ appendStringInfoString(&cmd,
+ "SELECT DISTINCT P.pubname AS pubname "
+ "FROM pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true AND p.pubname IN (");
+
+ GetPublicationsStr(publications, &cmd, true);
+ appendStringInfoString(&cmd, ")\n");
+
+ res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
+ pfree(cmd.data);
+
+ if (res->status != WALRCV_OK_TUPLES)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONNECTION_FAILURE),
+ errmsg("could not receive list of replicated tables from the publisher: %s",
+ res->err)));
+
+ /* Process tables. */
+ slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
+ while (tuplestore_gettupleslot(res->tuplestore, true, false, slot))
+ {
+ char *pubname;
+ bool isnull;
+
+ pubname = TextDatumGetCString(slot_getattr(slot, 1, &isnull));
+ Assert(!isnull);
+
+ ExecClearTuple(slot);
+ publist = list_append_unique(publist, makeString(pubname));
+ }
+
+ if (publist)
+ {
+ StringInfo pubnames = makeStringInfo();
+
+ /* Prepare the list of publication(s) for warning message. */
+ GetPublicationsStr(publist, pubnames, false);
+ ereport(ERROR,
+ errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("subscription \"%s\" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition",
+ subname),
+ errdetail_plural("The subscription is for a publication (%s) with publish_via_partition_root = true, but one or more partitioned tables have foreign tables as partitions.",
+ "The subscription is for publications (%s) with publish_via_partition_root = true but one or more partitioned tables have foreign tables as partitions.",
+ list_length(publist), pubnames->data),
+ errhint("Drop the foreign table from the publication or set publish_via_partition_root = false on publication or set copy_data = false."));
+ }
+
+ ExecDropSingleTupleTableSlot(slot);
+
+ walrcv_clear_result(res);
+}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b8837f26cb4..5859224900f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -1133,6 +1133,52 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
errmsg("\"%s\" is not partitioned",
RelationGetRelationName(parent))));
+ /*
+ * If we're creating a partition that's a foreign table, verify that
+ * the parent table is not in a publication with
+ * publish_via_partition_root enabled.
+ */
+ if (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(parent->rd_id));
+ schemaid = RelationGetNamespace(parent);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(parent->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub = GetPublication(puboid);
+
+ if (pub->pubviaroot)
+ ereport(ERROR,
+ errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot create foreign table \"%s\" as a partition of \"%s\"",
+ RelationGetRelationName(rel), RelationGetRelationName(parent)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(parent),
+ "publish_via_partition_root", pub->name));
+ }
+ }
+
/*
* The partition constraint of the default partition depends on the
* partition bounds of every other partition. It is possible that
@@ -20322,6 +20368,70 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("cannot attach temporary relation of another session as partition")));
+ /*
+ * If the relation to attach is a foreign table, or a partitioned table
+ * that contains a foreign table as partition, then verify that the parent
+ * table is not in a publication with publish_via_partition_root enabled.
+ */
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE ||
+ (attachrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
+ RelationHasForeignPartition(attachrel)))
+ {
+ Oid schemaid;
+ List *puboids;
+ List *ancestors;
+
+ /* Start with publications of all tables */
+ puboids = GetAllTablesPublications();
+
+ /* capture all publications that include this relation directly */
+ puboids = list_concat(puboids, GetRelationPublications(rel->rd_id));
+ schemaid = RelationGetNamespace(rel);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+
+ /* and do the same for its ancestors, if any */
+ ancestors = get_partition_ancestors(rel->rd_id);
+ foreach_oid(ancestor, ancestors)
+ {
+ puboids = list_concat(puboids, GetRelationPublications(ancestor));
+ schemaid = get_rel_namespace(ancestor);
+ puboids = list_concat(puboids, GetSchemaPublications(schemaid));
+ }
+
+ /* Now check the publish_via_partition_root bit for each of those */
+ list_sort(puboids, list_oid_cmp);
+ list_deduplicate_oid(puboids);
+ foreach_oid(puboid, puboids)
+ {
+ Publication *pub;
+
+ pub = GetPublication(puboid);
+ if (pub->pubviaroot)
+ {
+ if (attachrel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach foreign table \"%s\" to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("cannot attach table \"%s\" with a partition that's a foreign table to partition table \"%s\"",
+ RelationGetRelationName(attachrel),
+ RelationGetRelationName(rel)),
+ errdetail("Partitioned table \"%s\" is published with option \"%s\" in publication \"%s\".",
+ RelationGetRelationName(rel),
+ "publish_via_partition_root",
+ pub->name)));
+ }
+ }
+ }
+
/*
* Check if attachrel has any identity columns or any columns that aren't
* in the parent.
diff --git a/src/backend/partitioning/partdesc.c b/src/backend/partitioning/partdesc.c
index 328b4d450e4..56d176625e6 100644
--- a/src/backend/partitioning/partdesc.c
+++ b/src/backend/partitioning/partdesc.c
@@ -506,3 +506,35 @@ get_default_oid_from_partdesc(PartitionDesc partdesc)
return InvalidOid;
}
+
+/*
+ * Return true if the given partitioned table ultimately contains a
+ * partition that is a foreign table, false otherwise.
+ */
+bool
+RelationHasForeignPartition(Relation rel)
+{
+ PartitionDesc pd = RelationGetPartitionDesc(rel, true);
+
+ for (int i = 0; i < pd->nparts; i++)
+ {
+ if (pd->is_leaf[i])
+ {
+ if (get_rel_relkind(pd->oids[i]) == RELKIND_FOREIGN_TABLE)
+ return true;
+ }
+ else
+ {
+ Relation part;
+ bool ret;
+
+ part = table_open(pd->oids[i], NoLock);
+ ret = RelationHasForeignPartition(part);
+ table_close(part, NoLock);
+ if (ret)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/bin/pg_upgrade/check.c b/src/bin/pg_upgrade/check.c
index fb063a2de42..d2e58c4463a 100644
--- a/src/bin/pg_upgrade/check.c
+++ b/src/bin/pg_upgrade/check.c
@@ -31,6 +31,7 @@ static void check_new_cluster_logical_replication_slots(void);
static void check_new_cluster_subscription_configuration(void);
static void check_old_cluster_for_valid_slots(void);
static void check_old_cluster_subscription_state(void);
+static void check_for_valid_publication(ClusterInfo *cluster);
/*
* DataTypesUsageChecks - definitions of data type checks for the old cluster
@@ -634,6 +635,8 @@ check_and_dump_old_cluster(void)
check_old_cluster_subscription_state();
}
+ check_for_valid_publication(&old_cluster);
+
check_for_data_types_usage(&old_cluster);
/*
@@ -2273,3 +2276,89 @@ check_old_cluster_subscription_state(void)
else
check_ok();
}
+
+/*
+ * Callback function for processing results of query for
+ * check_for_valid_publication()'s UpgradeTask. If the query returned
+ * any rows (i.e., the check failed), write the details to the report file.
+ */
+static void
+process_pub_check(DbInfo *dbinfo, PGresult *res, void *arg)
+{
+ UpgradeTaskReport *report = (UpgradeTaskReport *) arg;
+ int ntups = PQntuples(res);
+ int i_puboid = PQfnumber(res, "oid");
+ int i_pubviaroot = PQfnumber(res, "pubviaroot");
+ int i_puballtables = PQfnumber(res, "puballtables");
+ int i_ftrelid = PQfnumber(res, "ftrelid");
+
+ if (ntups == 0)
+ return;
+
+ if (report->file == NULL &&
+ (report->file = fopen_priv(report->path, "w")) == NULL)
+ pg_fatal("could not open file \"%s\": %m", report->path);
+
+ fprintf(report->file, "In database: %s\n", dbinfo->db_name);
+
+ for (int rowno = 0; rowno < ntups; rowno++)
+ fprintf(report->file, "The publication with oid: %s, has foreign table with oid: %s as partition of a published table. "
+ "Publication is published with publish_via_partition_root = %s and it is published %s.\n",
+ PQgetvalue(res, rowno, i_puboid),
+ PQgetvalue(res, rowno, i_ftrelid),
+ strcmp(PQgetvalue(res, rowno, i_pubviaroot), "f") ? "true" : "false",
+ strcmp(PQgetvalue(res, rowno, i_puballtables), "f") ? "FOR ALL TABLES" : "FOR TABLE/FOR TABLES IN SCHEMA");
+}
+
+/*
+ * check_for_valid_publication
+ *
+ * Verify if the publication has any published foreign table which are
+ * partitions of a table.
+ *
+ * For PG 18 and below foreign tables can be indirectly published when it is
+ * partition of a partitioned table. Avoid upgrading the cluster if it has a
+ * publication with publish_via_partition_root = true and has foreign table
+ * which is partition of a published table.
+ */
+static void
+check_for_valid_publication(ClusterInfo *cluster)
+{
+ UpgradeTaskReport report;
+ UpgradeTask *task;
+ const char *query;
+
+ if (GET_MAJOR_VERSION(cluster->major_version) >= 1900)
+ return;
+
+ prep_status("Checking for invalid publications");
+
+ report.file = NULL;
+ snprintf(report.path, sizeof(report.path), "%s/%s",
+ log_opts.basedir,
+ "pubs_invalid.txt");
+
+ query = "SELECT p.oid, p.pubviaroot, p.puballtables, ft.ftrelid "
+ "from pg_catalog.pg_publication p, LATERAL "
+ "pg_get_publication_tables(p.pubname) gpt, LATERAL "
+ "pg_partition_tree(gpt.relid) gt JOIN pg_catalog.pg_foreign_table ft ON "
+ "ft.ftrelid = gt.relid WHERE p.pubviaroot = true;";
+
+ task = upgrade_task_create();
+ upgrade_task_add_step(task, query,
+ process_pub_check,
+ true, &report);
+ upgrade_task_run(task, cluster);
+ upgrade_task_free(task);
+
+ if (report.file)
+ {
+ fclose(report.file);
+ pg_log(PG_REPORT, "fatal");
+ pg_fatal("Your installation contains publications where one or more partitioned tables have foreign tables as partitions.\n"
+ "A list of potentially-affected publications is in the file:\n"
+ " %s", report.path);
+ }
+ else
+ check_ok();
+}
diff --git a/src/include/partitioning/partdesc.h b/src/include/partitioning/partdesc.h
index 34533f7004c..5fbafdc06f9 100644
--- a/src/include/partitioning/partdesc.h
+++ b/src/include/partitioning/partdesc.h
@@ -71,5 +71,6 @@ extern PartitionDesc PartitionDirectoryLookup(PartitionDirectory, Relation);
extern void DestroyPartitionDirectory(PartitionDirectory pdir);
extern Oid get_default_oid_from_partdesc(PartitionDesc partdesc);
+extern bool RelationHasForeignPartition(Relation rel);
#endif /* PARTDESC_H */
diff --git a/src/test/subscription/Makefile b/src/test/subscription/Makefile
index 50b65d8f6ea..11041f79667 100644
--- a/src/test/subscription/Makefile
+++ b/src/test/subscription/Makefile
@@ -14,6 +14,7 @@ top_builddir = ../../..
include $(top_builddir)/src/Makefile.global
EXTRA_INSTALL = contrib/hstore
+EXTRA_INSTALL += contrib/postgres_fdw
export with_icu
diff --git a/src/test/subscription/t/013_partition.pl b/src/test/subscription/t/013_partition.pl
index 4f78dd48815..466473bd387 100644
--- a/src/test/subscription/t/013_partition.pl
+++ b/src/test/subscription/t/013_partition.pl
@@ -8,6 +8,9 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
+my $stdout;
+my $stderr;
+
# setup
my $node_publisher = PostgreSQL::Test::Cluster->new('publisher');
@@ -890,4 +893,140 @@ $result = $node_subscriber2->safe_psql('postgres',
"SELECT a, b, c FROM tab5_1 ORDER BY 1");
is($result, qq(4||1), 'updates of tab5 replicated correctly');
+# Test with partitioned table with foreign table as its partition.
+$node_publisher->safe_psql('postgres', "CREATE DATABASE db1");
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE EXTENSION IF NOT EXISTS postgres_fdw;
+ CREATE SERVER fdw FOREIGN DATA WRAPPER postgres_fdw;
+ CREATE USER MAPPING FOR CURRENT_USER SERVER fdw;});
+
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;}
+);
+$node_publisher->safe_psql(
+ 'db1',
+ q{CREATE PUBLICATION pub_foreign_1 FOR TABLE sc1.tab6 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_2 FOR TABLES IN SCHEMA sc1 with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_3 FOR ALL TABLES with (publish_via_partition_root);
+ CREATE PUBLICATION pub_foreign_4 FOR TABLES IN SCHEMA sc2 with (publish_via_partition_root);
+ });
+
+$node_subscriber1->safe_psql('postgres', "CREATE DATABASE db1");
+$node_subscriber1->safe_psql(
+ 'db1',
+ q{CREATE SCHEMA sc1;
+ CREATE SCHEMA sc2;
+ CREATE TABLE sc1.tab6(id int) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_1 PARTITION OF sc1.tab6 FOR VALUES FROM (0) TO (5);
+ CREATE TABLE sc1.tab6_2 PARTITION OF sc1.tab6 FOR VALUES FROM (5) TO (15) PARTITION BY RANGE(id);
+ CREATE TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15);}
+);
+
+$publisher_connstr = $node_publisher->connstr . ' dbname=db1';
+
+# Create subscription on publication which has partitioned table with foreign
+# table as its partition.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLE on partitioned table with foreign table as its partition"
+);
+
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR TABLES IN SCHEMA on partitioned table with foreign table as its partition"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_3;"
+);
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Create subscription on a publication FOR ALL TABLES on partitioned table with foreign table as its partition"
+);
+
+# If publication is created with publish_via_partition_root = false, we can
+# create subscription on it. But replication of partitioned table with foreign
+# is skipped
+$node_publisher->safe_psql('db1',
+ "ALTER PUBLICATION pub_foreign_1 SET (publish_via_partition_root = false)"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_1;"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# if subscription is created with copy_data=false, we can create subscription
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_2 WITH (copy_data=false)"
+);
+$node_subscriber1->safe_psql('db1', 'DROP SUBSCRIPTION sub_foreign_1');
+
+# Alter subscription to include publisher which has partitioned table with
+# foreign table as its partition.
+$node_subscriber1->safe_psql('db1',
+ "CREATE SUBSCRIPTION sub_foreign_1 CONNECTION '$publisher_connstr' PUBLICATION pub_foreign_4;"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 ADD PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "Add publication which has partitioned table with foreign table as its partition to subscription"
+);
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER SUBSCRIPTION sub_foreign_1 SET PUBLICATION pub_foreign_2;");
+like(
+ $stderr,
+ qr/ERROR: subscription "sub_foreign_1" with copy_data = true cannot subscribe to a publication with publish_via_partition_root = true and publishes partitioned table with foreign table as partition/,
+ "set publication which has partitioned table with foreign table as its partition for subscription"
+);
+
+# Create foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw;"
+);
+$node_publisher->safe_psql('db1', "DROP FOREIGN TABLE sc1.tab6_2_1");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 PARTITION OF sc1.tab6_2 FOR VALUES FROM (10) TO (15) SERVER fdw"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot create foreign table "tab6_2_1" as a partition of "tab6_2"/,
+ "Create foreign table as a partition of partitioned table already part of publication"
+);
+
+# Attach foreign table as a partition of partitioned table already part of
+# publication.
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_subscriber1->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+$node_publisher->safe_psql('db1',
+ "CREATE FOREIGN TABLE sc1.tab6_2_1 (id int) SERVER fdw");
+($result, $stdout, $stderr) = $node_publisher->psql('db1',
+ "ALTER TABLE sc1.tab6_2 ATTACH PARTITION sc1.tab6_2_1 FOR VALUES FROM (10) TO (15)"
+);
+like(
+ $stderr,
+ qr/ERROR: cannot attach foreign table "tab6_2_1" to partition table "tab6_2"/,
+ "Attach foreign table as a partition of already published partitioned table"
+);
+
done_testing();
--
2.34.1