From cf223decbd3f86ca28ea5087ce9f50d7ee46bf04 Mon Sep 17 00:00:00 2001
From: Fujii Masao <fujii@postgresql.org>
Date: Fri, 24 Jul 2026 00:30:07 +0900
Subject: [PATCH v3 1/3] Collect ALTER PUBLICATION mapping drops for event
 triggers

ALTER PUBLICATION is expected to emit its own ddl_command_end entry
for event triggers. Previously, however, commands that only removed
publication membership entries did not do so.

As a result, sql_drop reported the removed membership entries, but
pg_event_trigger_ddl_commands() did not report the corresponding
ALTER PUBLICATION command. This affected
ALTER PUBLICATION ... DROP TABLE(S), DROP TABLES IN SCHEMA,
and SET forms whose net effect was only to remove publication memberships.

Fix this by collecting the publication itself as the
ALTER PUBLICATION command whenever the command removes table or
schema membership entries, while preserving the existing per-membership
entries for additions.

As a side-effect of this fix, SET commands that both add and remove memberships
may now produce both a publication-level ALTER PUBLICATION entry and
the existing per-membership addition entries. This is intentional, as
they represent different parts of the command.

Backpatch to all supported versions.
---
 src/backend/commands/publicationcmds.c      | 56 +++++++++++++++------
 src/test/regress/expected/event_trigger.out | 24 +++++++++
 src/test/regress/sql/event_trigger.sql      | 20 ++++++++
 3 files changed, 85 insertions(+), 15 deletions(-)

diff --git a/src/backend/commands/publicationcmds.c b/src/backend/commands/publicationcmds.c
index 96838730fe1..ebb7750ee30 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -66,10 +66,10 @@ static void CloseTableList(List *rels);
 static void LockSchemaList(List *schemalist);
 static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 								 AlterPublicationStmt *stmt);
-static void PublicationDropTables(Oid pubid, List *rels, bool missing_ok);
+static bool PublicationDropTables(Oid pubid, List *rels, bool missing_ok);
 static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists,
 								  AlterPublicationStmt *stmt);
-static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok);
+static bool PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok);
 static char defGetGeneratedColsOption(DefElem *def);
 
 
@@ -1238,7 +1238,7 @@ InvalidatePublicationRels(List *relids)
 /*
  * Add or remove table to/from publication.
  */
-static void
+static bool
 AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 					   List *tables, const char *queryString,
 					   bool publish_schema)
@@ -1246,6 +1246,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	List	   *rels = NIL;
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
 	Oid			pubid = pubform->oid;
+	bool		dropped = false;
 
 	/*
 	 * Nothing to do if no objects, except in SET: for that it is quite
@@ -1253,7 +1254,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	 * to remove all the existing tables.
 	 */
 	if (!tables && stmt->action != AP_SetObjects)
-		return;
+		return false;
 
 	rels = OpenTableList(tables);
 
@@ -1269,7 +1270,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		PublicationAddTables(pubid, rels, false, stmt);
 	}
 	else if (stmt->action == AP_DropObjects)
-		PublicationDropTables(pubid, rels, false);
+		dropped = PublicationDropTables(pubid, rels, false);
 	else						/* AP_SetObjects */
 	{
 		List	   *oldrelids = NIL;
@@ -1404,7 +1405,7 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 		}
 
 		/* And drop them. */
-		PublicationDropTables(pubid, delrels, true);
+		dropped = PublicationDropTables(pubid, delrels, true);
 
 		/*
 		 * Don't bother calculating the difference for adding, we'll catch and
@@ -1416,6 +1417,8 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
 	}
 
 	CloseTableList(rels);
+
+	return dropped;
 }
 
 /*
@@ -1423,11 +1426,12 @@ AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup,
  *
  * Add or remove schemas to/from publication.
  */
-static void
+static bool
 AlterPublicationSchemas(AlterPublicationStmt *stmt,
 						HeapTuple tup, List *schemaidlist)
 {
 	Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup);
+	bool		dropped = false;
 
 	/*
 	 * Nothing to do if no objects, except in SET: for that it is quite
@@ -1435,7 +1439,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 	 * to remove all the existing schemas.
 	 */
 	if (!schemaidlist && stmt->action != AP_SetObjects)
-		return;
+		return false;
 
 	/*
 	 * Schema lock is held until the publication is altered to prevent
@@ -1478,7 +1482,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		PublicationAddSchemas(pubform->oid, schemaidlist, false, stmt);
 	}
 	else if (stmt->action == AP_DropObjects)
-		PublicationDropSchemas(pubform->oid, schemaidlist, false);
+		dropped = PublicationDropSchemas(pubform->oid, schemaidlist, false);
 	else						/* AP_SetObjects */
 	{
 		List	   *oldschemaids = GetPublicationSchemas(pubform->oid);
@@ -1494,7 +1498,7 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		LockSchemaList(delschemas);
 
 		/* And drop them */
-		PublicationDropSchemas(pubform->oid, delschemas, true);
+		dropped = PublicationDropSchemas(pubform->oid, delschemas, true);
 
 		/*
 		 * Don't bother calculating the difference for adding, we'll catch and
@@ -1502,6 +1506,8 @@ AlterPublicationSchemas(AlterPublicationStmt *stmt,
 		 */
 		PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt);
 	}
+
+	return dropped;
 }
 
 /*
@@ -1686,6 +1692,8 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 		List	   *exceptrelations = NIL;
 		List	   *schemaidlist = NIL;
 		Oid			pubid = pubform->oid;
+		bool		tables_dropped;
+		bool		schemas_dropped;
 
 		ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations,
 								   &exceptrelations, &schemaidlist);
@@ -1712,10 +1720,20 @@ AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt)
 						   stmt->pubname));
 
 		relations = list_concat(relations, exceptrelations);
-		AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
-							   schemaidlist != NIL);
-		AlterPublicationSchemas(stmt, tup, schemaidlist);
+		tables_dropped =
+			AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext,
+								   schemaidlist != NIL);
+		schemas_dropped = AlterPublicationSchemas(stmt, tup, schemaidlist);
 		AlterPublicationAllFlags(stmt, rel, tup);
+
+		if (tables_dropped || schemas_dropped)
+		{
+			ObjectAddress obj;
+
+			ObjectAddressSet(obj, PublicationRelationId, pubid);
+			EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress,
+											 (Node *) stmt);
+		}
 	}
 
 	/* Cleanup. */
@@ -2063,12 +2081,13 @@ PublicationAddTables(Oid pubid, List *rels, bool if_not_exists,
 /*
  * Remove listed tables from the publication.
  */
-static void
+static bool
 PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 {
 	ObjectAddress obj;
 	ListCell   *lc;
 	Oid			prid;
+	bool		dropped = false;
 
 	foreach(lc, rels)
 	{
@@ -2102,7 +2121,10 @@ PublicationDropTables(Oid pubid, List *rels, bool missing_ok)
 
 		ObjectAddressSet(obj, PublicationRelRelationId, prid);
 		performDeletion(&obj, DROP_CASCADE, 0);
+		dropped = true;
 	}
+
+	return dropped;
 }
 
 /*
@@ -2134,12 +2156,13 @@ PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists,
 /*
  * Remove listed schemas from the publication.
  */
-static void
+static bool
 PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 {
 	ObjectAddress obj;
 	ListCell   *lc;
 	Oid			psid;
+	bool		dropped = false;
 
 	foreach(lc, schemas)
 	{
@@ -2162,7 +2185,10 @@ PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok)
 
 		ObjectAddressSet(obj, PublicationNamespaceRelationId, psid);
 		performDeletion(&obj, DROP_CASCADE, 0);
+		dropped = true;
 	}
+
+	return dropped;
 }
 
 /*
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index 2fb925fc0ff..44503e6d0bd 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -398,6 +398,14 @@ DROP ROLE regress_evt_user;
 DROP EVENT TRIGGER regress_event_trigger_drop_objects;
 DROP EVENT TRIGGER undroppable;
 -- Event triggers on relations.
+CREATE TABLE evttrig_pub_tbl (a int);
+CREATE TABLE evttrig_pub_tbl2 (a int);
+CREATE TABLE evttrig_pub_tbl3 (a int);
+CREATE SCHEMA evttrig_pub_schema;
+CREATE SCHEMA evttrig_pub_schema2;
+CREATE PUBLICATION evttrig_pub FOR TABLE evttrig_pub_tbl,
+  evttrig_pub_tbl2, evttrig_pub_tbl3, TABLES IN SCHEMA evttrig_pub_schema,
+  evttrig_pub_schema2;
 CREATE OR REPLACE FUNCTION event_trigger_report_dropped()
  RETURNS event_trigger
  LANGUAGE plpgsql
@@ -445,6 +453,16 @@ NOTICE:  NORMAL: orig=t normal=f istemp=f type=table identity=public.tv2 schema=
 NOTICE:  NORMAL: orig=f normal=t istemp=f type=table constraint identity=te1_b_fkey on public.te1 schema=public name=<NULL> addr={public,te1,te1_b_fkey} args={}
 NOTICE:  NORMAL: orig=t normal=f istemp=f type=table identity=public.tv1 schema=public name=tv1 addr={public,tv1} args={}
 NOTICE:  NORMAL: orig=f normal=t istemp=f type=table constraint identity=te1_a_fkey on public.te1 schema=public name=<NULL> addr={public,te1,te1_a_fkey} args={}
+ALTER PUBLICATION evttrig_pub DROP TABLE evttrig_pub_tbl;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=publication relation identity=public.evttrig_pub_tbl in publication evttrig_pub schema=<NULL> name=<NULL> addr={public,evttrig_pub_tbl} args={evttrig_pub}
+NOTICE:  END: command_tag=ALTER PUBLICATION type=publication identity=evttrig_pub
+ALTER PUBLICATION evttrig_pub DROP TABLES IN SCHEMA evttrig_pub_schema;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=publication namespace identity=evttrig_pub_schema in publication evttrig_pub schema=<NULL> name=<NULL> addr={evttrig_pub_schema} args={evttrig_pub}
+NOTICE:  END: command_tag=ALTER PUBLICATION type=publication identity=evttrig_pub
+ALTER PUBLICATION evttrig_pub SET TABLE evttrig_pub_tbl2;
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=publication namespace identity=evttrig_pub_schema2 in publication evttrig_pub schema=<NULL> name=<NULL> addr={evttrig_pub_schema2} args={evttrig_pub}
+NOTICE:  NORMAL: orig=t normal=f istemp=f type=publication relation identity=public.evttrig_pub_tbl3 in publication evttrig_pub schema=<NULL> name=<NULL> addr={public,evttrig_pub_tbl3} args={evttrig_pub}
+NOTICE:  END: command_tag=ALTER PUBLICATION type=publication identity=evttrig_pub
 CREATE SCHEMA evttrig
 	CREATE TABLE one (col_a SERIAL PRIMARY KEY, col_b text DEFAULT 'forty two', col_c SERIAL)
 	CREATE INDEX one_idx ON one (col_b)
@@ -601,6 +619,12 @@ NOTICE:  END: command_tag=CREATE OPERATOR FAMILY type=operator family identity=p
 NOTICE:  END: command_tag=CREATE OPERATOR CLASS type=operator class identity=public.evttrigopclass USING btree
 DROP EVENT TRIGGER regress_event_trigger_report_dropped;
 DROP EVENT TRIGGER regress_event_trigger_report_end;
+DROP PUBLICATION evttrig_pub;
+DROP TABLE evttrig_pub_tbl;
+DROP TABLE evttrig_pub_tbl2;
+DROP TABLE evttrig_pub_tbl3;
+DROP SCHEMA evttrig_pub_schema;
+DROP SCHEMA evttrig_pub_schema2;
 -- only allowed from within an event trigger function, should fail
 select pg_event_trigger_table_rewrite_oid();
 ERROR:  pg_event_trigger_table_rewrite_oid() can only be called in a table_rewrite event trigger function
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index c1deac2d628..34bd0c23fa9 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -300,6 +300,15 @@ DROP EVENT TRIGGER regress_event_trigger_drop_objects;
 DROP EVENT TRIGGER undroppable;
 
 -- Event triggers on relations.
+CREATE TABLE evttrig_pub_tbl (a int);
+CREATE TABLE evttrig_pub_tbl2 (a int);
+CREATE TABLE evttrig_pub_tbl3 (a int);
+CREATE SCHEMA evttrig_pub_schema;
+CREATE SCHEMA evttrig_pub_schema2;
+CREATE PUBLICATION evttrig_pub FOR TABLE evttrig_pub_tbl,
+  evttrig_pub_tbl2, evttrig_pub_tbl3, TABLES IN SCHEMA evttrig_pub_schema,
+  evttrig_pub_schema2;
+
 CREATE OR REPLACE FUNCTION event_trigger_report_dropped()
  RETURNS event_trigger
  LANGUAGE plpgsql
@@ -340,6 +349,10 @@ REVOKE SELECT ON PROPERTY GRAPH gx FROM public;
 DROP PROPERTY GRAPH gx;
 DROP TABLE tv1, tv2, te1;
 
+ALTER PUBLICATION evttrig_pub DROP TABLE evttrig_pub_tbl;
+ALTER PUBLICATION evttrig_pub DROP TABLES IN SCHEMA evttrig_pub_schema;
+ALTER PUBLICATION evttrig_pub SET TABLE evttrig_pub_tbl2;
+
 CREATE SCHEMA evttrig
 	CREATE TABLE one (col_a SERIAL PRIMARY KEY, col_b text DEFAULT 'forty two', col_c SERIAL)
 	CREATE INDEX one_idx ON one (col_b)
@@ -422,6 +435,13 @@ CREATE OPERATOR CLASS evttrigopclass FOR TYPE int USING btree AS STORAGE int;
 DROP EVENT TRIGGER regress_event_trigger_report_dropped;
 DROP EVENT TRIGGER regress_event_trigger_report_end;
 
+DROP PUBLICATION evttrig_pub;
+DROP TABLE evttrig_pub_tbl;
+DROP TABLE evttrig_pub_tbl2;
+DROP TABLE evttrig_pub_tbl3;
+DROP SCHEMA evttrig_pub_schema;
+DROP SCHEMA evttrig_pub_schema2;
+
 -- only allowed from within an event trigger function, should fail
 select pg_event_trigger_table_rewrite_oid();
 
-- 
2.37.1 (Apple Git-137.1)

