From 757ece6bf5086ccd365b5d6b10b1918a352c1b95 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Thu, 10 Sep 2026 09:54:03 +0530 Subject: [PATCH v4] Prevent unlogged tables in publication EXCEPT clauses Reject changing a table to UNLOGGED when it is referenced by a publication's EXCEPT clause. This keeps the catalog state consistent with the restriction that unlogged tables cannot be specified in publication EXCEPT clauses and ensures that pg_dump can recreate the publication. Author: Vignesh C Reviewed-by: Amit Kapila Reviewed-by: Chao Li Reviewed-by: shveta malik Reviewed-by: Hayato Kuroda Discussion: https://postgr.es/m/CALDaNm1r2MkGu6h8zgU1Kj1sX-FcMQ7wGTeLSnhx-5joiyXEvg@mail.gmail.com Backpatch-through: 19, where it was introduced --- src/backend/catalog/pg_publication.c | 35 +++++++++++++++++++++++ src/backend/commands/tablecmds.c | 33 +++++++++++++++------ src/include/catalog/pg_publication.h | 1 + src/test/regress/expected/publication.out | 10 +++++++ src/test/regress/sql/publication.sql | 8 ++++++ 5 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index 12af7d15536..7516b0107e7 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -928,6 +928,41 @@ GetRelationExcludedPublications(Oid relid) return get_relation_publications(relid, true); } +/* + * Check whether the relation is referenced by any publication. + * + * Returns true if the relation has a pg_publication_rel entry, and sets + * *isexcept according to whether that entry names the relation in the + * publication's EXCEPT clause rather than including it in the publication. + * + * This examines only the first entry found, so it avoids building a list of + * publication oids when the caller only needs to know whether the relation is + * referenced and in which way. + */ +bool +RelationHasPublication(Oid relid, bool *isexcept) +{ + CatCList *pubrellist; + bool found = false; + + /* Every entry in this list is for relid, so the first one will do. */ + pubrellist = SearchSysCacheList1(PUBLICATIONRELMAP, + ObjectIdGetDatum(relid)); + if (pubrellist->n_members > 0) + { + HeapTuple tup = &pubrellist->members[0]->tuple; + Form_pg_publication_rel pubrel; + + pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); + *isexcept = pubrel->prexcept; + found = true; + } + + ReleaseSysCacheList(pubrellist); + + return found; +} + /* * Internal function to get the list of relation oids for a publication. * diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 8dc70bfa0f1..89ea97b541a 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -19541,16 +19541,31 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged) } /* - * Check that the table is not part of any publication when changing to - * UNLOGGED, as UNLOGGED tables can't be published. + * UNLOGGED tables cannot be published, and they are not allowed in a + * publication's EXCEPT clause either, so reject the change if the table + * is referenced by a publication in either way. */ - if (!toLogged && - GetRelationIncludedPublications(RelationGetRelid(rel)) != NIL) - ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("cannot change table \"%s\" to unlogged because it is part of a publication", - RelationGetRelationName(rel)), - errdetail("Unlogged relations cannot be replicated."))); + if (!toLogged) + { + bool isexcept; + + if (RelationHasPublication(RelationGetRelid(rel), &isexcept)) + { + if (isexcept) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change table \"%s\" to unlogged because it is referenced in a publication EXCEPT clause", + RelationGetRelationName(rel)), + errdetail("Unlogged relations cannot be specified in a publication EXCEPT clause."), + errhint("Remove the table from the EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES first."))); + else + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("cannot change table \"%s\" to unlogged because it is part of a publication", + RelationGetRelationName(rel)), + errdetail("Unlogged relations cannot be replicated."))); + } + } /* * Check existing foreign key constraints to preserve the invariant that diff --git a/src/include/catalog/pg_publication.h b/src/include/catalog/pg_publication.h index 89b4bb14f62..2d62972976e 100644 --- a/src/include/catalog/pg_publication.h +++ b/src/include/catalog/pg_publication.h @@ -157,6 +157,7 @@ extern Publication *GetPublication(Oid pubid); extern Publication *GetPublicationByName(const char *pubname, bool missing_ok); extern List *GetRelationIncludedPublications(Oid relid); extern List *GetRelationExcludedPublications(Oid relid); +extern bool RelationHasPublication(Oid relid, bool *isexcept); /*--------- * Expected values for pub_partopt parameter of diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index 4f21462cc17..a83f0ccdcb8 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -279,6 +279,16 @@ CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (test ERROR: syntax error at or near "testpub_tbl1" LINE 1: ..._foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tb... ^ +-- A table in an EXCEPT clause cannot be changed to UNLOGGED. +CREATE TABLE testpub_exc_unlogged_tbl (a int); +CREATE PUBLICATION testpub_exc_unlogged FOR ALL TABLES EXCEPT (TABLE testpub_exc_unlogged_tbl); +-- fail - the table is referenced in a publication EXCEPT clause +ALTER TABLE testpub_exc_unlogged_tbl SET UNLOGGED; +ERROR: cannot change table "testpub_exc_unlogged_tbl" to unlogged because it is referenced in a publication EXCEPT clause +DETAIL: Unlogged relations cannot be specified in a publication EXCEPT clause. +HINT: Remove the table from the EXCEPT clause using ALTER PUBLICATION ... SET ALL TABLES first. +DROP PUBLICATION testpub_exc_unlogged; +DROP TABLE testpub_exc_unlogged_tbl; --------------------------------------------- -- SET ALL TABLES/SEQUENCES --------------------------------------------- diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index fac54b02e27..80c244c9138 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -126,6 +126,14 @@ CREATE PUBLICATION testpub_foralltables_excepttable1 FOR ALL TABLES EXCEPT (TABL -- fail - first table in the EXCEPT list should use TABLE keyword CREATE PUBLICATION testpub_foralltables_excepttable2 FOR ALL TABLES EXCEPT (testpub_tbl1, testpub_tbl2); +-- A table in an EXCEPT clause cannot be changed to UNLOGGED. +CREATE TABLE testpub_exc_unlogged_tbl (a int); +CREATE PUBLICATION testpub_exc_unlogged FOR ALL TABLES EXCEPT (TABLE testpub_exc_unlogged_tbl); +-- fail - the table is referenced in a publication EXCEPT clause +ALTER TABLE testpub_exc_unlogged_tbl SET UNLOGGED; +DROP PUBLICATION testpub_exc_unlogged; +DROP TABLE testpub_exc_unlogged_tbl; + --------------------------------------------- -- SET ALL TABLES/SEQUENCES --------------------------------------------- -- 2.55.0