From acb7ddbb4a53776c9451a210bed3d53781c81e22 Mon Sep 17 00:00:00 2001 From: Shlok Kyal Date: Mon, 1 Dec 2025 16:17:53 +0530 Subject: [PATCH v29 2/4] Support ADD ALL TABLES in ALTER PUBLICATION This patch adds support for using ADD ALL TABLES in ALTER PUBLICATION, allowing an existing publication to be changed into an ALL TABLES publication. This command is permitted only when the publication have no tables or schemas explicitly added and its ALL TABLES flag is not set. Usage: ALTER PUBLICATION pub1 ADD ALL TABLES --- doc/src/sgml/logical-replication.sgml | 10 +-- doc/src/sgml/ref/alter_publication.sgml | 14 +++- src/backend/commands/publicationcmds.c | 83 +++++++++++++++++++++-- src/backend/parser/gram.y | 10 +++ src/bin/psql/tab-complete.in.c | 2 +- src/include/nodes/parsenodes.h | 1 + src/test/regress/expected/publication.out | 62 +++++++++++++++++ src/test/regress/sql/publication.sql | 42 ++++++++++++ 8 files changed, 214 insertions(+), 10 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index aa013f348d4..c420469feaa 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -2550,10 +2550,12 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER - To add tables to a publication, the user must have ownership rights on the - table. To add all tables in schema to a publication, the user must be a - superuser. To create a publication that publishes all tables, all tables in - schema, or all sequences automatically, the user must be a superuser. + To create a publication using FOR ALL TABLES, + FOR ALL SEQUENCES or + FOR TABLES IN SCHEMA, the user must be a superuser. To add + ALL TABLES or TABLES IN SCHEMA to a + publication, the user must be a superuser. To add tables to a publication, + the user must have ownership rights on the table. diff --git a/doc/src/sgml/ref/alter_publication.sgml b/doc/src/sgml/ref/alter_publication.sgml index 7d7e6341921..0ab2a9d007e 100644 --- a/doc/src/sgml/ref/alter_publication.sgml +++ b/doc/src/sgml/ref/alter_publication.sgml @@ -24,6 +24,7 @@ PostgreSQL documentation ALTER PUBLICATION name ADD publication_object [, ...] ALTER PUBLICATION name SET publication_object [, ...] ALTER PUBLICATION name DROP publication_drop_object [, ...] +ALTER PUBLICATION name ADD ALL TABLES ALTER PUBLICATION name SET ( publication_parameter [= value] [, ... ] ) ALTER PUBLICATION name OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER | SESSION_USER } ALTER PUBLICATION name RENAME TO new_name @@ -91,6 +92,16 @@ ALTER PUBLICATION name RESET + + ADD ALL TABLES + + + This form adds all tables to the publication. This requires the + publication to not have any existing table or schema list. + + + + SET ( publication_parameter [= value] [, ... ] ) @@ -157,7 +168,8 @@ ALTER PUBLICATION name RESET You must own the publication to use ALTER PUBLICATION. Adding a table to a publication additionally requires owning that table. - The ADD TABLES IN SCHEMA, + The ADD ALL TABLES, + ADD TABLES IN SCHEMA, SET TABLES IN SCHEMA to a publication and RESET of publication requires the invoking user to be a superuser. To alter the owner, you must be able to diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c index 4f8342f721c..7a9020ad43f 100644 --- a/src/backend/commands/publicationcmds.c +++ b/src/backend/commands/publicationcmds.c @@ -1325,6 +1325,79 @@ AlterPublicationReset(ParseState *pstate, AlterPublicationStmt *stmt, CloseTableList(rels); } +/* + * Check whether we can alter the publication to add ALL TABLES. + * + * It is not allowed if the publication already is defined as ALL TABLES, or + * if there are any schemas or tables associated with the publication. + */ +static void +CheckAlterPublicationAllTables(HeapTuple tup) +{ + List *pubobjs; + Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); + + if (pubform->puballtables) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" is already defined as FOR ALL TABLES", + NameStr(pubform->pubname))); + + pubobjs = GetPublicationSchemas(pubform->oid); + if (list_length(pubobjs)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" has schemas associated with it", + NameStr(pubform->pubname)), + errdetail("ALL TABLES cannot be added when schemas are associated with the publication.")); + + pubobjs = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT); + if (list_length(pubobjs)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("publication \"%s\" has tables associated with it", + NameStr(pubform->pubname)), + errdetail("ALL TABLES cannot be added when tables are associated with the publication.")); +} + +/* + * Set publication to publish all tables. + */ +static void +AlterPublicationSetAllTables(Relation rel, HeapTuple tup) +{ + Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); + bool nulls[Natts_pg_publication]; + bool replaces[Natts_pg_publication]; + Datum values[Natts_pg_publication]; + + /* Add ALL TABLES to the publication requires superuser */ + if (!superuser()) + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to ADD ALL TABLES to the publication")); + + /* Lock the publication so nobody else can do anything with it. */ + LockDatabaseObject(PublicationRelationId, pubform->oid, 0, + AccessExclusiveLock); + + CheckAlterPublicationAllTables(tup); + + memset(values, 0, sizeof(values)); + memset(nulls, false, sizeof(nulls)); + memset(replaces, false, sizeof(replaces)); + + /* Set ALL TABLES flag */ + values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(true); + replaces[Anum_pg_publication_puballtables - 1] = true; + + tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, + replaces); + + /* Update the catalog. */ + CatalogTupleUpdate(rel, &tup->t_self, tup); +} + /* * Add or remove table to/from publication. */ @@ -1667,6 +1740,9 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION, stmt->pubname); + if (stmt->for_all_tables) + AlterPublicationSetAllTables(rel, tup); + if (stmt->options) AlterPublicationOptions(pstate, stmt, rel, tup); else if (stmt->action == AP_Reset) @@ -1680,10 +1756,7 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, &schemaidlist); - CheckAlterPublication(stmt, tup, relations, schemaidlist); - heap_freetuple(tup); - /* Lock the publication so nobody else can do anything with it. */ LockDatabaseObject(PublicationRelationId, pubid, 0, AccessExclusiveLock); @@ -1692,7 +1765,7 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) * It is possible that by the time we acquire the lock on publication, * concurrent DDL has removed it. We can test this by checking the * existence of publication. We get the tuple again to avoid the risk - * of any publication option getting changed. + * of any publication option or ALL TABLES flag getting changed. */ tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid)); if (!HeapTupleIsValid(tup)) @@ -1701,6 +1774,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) errmsg("publication \"%s\" does not exist", stmt->pubname)); + CheckAlterPublication(stmt, tup, relations, schemaidlist); + AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext, schemaidlist != NIL); AlterPublicationSchemas(stmt, tup, schemaidlist); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index a8b9ae6182d..9d648ccb47b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -10913,6 +10913,8 @@ pub_all_obj_type_list: PublicationAllObjSpec * TABLE table_name [, ...] * TABLES IN SCHEMA schema_name [, ...] * + * ALTER PUBLICATION name ADD ALL TABLES + * * ALTER PUBLICATION name RESET * *****************************************************************************/ @@ -10956,6 +10958,14 @@ AlterPublicationStmt: n->action = AP_DropObjects; $$ = (Node *) n; } + | ALTER PUBLICATION name ADD_P ALL TABLES + { + AlterPublicationStmt *n = makeNode(AlterPublicationStmt); + n->pubname = $3; + n->for_all_tables = true; + n->action = AP_AddObjects; + $$ = (Node *)n; + } | ALTER PUBLICATION name RESET { AlterPublicationStmt *n = makeNode(AlterPublicationStmt); diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c index 83599de2225..b1175e0c08b 100644 --- a/src/bin/psql/tab-complete.in.c +++ b/src/bin/psql/tab-complete.in.c @@ -2292,7 +2292,7 @@ match_previous_words(int pattern_id, COMPLETE_WITH("ADD", "DROP", "OWNER TO", "RENAME TO", "RESET", "SET"); /* ALTER PUBLICATION ADD */ else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD")) - COMPLETE_WITH("TABLES IN SCHEMA", "TABLE"); + COMPLETE_WITH("ALL TABLES", "TABLES IN SCHEMA", "TABLE"); else if (Matches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE")) COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); else if (HeadMatches("ALTER", "PUBLICATION", MatchAny, "ADD|SET", "TABLE") && diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8cf75724a7b..c22d75e80a2 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -4342,6 +4342,7 @@ typedef struct AlterPublicationStmt * objects. */ List *pubobjects; /* Optional list of publication objects */ + bool for_all_tables; /* Special publication for all tables in db */ AlterPublicationAction action; /* What action to perform with the given * objects */ } AlterPublicationStmt; diff --git a/src/test/regress/expected/publication.out b/src/test/regress/expected/publication.out index ce5b3b649d5..ec12f7cfbaa 100644 --- a/src/test/regress/expected/publication.out +++ b/src/test/regress/expected/publication.out @@ -2064,6 +2064,68 @@ ALTER PUBLICATION testpub_reset RESET; regress_publication_user | f | f | t | t | t | t | none | f (1 row) +-- ====================================================== +-- Tests for ALTER PUBLICATION ... ADD ALL TABLES +-- Verify that only superuser can ADD ALL TABLES +ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2; +SET ROLE regress_publication_user2; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser +ERROR: must be superuser to ADD ALL TABLES to the publication +ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user; +SET ROLE regress_publication_user; +-- Can add ALL TABLES to an empty publication +DROP PUBLICATION testpub_reset; +CREATE PUBLICATION testpub_reset; +WARNING: "wal_level" is insufficient to publish logical changes +HINT: Set "wal_level" to "logical" before creating subscriptions. +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + Publication testpub_reset + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + regress_publication_user | t | f | t | t | t | t | none | f +(1 row) + +-- Can't add ALL TABLES to 'ALL TABLES' publication +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +ERROR: publication "testpub_reset" is already defined as FOR ALL TABLES +-- Can add ALL TABLES to ALL SEQUENCES publication +DROP PUBLICATION testpub_reset; +CREATE PUBLICATION testpub_reset for ALL SEQUENCES; +WARNING: "wal_level" is insufficient to publish logical changes +HINT: Set "wal_level" to "logical" before creating subscriptions. +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + Publication testpub_reset + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + regress_publication_user | t | t | t | t | t | t | none | f +(1 row) + +-- Can't add ALL TABLES to 'FOR TABLE' publication +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +ERROR: publication "testpub_reset" has tables associated with it +DETAIL: ALL TABLES cannot be added when tables are associated with the publication. +-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +ERROR: publication "testpub_reset" has schemas associated with it +DETAIL: ALL TABLES cannot be added when schemas are associated with the publication. +-- Can add ALL TABLES when the 'publish', 'publish_via_partition_root', +-- 'publish_generated_columns' parameters does not have default value +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset SET (publish = '', publish_via_partition_root = 'true', publish_generated_columns = stored); +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + Publication testpub_reset + Owner | All tables | All sequences | Inserts | Updates | Deletes | Truncates | Generated columns | Via root +--------------------------+------------+---------------+---------+---------+---------+-----------+-------------------+---------- + regress_publication_user | t | f | f | f | f | f | stored | t +(1 row) + DROP PUBLICATION testpub_reset; DROP TABLE pub_sch1.tbl1; DROP SCHEMA pub_sch1; diff --git a/src/test/regress/sql/publication.sql b/src/test/regress/sql/publication.sql index f0432f67b4a..5259331137b 100644 --- a/src/test/regress/sql/publication.sql +++ b/src/test/regress/sql/publication.sql @@ -1299,6 +1299,48 @@ ALTER PUBLICATION testpub_reset SET (publish_generated_columns = stored); ALTER PUBLICATION testpub_reset RESET; \dRp+ testpub_reset +-- ====================================================== + +-- Tests for ALTER PUBLICATION ... ADD ALL TABLES +-- Verify that only superuser can ADD ALL TABLES +ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user2; +SET ROLE regress_publication_user2; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; -- fail - must be superuser +ALTER PUBLICATION testpub_reset OWNER TO regress_publication_user; +SET ROLE regress_publication_user; + +-- Can add ALL TABLES to an empty publication +DROP PUBLICATION testpub_reset; +CREATE PUBLICATION testpub_reset; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + +-- Can't add ALL TABLES to 'ALL TABLES' publication +ALTER PUBLICATION testpub_reset ADD ALL TABLES; + +-- Can add ALL TABLES to ALL SEQUENCES publication +DROP PUBLICATION testpub_reset; +CREATE PUBLICATION testpub_reset for ALL SEQUENCES; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + +-- Can't add ALL TABLES to 'FOR TABLE' publication +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset ADD TABLE pub_sch1.tbl1; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; + +-- Can't add ALL TABLES to 'TABLES IN SCHEMA' publication +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset ADD TABLES IN SCHEMA public; +ALTER PUBLICATION testpub_reset ADD ALL TABLES; + +-- Can add ALL TABLES when the 'publish', 'publish_via_partition_root', +-- 'publish_generated_columns' parameters does not have default value +ALTER PUBLICATION testpub_reset RESET; +ALTER PUBLICATION testpub_reset SET (publish = '', publish_via_partition_root = 'true', publish_generated_columns = stored); +ALTER PUBLICATION testpub_reset ADD ALL TABLES; +\dRp+ testpub_reset + DROP PUBLICATION testpub_reset; DROP TABLE pub_sch1.tbl1; DROP SCHEMA pub_sch1; -- 2.34.1