From cd30321a8338d32e63222070c75fce8b660b16d8 Mon Sep 17 00:00:00 2001 From: Hayato Kuroda 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