From cd30321a8338d32e63222070c75fce8b660b16d8 Mon Sep 17 00:00:00 2001
From: Hayato Kuroda <kuroda.hayato@fujitsu.com>
Date: Wed, 16 Sep 2026 18:49:28 +0900
Subject: [PATCH] Recheck table persistence after table_rewrite triggers

ALTER TABLE ... SET LOGGED/UNLOGGED checks publication membership and
foreign key relationships during command preparation.  A table_rewrite
event trigger, however, can execute DDL after those checks, allowing the command
to leave tables with incompatible persistence or make an unlogged table part of
a publication.

Run a same check as preparation phase immediately after firing an event trigger.
---
 src/backend/commands/tablecmds.c            | 103 ++++++++++++++------
 src/test/regress/expected/event_trigger.out |  78 +++++++++++++++
 src/test/regress/sql/event_trigger.sql      |  90 +++++++++++++++++
 3 files changed, 239 insertions(+), 32 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2f073ddb84a..4b68fcb516f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -692,6 +692,7 @@ static void ATPrepSetAccessMethod(AlteredTableInfo *tab, Relation rel, const cha
 static void ATExecSetAccessMethodNoStorage(Relation rel, Oid newAccessMethodId);
 static void ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel,
 									bool toLogged);
+static void ATRewriteChangePersistence(AlteredTableInfo *tab);
 static void ATPrepSetTableSpace(AlteredTableInfo *tab, Relation rel,
 								const char *tablespacename, LOCKMODE lockmode);
 static void ATExecSetTableSpace(Oid tableOid, Oid newTableSpace, LOCKMODE lockmode);
@@ -6018,10 +6019,20 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			 * And fire it only once.
 			 */
 			if (parsetree)
+			{
 				EventTriggerTableRewrite((Node *) parsetree,
 										 tab->relid,
 										 tab->rewrite);
 
+				/*
+				 * Event triggers can execute DDL that invalidates the
+				 * persistence checks performed during preparation, so repeat
+				 * them before rewriting.
+				 */
+				if (tab->chgPersistence)
+					ATRewriteChangePersistence(tab);
+			}
+
 			/*
 			 * Create transient table that will receive the modified data.
 			 *
@@ -19498,48 +19509,20 @@ ATExecSetCompression(Relation rel,
 	return address;
 }
 
-
 /*
- * Preparation phase for SET LOGGED/UNLOGGED
+ * Do common checks for SET LOGGED/UNLOGGED.
  *
- * This verifies that we're not trying to change a temp table.  Also,
- * existing foreign key constraints are checked to avoid ending up with
- * permanent tables referencing unlogged tables.
+ * Ensure that unlogged tables are not named in publications and that the
+ * persistence of tables connected by foreign key constraints is compatible.
  */
 static void
-ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
+CommonCheckForChangePersistence(Relation rel, bool toLogged)
 {
 	Relation	pg_constraint;
 	HeapTuple	tuple;
 	SysScanDesc scan;
 	ScanKeyData skey[1];
 
-	/*
-	 * Disallow changing status for a temp table.  Also verify whether we can
-	 * get away with doing nothing; in such cases we don't need to run the
-	 * checks below, either.
-	 */
-	switch (rel->rd_rel->relpersistence)
-	{
-		case RELPERSISTENCE_TEMP:
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
-							RelationGetRelationName(rel)),
-					 errtable(rel)));
-			break;
-		case RELPERSISTENCE_PERMANENT:
-			if (toLogged)
-				/* nothing to do */
-				return;
-			break;
-		case RELPERSISTENCE_UNLOGGED:
-			if (!toLogged)
-				/* nothing to do */
-				return;
-			break;
-	}
-
 	/*
 	 * UNLOGGED tables can neither be published nor be named in a
 	 * publication's EXCEPT clause, so reject the change if the table is
@@ -19619,6 +19602,45 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	systable_endscan(scan);
 
 	table_close(pg_constraint, AccessShareLock);
+}
+
+/*
+ * Preparation phase for SET LOGGED/UNLOGGED
+ *
+ * This verifies that we're not trying to change a temp table.  Also,
+ * existing foreign key constraints are checked to avoid ending up with
+ * permanent tables referencing unlogged tables.
+ */
+static void
+ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
+{
+	/*
+	 * Disallow changing status for a temp table.  Also verify whether we can
+	 * get away with doing nothing; in such cases we don't need to run the
+	 * checks below, either.
+	 */
+	switch (rel->rd_rel->relpersistence)
+	{
+		case RELPERSISTENCE_TEMP:
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					 errmsg("cannot change logged status of table \"%s\" because it is temporary",
+							RelationGetRelationName(rel)),
+					 errtable(rel)));
+			break;
+		case RELPERSISTENCE_PERMANENT:
+			if (toLogged)
+				/* nothing to do */
+				return;
+			break;
+		case RELPERSISTENCE_UNLOGGED:
+			if (!toLogged)
+				/* nothing to do */
+				return;
+			break;
+	}
+
+	CommonCheckForChangePersistence(rel, toLogged);
 
 	/* force rewrite if necessary; see comment in ATRewriteTables */
 	tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
@@ -19629,6 +19651,23 @@ ATPrepChangePersistence(AlteredTableInfo *tab, Relation rel, bool toLogged)
 	tab->chgPersistence = true;
 }
 
+/*
+ * Same as ATPrepChangePersistence, but for the rewrite phase.
+ */
+static void
+ATRewriteChangePersistence(AlteredTableInfo *tab)
+{
+	Relation	rel;
+	bool		toLogged;
+
+	rel = table_open(tab->relid, AccessShareLock);
+	toLogged = (tab->newrelpersistence == RELPERSISTENCE_PERMANENT);
+
+	CommonCheckForChangePersistence(rel, toLogged);
+
+	table_close(rel, AccessShareLock);
+}
+
 /*
  * Execute ALTER TABLE SET SCHEMA
  */
diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out
index f57e8ffa7a5..bc05297f81a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -639,6 +639,84 @@ ERROR:  cannot alter type "rewritetype" because column "rewritemetoo3.a" uses it
 drop table rewriteme;
 drop event trigger no_rewrite_allowed;
 drop function test_evtrig_no_rewrite();
+-- Recheck persistence restrictions after table rewrite event triggers.
+-- Case: adding a table to a publication during a rewrite event.
+CREATE TABLE rewrite_pub_target (a int);
+-- Suppress warning that depends on wal_level
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION rewrite_pub;
+RESET client_min_messages;
+-- Define a function which adds a table to the publication for a rewrite
+-- event trigger.
+CREATE FUNCTION test_evtrig_add_table() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_pub_target'::regclass THEN
+    EXECUTE 'ALTER PUBLICATION rewrite_pub ADD TABLE rewrite_pub_target';
+  END IF;
+END;
+$$;
+CREATE EVENT TRIGGER add_table_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_add_table();
+-- Should fail
+ALTER TABLE rewrite_pub_target SET UNLOGGED;
+ERROR:  cannot change table "rewrite_pub_target" to unlogged because it is referenced by a publication
+DETAIL:  Unlogged relations cannot be published or excluded via an EXCEPT clause.
+HINT:  Drop the table from the publication, or remove it from the publication's EXCEPT clause, first.
+-- Cleanup
+DROP EVENT TRIGGER add_table_during_rewrite;
+DROP FUNCTION test_evtrig_add_table();
+DROP PUBLICATION rewrite_pub;
+DROP TABLE rewrite_pub_target;
+-- Case: changing the referenced table to unlogged during a rewrite event.
+CREATE TABLE rewrite_referenced (a int PRIMARY KEY);
+CREATE UNLOGGED TABLE rewrite_to_logged (a int REFERENCES rewrite_referenced);
+-- Define a function which sets a table to unlogged for a rewrite event
+-- trigger.
+CREATE FUNCTION test_evtrig_set_unlogged() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_to_logged'::regclass THEN
+    EXECUTE 'ALTER TABLE rewrite_referenced SET UNLOGGED';
+  END IF;
+END;
+$$;
+CREATE EVENT TRIGGER set_unlogged_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_set_unlogged();
+-- Should fail
+ALTER TABLE rewrite_to_logged SET LOGGED;
+ERROR:  could not change table "rewrite_to_logged" to logged because it references unlogged table "rewrite_referenced"
+-- Cleanup
+DROP EVENT TRIGGER set_unlogged_during_rewrite;
+DROP FUNCTION test_evtrig_set_unlogged();
+DROP TABLE rewrite_to_logged;
+DROP TABLE rewrite_referenced;
+-- Case: changing the referencing table to logged during a rewrite event.
+CREATE TABLE rewrite_to_unlogged (a int PRIMARY KEY);
+CREATE UNLOGGED TABLE rewrite_referencing (a int REFERENCES rewrite_to_unlogged);
+-- Define a function which sets a table to logged for a rewrite event
+-- trigger.
+CREATE FUNCTION test_evtrig_set_logged() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_to_unlogged'::regclass THEN
+    EXECUTE 'ALTER TABLE rewrite_referencing SET LOGGED';
+  END IF;
+END;
+$$;
+CREATE EVENT TRIGGER set_logged_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_set_logged();
+-- Should fail
+ALTER TABLE rewrite_to_unlogged SET UNLOGGED;
+ERROR:  could not change table "rewrite_to_unlogged" to unlogged because it references logged table "rewrite_referencing"
+-- Cleanup
+DROP EVENT TRIGGER set_logged_during_rewrite;
+DROP FUNCTION test_evtrig_set_logged();
+DROP TABLE rewrite_referencing;
+DROP TABLE rewrite_to_unlogged;
 -- Tests for REINDEX
 CREATE OR REPLACE FUNCTION reindex_start_command()
 RETURNS event_trigger AS $$
diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql
index 32e9bb58c5e..bfa2f1f1cc1 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -476,6 +476,96 @@ drop table rewriteme;
 drop event trigger no_rewrite_allowed;
 drop function test_evtrig_no_rewrite();
 
+-- Recheck persistence restrictions after table rewrite event triggers.
+
+-- Case: adding a table to a publication during a rewrite event.
+CREATE TABLE rewrite_pub_target (a int);
+
+-- Suppress warning that depends on wal_level
+SET client_min_messages = 'ERROR';
+CREATE PUBLICATION rewrite_pub;
+RESET client_min_messages;
+
+-- Define a function which adds a table to the publication for a rewrite
+-- event trigger.
+CREATE FUNCTION test_evtrig_add_table() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_pub_target'::regclass THEN
+    EXECUTE 'ALTER PUBLICATION rewrite_pub ADD TABLE rewrite_pub_target';
+  END IF;
+END;
+$$;
+
+CREATE EVENT TRIGGER add_table_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_add_table();
+
+-- Should fail
+ALTER TABLE rewrite_pub_target SET UNLOGGED;
+
+-- Cleanup
+DROP EVENT TRIGGER add_table_during_rewrite;
+DROP FUNCTION test_evtrig_add_table();
+DROP PUBLICATION rewrite_pub;
+DROP TABLE rewrite_pub_target;
+
+-- Case: changing the referenced table to unlogged during a rewrite event.
+CREATE TABLE rewrite_referenced (a int PRIMARY KEY);
+CREATE UNLOGGED TABLE rewrite_to_logged (a int REFERENCES rewrite_referenced);
+
+-- Define a function which sets a table to unlogged for a rewrite event
+-- trigger.
+CREATE FUNCTION test_evtrig_set_unlogged() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_to_logged'::regclass THEN
+    EXECUTE 'ALTER TABLE rewrite_referenced SET UNLOGGED';
+  END IF;
+END;
+$$;
+
+CREATE EVENT TRIGGER set_unlogged_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_set_unlogged();
+
+-- Should fail
+ALTER TABLE rewrite_to_logged SET LOGGED;
+
+-- Cleanup
+DROP EVENT TRIGGER set_unlogged_during_rewrite;
+DROP FUNCTION test_evtrig_set_unlogged();
+DROP TABLE rewrite_to_logged;
+DROP TABLE rewrite_referenced;
+
+-- Case: changing the referencing table to logged during a rewrite event.
+CREATE TABLE rewrite_to_unlogged (a int PRIMARY KEY);
+CREATE UNLOGGED TABLE rewrite_referencing (a int REFERENCES rewrite_to_unlogged);
+
+-- Define a function which sets a table to logged for a rewrite event
+-- trigger.
+CREATE FUNCTION test_evtrig_set_logged() RETURNS event_trigger
+LANGUAGE plpgsql AS $$
+BEGIN
+  IF pg_event_trigger_table_rewrite_oid() = 'rewrite_to_unlogged'::regclass THEN
+    EXECUTE 'ALTER TABLE rewrite_referencing SET LOGGED';
+  END IF;
+END;
+$$;
+
+CREATE EVENT TRIGGER set_logged_during_rewrite ON table_rewrite
+  WHEN TAG IN ('ALTER TABLE')
+  EXECUTE FUNCTION test_evtrig_set_logged();
+
+-- Should fail
+ALTER TABLE rewrite_to_unlogged SET UNLOGGED;
+
+-- Cleanup
+DROP EVENT TRIGGER set_logged_during_rewrite;
+DROP FUNCTION test_evtrig_set_logged();
+DROP TABLE rewrite_referencing;
+DROP TABLE rewrite_to_unlogged;
+
 -- Tests for REINDEX
 CREATE OR REPLACE FUNCTION reindex_start_command()
 RETURNS event_trigger AS $$
-- 
2.52.0

