diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml index 6f71a27855..0219972772 100644 --- a/doc/src/sgml/event-trigger.sgml +++ b/doc/src/sgml/event-trigger.sgml @@ -64,6 +64,16 @@ system catalogs, so it's not possible to look them up anymore. + + The table_rewrite event occurs just before a table is + rewritten by the command ALTER TABLE. While other + control statements are available to rewrite a table, + like CLUSTER and VACUUM, + the table_rewrite event is currently only triggered by + the ALTER TABLE command, and only when that command + attempts to rewrite the table. + + Event triggers (like other functions) cannot be executed in an aborted transaction. Thus, if a DDL command fails with an error, any associated @@ -120,6 +130,7 @@ ddl_command_start ddl_command_end sql_drop + table_rewrite @@ -128,510 +139,595 @@ X X - + - ALTER COLLATION X X - + - ALTER CONVERSION X X - + - ALTER DOMAIN X X - + - ALTER EXTENSION X X - + - ALTER FOREIGN DATA WRAPPER X X - + - ALTER FOREIGN TABLE X X X + - ALTER FUNCTION X X - + - ALTER LANGUAGE X X - + - ALTER OPERATOR X X - + - ALTER OPERATOR CLASS X X - + - ALTER OPERATOR FAMILY X X - + - ALTER POLICY X X - + - ALTER SCHEMA X X - + - ALTER SEQUENCE X X - + - ALTER SERVER X X - + - ALTER TABLE X X X + X ALTER TEXT SEARCH CONFIGURATION X X - + - ALTER TEXT SEARCH DICTIONARY X X - + - ALTER TEXT SEARCH PARSER X X - + - ALTER TEXT SEARCH TEMPLATE X X - + - ALTER TRIGGER X X - + - ALTER TYPE X X - + - ALTER USER MAPPING X X - + - ALTER VIEW X X - + - CREATE AGGREGATE X X - + - CREATE CAST X X - + - CREATE COLLATION X X - + - CREATE CONVERSION X X - + - CREATE DOMAIN X X - + - CREATE EXTENSION X X - + - CREATE FOREIGN DATA WRAPPER X X - + - CREATE FOREIGN TABLE X X - + - CREATE FUNCTION X X - + - CREATE INDEX X X - + - CREATE LANGUAGE X X - + - CREATE OPERATOR X X - + - CREATE OPERATOR CLASS X X - + - CREATE OPERATOR FAMILY X X - + - CREATE POLICY X X - + - CREATE RULE X X - + - CREATE SCHEMA X X - + - CREATE SEQUENCE X X - + - CREATE SERVER X X - + - CREATE TABLE X X - + - CREATE TABLE AS X X - + - CREATE TEXT SEARCH CONFIGURATION X X - + - CREATE TEXT SEARCH DICTIONARY X X - + - CREATE TEXT SEARCH PARSER X X - + - CREATE TEXT SEARCH TEMPLATE X X - + - CREATE TRIGGER X X - + - CREATE TYPE X X - + - CREATE USER MAPPING X X - + - CREATE VIEW X X - + - DROP AGGREGATE X X X + - DROP CAST X X X + - DROP COLLATION X X X + - DROP CONVERSION X X X + - DROP DOMAIN X X X + - DROP EXTENSION X X X + - DROP FOREIGN DATA WRAPPER X X X + - DROP FOREIGN TABLE X X X + - DROP FUNCTION X X X + - DROP INDEX X X X + - DROP LANGUAGE X X X + - DROP OPERATOR X X X + - DROP OPERATOR CLASS X X X + - DROP OPERATOR FAMILY X X X + - DROP OWNED X X X + - DROP POLICY X X X + - DROP RULE X X X + - DROP SCHEMA X X X + - DROP SEQUENCE X X X + - DROP SERVER X X X + - DROP TABLE X X X + - DROP TEXT SEARCH CONFIGURATION X X X + - DROP TEXT SEARCH DICTIONARY X X X + - DROP TEXT SEARCH PARSER X X X + - DROP TEXT SEARCH TEMPLATE X X X + - DROP TRIGGER X X X + - DROP TYPE X X X + - DROP USER MAPPING X X X + - DROP VIEW X X X + - IMPORT FOREIGN SCHEMA X X - + - SELECT INTO X X - + - @@ -843,4 +939,58 @@ COMMIT; event triggers.) + + + A Table Rewrite Event Trigger Example + + + Thanks to the table_rewrite event, it is possible to implement + a table rewriting policy only allowing the rewrite in maintenance windows. + + + + Here's an example implementing such a policy. + +CREATE OR REPLACE FUNCTION no_rewrite() + RETURNS event_trigger + LANGUAGE plpgsql AS +$$ +--- +--- Implement local Table Rewriting policy: +--- public.foo is not allowed rewriting, ever +--- other tables are only allowed rewriting between 1am and 6am +--- unless they have more than 100 blocks +--- +DECLARE + table_oid oid := pg_event_trigger_table_rewrite_oid(); + current_hour integer := extract('hour' from current_time); + pages integer; + max_pages integer := 100; +BEGIN + IF pg_event_trigger_table_rewrite_oid() = 'public.foo'::regclass + THEN + RAISE EXCEPTION 'you''re not allowed to rewrite the table %', + table_oid::regclass; + END IF; + + SELECT INTO pages relpages FROM pg_class WHERE oid = table_oid; + IF pages > max_pages + THEN + RAISE EXCEPTION 'rewrites only allowed for table with less than % pages', + max_pages; + END IF; + + IF current_hour NOT BETWEEN 1 AND 6 + THEN + RAISE EXCEPTION 'rewrites only allowed between 1am and 6am'; + END IF; +END; +$$; + +CREATE EVENT TRIGGER no_rewrite_allowed + ON table_rewrite + EXECUTE PROCEDURE no_rewrite(); + + + diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 62ec275a9e..c3b6126943 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -17607,15 +17607,23 @@ FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger(); Event Trigger Functions + + PostgreSQL provides these helper functions + to retrieve information from event triggers. + + + + For more information about event triggers, + see . + + + + Processing objects dropped by a DDL command. + pg_event_trigger_dropped_objects - - Currently PostgreSQL provides one built-in event trigger - helper function, pg_event_trigger_dropped_objects. - - pg_event_trigger_dropped_objects returns a list of all objects dropped by the command in whose sql_drop event it is called. @@ -17709,11 +17717,72 @@ CREATE EVENT TRIGGER test_event_trigger_for_drops EXECUTE PROCEDURE test_event_trigger_for_drops(); + - - For more information about event triggers, - see . + + Handling a Table Rewrite Event + + + The functions shown in + + provide information about a table for which a + table_rewrite event has just been called. + If called in any other context, an error is raised. + + + + Table Rewrite information + + + Name Return Type Description + + + + + + pg_event_trigger_table_rewrite_oid + pg_event_trigger_table_rewrite_oid() + + Oid + The Oid of the table about to be rewritten. + + + + + pg_event_trigger_table_rewrite_reason + pg_event_trigger_table_rewrite_reason() + + int + + The reason code(s) explaining the reason for rewriting. The exact + meaning of the codes is release dependent. + + + + +
+ + + The pg_event_trigger_table_rewrite_oid function can be used + in an event trigger like this: + +CREATE FUNCTION test_event_trigger_table_rewrite_oid() + RETURNS event_trigger + LANGUAGE plpgsql AS +$$ +BEGIN + RAISE NOTICE 'rewriting table % for reason %', + pg_event_trigger_table_rewrite_oid()::regclass, + pg_event_trigger_table_rewrite_reason(); +END; +$$; + +CREATE EVENT TRIGGER test_table_rewrite_oid + ON table_rewrite + EXECUTE PROCEDURE test_event_trigger_table_rewrite_oid(); + +
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 6a3002f526..6fb4817845 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -42,11 +42,16 @@ #include "utils/syscache.h" #include "tcop/utility.h" - typedef struct EventTriggerQueryState { + /* sql_drop */ slist_head SQLDropList; bool in_sql_drop; + + /* table_rewrite */ + Oid table_rewrite_oid; /* InvalidOid, or set for table_rewrite event */ + int table_rewrite_reason; /* AT_REWRITE reason */ + MemoryContext cxt; struct EventTriggerQueryState *previous; } EventTriggerQueryState; @@ -119,11 +124,14 @@ static void AlterEventTriggerOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId); static event_trigger_command_tag_check_result check_ddl_tag(const char *tag); +static event_trigger_command_tag_check_result check_table_rewrite_ddl_tag( + const char *tag); static void error_duplicate_filter_variable(const char *defname); static Datum filter_list_to_array(List *filterlist); static Oid insert_event_trigger_tuple(char *trigname, char *eventname, Oid evtOwner, Oid funcoid, List *tags); static void validate_ddl_tags(const char *filtervar, List *taglist); +static void validate_table_rewrite_tags(const char *filtervar, List *taglist); static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata); /* @@ -154,7 +162,8 @@ CreateEventTrigger(CreateEventTrigStmt *stmt) /* Validate event name. */ if (strcmp(stmt->eventname, "ddl_command_start") != 0 && strcmp(stmt->eventname, "ddl_command_end") != 0 && - strcmp(stmt->eventname, "sql_drop") != 0) + strcmp(stmt->eventname, "sql_drop") != 0 && + strcmp(stmt->eventname, "table_rewrite") != 0) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized event name \"%s\"", @@ -183,6 +192,9 @@ CreateEventTrigger(CreateEventTrigStmt *stmt) strcmp(stmt->eventname, "sql_drop") == 0) && tags != NULL) validate_ddl_tags("tag", tags); + else if (strcmp(stmt->eventname, "table_rewrite") == 0 + && tags != NULL) + validate_table_rewrite_tags("tag", tags); /* * Give user a nice error message if an event trigger of the same name @@ -280,6 +292,38 @@ check_ddl_tag(const char *tag) return EVENT_TRIGGER_COMMAND_TAG_OK; } +/* + * Validate DDL command tags for event table_rewrite. + */ +static void +validate_table_rewrite_tags(const char *filtervar, List *taglist) +{ + ListCell *lc; + + foreach(lc, taglist) + { + const char *tag = strVal(lfirst(lc)); + event_trigger_command_tag_check_result result; + + result = check_table_rewrite_ddl_tag(tag); + if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + /* translator: %s represents an SQL statement name */ + errmsg("event triggers are not supported for %s", + tag))); + } +} + +static event_trigger_command_tag_check_result +check_table_rewrite_ddl_tag(const char *tag) +{ + if (pg_strcasecmp(tag, "ALTER TABLE") == 0) + return EVENT_TRIGGER_COMMAND_TAG_OK; + + return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED; +} + /* * Complain about a duplicate filter variable. */ @@ -641,8 +685,18 @@ EventTriggerCommonSetup(Node *parsetree, const char *dbgtag; dbgtag = CreateCommandTag(parsetree); - if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK) - elog(ERROR, "unexpected command tag \"%s\"", dbgtag); + if (event == EVT_DDLCommandStart || + event == EVT_DDLCommandEnd || + event == EVT_SQLDrop) + { + if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK) + elog(ERROR, "unexpected command tag \"%s\"", dbgtag); + } + else if (event == EVT_TableRewrite) + { + if (check_table_rewrite_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK) + elog(ERROR, "unexpected command tag \"%s\"", dbgtag); + } } #endif @@ -838,6 +892,80 @@ EventTriggerSQLDrop(Node *parsetree) list_free(runlist); } + +/* + * Fire table_rewrite triggers. + */ +void +EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason) +{ + List *runlist; + EventTriggerData trigdata; + + elog(DEBUG1, "EventTriggerTableRewrite(%u)", tableOid); + + /* + * Event Triggers are completely disabled in standalone mode. There are + * (at least) two reasons for this: + * + * 1. A sufficiently broken event trigger might not only render the + * database unusable, but prevent disabling itself to fix the situation. + * In this scenario, restarting in standalone mode provides an escape + * hatch. + * + * 2. BuildEventTriggerCache relies on systable_beginscan_ordered, and + * therefore will malfunction if pg_event_trigger's indexes are damaged. + * To allow recovery from a damaged index, we need some operating mode + * wherein event triggers are disabled. (Or we could implement + * heapscan-and-sort logic for that case, but having disaster recovery + * scenarios depend on code that's otherwise untested isn't appetizing.) + */ + if (!IsUnderPostmaster) + return; + + runlist = EventTriggerCommonSetup(parsetree, + EVT_TableRewrite, + "table_rewrite", + &trigdata); + if (runlist == NIL) + return; + + /* + * Make sure pg_event_trigger_table_rewrite_oid only works when running + * these triggers. Use PG_TRY to ensure table_rewrite_oid is reset even + * when one trigger fails. (This is perhaps not necessary, as the + * currentState variable will be removed shortly by our caller, but it + * seems better to play safe.) + */ + currentEventTriggerState->table_rewrite_oid = tableOid; + currentEventTriggerState->table_rewrite_reason = reason; + + /* Run the triggers. */ + PG_TRY(); + { + EventTriggerInvoke(runlist, &trigdata); + } + PG_CATCH(); + { + currentEventTriggerState->table_rewrite_oid = InvalidOid; + currentEventTriggerState->table_rewrite_reason = 0; + PG_RE_THROW(); + } + PG_END_TRY(); + + currentEventTriggerState->table_rewrite_oid = InvalidOid; + currentEventTriggerState->table_rewrite_reason = 0; + + /* Cleanup. */ + list_free(runlist); + + /* + * Make sure anything the event triggers did will be visible to the main + * command. + */ + CommandCounterIncrement(); +} + /* * Invoke each event trigger in a list of event triggers. */ @@ -871,6 +999,8 @@ EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata) FunctionCallInfoData fcinfo; PgStat_FunctionCallUsage fcusage; + elog(DEBUG1, "EventTriggerInvoke %u", fnoid); + /* * We want each event trigger to be able to see the results of the * previous event trigger's action. Caller is responsible for any @@ -1026,8 +1156,9 @@ EventTriggerBeginCompleteQuery(void) MemoryContext cxt; /* - * Currently, sql_drop events are the only reason to have event trigger - * state at all; so if there are none, don't install one. + * Currently, sql_drop and table_rewrite events are the only reason to + * have event trigger state at all; so if there are none, don't install + * one. */ if (!trackDroppedObjectsNeeded()) return false; @@ -1041,6 +1172,7 @@ EventTriggerBeginCompleteQuery(void) state->cxt = cxt; slist_init(&(state->SQLDropList)); state->in_sql_drop = false; + state->table_rewrite_oid = InvalidOid; state->previous = currentEventTriggerState; currentEventTriggerState = state; @@ -1080,8 +1212,9 @@ EventTriggerEndCompleteQuery(void) bool trackDroppedObjectsNeeded(void) { - /* true if any sql_drop event trigger exists */ - return list_length(EventCacheLookup(EVT_SQLDrop)) > 0; + /* true if any sql_drop or table_rewrite event trigger exists */ + return list_length(EventCacheLookup(EVT_SQLDrop)) > 0 || + list_length(EventCacheLookup(EVT_TableRewrite)) > 0; } /* @@ -1297,3 +1430,46 @@ pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS) return (Datum) 0; } + +/* + * pg_event_trigger_table_rewrite_oid + * + * Make the Oid of the table going to be rewritten available to the user + * function run by the Event Trigger. + */ +Datum +pg_event_trigger_table_rewrite_oid(PG_FUNCTION_ARGS) +{ + /* + * Protect this function from being called out of context + */ + if (!currentEventTriggerState || + currentEventTriggerState->table_rewrite_oid == InvalidOid) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s can only be called in a table_rewrite event trigger function", + "pg_event_trigger_table_rewrite_oid()"))); + + PG_RETURN_OID(currentEventTriggerState->table_rewrite_oid); +} + +/* + * pg_event_trigger_table_rewrite_reason + * + * Make the rewrite reason available to the user. + */ +Datum +pg_event_trigger_table_rewrite_reason(PG_FUNCTION_ARGS) +{ + /* + * Protect this function from being called out of context + */ + if (!currentEventTriggerState || + currentEventTriggerState->table_rewrite_reason == 0) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s can only be called in a table_rewrite event trigger function", + "pg_event_trigger_table_rewrite_reason()"))); + + PG_RETURN_INT32(currentEventTriggerState->table_rewrite_reason); +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 2333e1bed9..1e737a01c9 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -46,6 +46,7 @@ #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" +#include "commands/event_trigger.h" #include "commands/policy.h" #include "commands/sequence.h" #include "commands/tablecmds.h" @@ -153,7 +154,7 @@ typedef struct AlteredTableInfo List *constraints; /* List of NewConstraint */ List *newvals; /* List of NewColumnValue */ bool new_notnull; /* T if we added new NOT NULL constraints */ - bool rewrite; /* T if a rewrite is forced */ + int rewrite; /* Reason for forced rewrite, if any */ Oid newTableSpace; /* new tablespace; 0 means no change */ bool chgPersistence; /* T if SET LOGGED/UNLOGGED is used */ char newrelpersistence; /* if above is true */ @@ -303,13 +304,15 @@ static void validateForeignKeyConstraint(char *conname, static void createForeignKeyTriggers(Relation rel, Oid refRelOid, Constraint *fkconstraint, Oid constraintOid, Oid indexOid); -static void ATController(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode); +static void ATController(AlterTableStmt *parsetree, + Relation rel, List *cmds, bool recurse, LOCKMODE lockmode); static void ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, bool recurse, bool recursing, LOCKMODE lockmode); static void ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode); static void ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, AlterTableCmd *cmd, LOCKMODE lockmode); -static void ATRewriteTables(List **wqueue, LOCKMODE lockmode); +static void ATRewriteTables(AlterTableStmt *parsetree, + List **wqueue, LOCKMODE lockmode); static void ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode); static AlteredTableInfo *ATGetQueueEntry(List **wqueue, Relation rel); static void ATSimplePermissions(Relation rel, int allowed_targets); @@ -2724,7 +2727,8 @@ AlterTable(Oid relid, LOCKMODE lockmode, AlterTableStmt *stmt) CheckTableNotInUse(rel, "ALTER TABLE"); - ATController(rel, stmt->cmds, interpretInhOption(stmt->relation->inhOpt), + ATController(stmt, + rel, stmt->cmds, interpretInhOption(stmt->relation->inhOpt), lockmode); } @@ -2747,7 +2751,7 @@ AlterTableInternal(Oid relid, List *cmds, bool recurse) rel = relation_open(relid, lockmode); - ATController(rel, cmds, recurse, lockmode); + ATController(NULL, rel, cmds, recurse, lockmode); } /* @@ -3015,8 +3019,15 @@ AlterTableGetLockLevel(List *cmds) return lockmode; } +/* + * ATController provides top level control over the phases. + * + * parsetree is passed in to allow it to be passed to event triggers + * when requested. + */ static void -ATController(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode) +ATController(AlterTableStmt *parsetree, + Relation rel, List *cmds, bool recurse, LOCKMODE lockmode) { List *wqueue = NIL; ListCell *lcmd; @@ -3036,7 +3047,7 @@ ATController(Relation rel, List *cmds, bool recurse, LOCKMODE lockmode) ATRewriteCatalogs(&wqueue, lockmode); /* Phase 3: scan/rewrite tables as needed */ - ATRewriteTables(&wqueue, lockmode); + ATRewriteTables(parsetree, &wqueue, lockmode); } /* @@ -3195,7 +3206,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* force rewrite if necessary; see comment in ATRewriteTables */ if (tab->chgPersistence) { - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE; tab->newrelpersistence = RELPERSISTENCE_PERMANENT; } pass = AT_PASS_MISC; @@ -3206,7 +3217,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, /* force rewrite if necessary; see comment in ATRewriteTables */ if (tab->chgPersistence) { - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE; tab->newrelpersistence = RELPERSISTENCE_UNLOGGED; } pass = AT_PASS_MISC; @@ -3607,7 +3618,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, * ATRewriteTables: ALTER TABLE phase 3 */ static void -ATRewriteTables(List **wqueue, LOCKMODE lockmode) +ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode) { ListCell *ltab; @@ -3634,7 +3645,7 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode) * constraints, so it's not necessary/appropriate to enforce them just * during ALTER.) */ - if (tab->newvals != NIL || tab->rewrite) + if (tab->newvals != NIL || tab->rewrite > 0) { Relation rel; @@ -3655,7 +3666,7 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode) * and assigns a new relfilenode, we automatically create or drop an * init fork for the relation as appropriate. */ - if (tab->rewrite) + if (tab->rewrite > 0) { /* Build a temporary relation and copy data */ Relation OldHeap; @@ -3709,6 +3720,21 @@ ATRewriteTables(List **wqueue, LOCKMODE lockmode) heap_close(OldHeap, NoLock); + /* + * Fire off an Event Trigger now, before actually rewriting the + * table. + * + * We don't support Event Trigger for nested commands anywhere, + * here included, and parsetree is given NULL when coming from + * AlterTableInternal. + * + * And fire it only once. + */ + if (parsetree) + EventTriggerTableRewrite((Node *)parsetree, + tab->relid, + tab->rewrite); + /* * Create transient table that will receive the modified data. * @@ -4002,7 +4028,7 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap, LOCKMODE lockmode) while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL) { - if (tab->rewrite) + if (tab->rewrite > 0) { Oid tupOid = InvalidOid; @@ -4828,7 +4854,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, newval->expr = expression_planner(defval); tab->newvals = lappend(tab->newvals, newval); - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_DEFAULT_VAL; } /* @@ -4845,7 +4871,7 @@ ATExecAddColumn(List **wqueue, AlteredTableInfo *tab, Relation rel, * table to fix that. */ if (isOid) - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_ALTER_OID; /* * Add needed dependency entries for the new column. @@ -5657,7 +5683,7 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName, tab = ATGetQueueEntry(wqueue, rel); /* Tell Phase 3 to physically remove the OID column */ - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_ALTER_OID; } } @@ -5683,7 +5709,7 @@ ATExecAddIndex(AlteredTableInfo *tab, Relation rel, /* suppress schema rights check when rebuilding existing index */ check_rights = !is_rebuild; /* skip index build if phase 3 will do it or we're reusing an old one */ - skip_build = tab->rewrite || OidIsValid(stmt->oldNode); + skip_build = tab->rewrite > 0 || OidIsValid(stmt->oldNode); /* suppress notices when rebuilding existing index */ quiet = is_rebuild; @@ -7686,7 +7712,7 @@ ATPrepAlterColumnType(List **wqueue, tab->newvals = lappend(tab->newvals, newval); if (ATColumnChangeRequiresRewrite(transform, attnum)) - tab->rewrite = true; + tab->rewrite |= AT_REWRITE_COLUMN_REWRITE; } else if (transform) ereport(ERROR, @@ -8431,7 +8457,7 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd, con->old_pktable_oid = refRelId; /* rewriting neither side of a FK */ if (con->contype == CONSTR_FOREIGN && - !rewrite && !tab->rewrite) + !rewrite && tab->rewrite == 0) TryReuseForeignKey(oldId, con); cmd->subtype = AT_ReAddConstraint; tab->subcmds[AT_PASS_OLD_CONSTR] = diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c index ae71bd6bc7..b9d442cdfc 100644 --- a/src/backend/utils/cache/evtcache.c +++ b/src/backend/utils/cache/evtcache.c @@ -169,6 +169,8 @@ BuildEventTriggerCache(void) event = EVT_DDLCommandEnd; else if (strcmp(evtevent, "sql_drop") == 0) event = EVT_SQLDrop; + else if (strcmp(evtevent, "table_rewrite") == 0) + event = EVT_TableRewrite; else continue; diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index deba4b1fa2..910cfc6bb1 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -5045,6 +5045,10 @@ DESCR("peek at binary changes from replication slot"); /* event triggers */ DATA(insert OID = 3566 ( pg_event_trigger_dropped_objects PGNSP PGUID 12 10 100 0 0 f f f f t t s 0 0 2249 "" "{26,26,23,25,25,25,25}" "{o,o,o,o,o,o,o}" "{classid, objid, objsubid, object_type, schema_name, object_name, object_identity}" _null_ pg_event_trigger_dropped_objects _null_ _null_ _null_ )); DESCR("list objects dropped by the current command"); +DATA(insert OID = 4566 ( pg_event_trigger_table_rewrite_oid PGNSP PGUID 12 1 0 0 0 f f f f t f s 0 0 26 "" "{26}" "{o}" "{oid}" _null_ pg_event_trigger_table_rewrite_oid _null_ _null_ _null_ )); +DESCR("return Oid of the table getting rewritten"); +DATA(insert OID = 4567 ( pg_event_trigger_table_rewrite_reason PGNSP PGUID 12 1 0 0 0 f f f f t f s 0 0 23 "" _null_ _null_ _null_ _null_ pg_event_trigger_table_rewrite_reason _null_ _null_ _null_ )); +DESCR("return reason code for table getting rewritten"); /* generic transition functions for ordered-set aggregates */ DATA(insert OID = 3970 ( ordered_set_transition PGNSP PGUID 12 1 0 0 0 f f f f f f i 2 0 2281 "2281 2276" _null_ _null_ _null_ _null_ ordered_set_transition _null_ _null_ _null_ )); diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h index 0233f4c483..bcd656b72d 100644 --- a/src/include/commands/event_trigger.h +++ b/src/include/commands/event_trigger.h @@ -26,6 +26,11 @@ typedef struct EventTriggerData const char *tag; /* command tag */ } EventTriggerData; +#define AT_REWRITE_ALTER_PERSISTENCE 0x01 +#define AT_REWRITE_DEFAULT_VAL 0x02 +#define AT_REWRITE_COLUMN_REWRITE 0x04 +#define AT_REWRITE_ALTER_OID 0x08 + /* * EventTriggerData is the node type that is passed as fmgr "context" info * when a function is called by the event trigger manager. @@ -46,6 +51,7 @@ extern bool EventTriggerSupportsObjectClass(ObjectClass objclass); extern void EventTriggerDDLCommandStart(Node *parsetree); extern void EventTriggerDDLCommandEnd(Node *parsetree); extern void EventTriggerSQLDrop(Node *parsetree); +extern void EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason); extern bool EventTriggerBeginCompleteQuery(void); extern void EventTriggerEndCompleteQuery(void); diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index 565cff3e0d..2da3002e5d 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -1200,6 +1200,8 @@ extern Datum unique_key_recheck(PG_FUNCTION_ARGS); /* commands/event_trigger.c */ extern Datum pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS); +extern Datum pg_event_trigger_table_rewrite_oid(PG_FUNCTION_ARGS); +extern Datum pg_event_trigger_table_rewrite_reason(PG_FUNCTION_ARGS); /* commands/extension.c */ extern Datum pg_available_extensions(PG_FUNCTION_ARGS); diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h index c4c284fca0..4bbd80a4ad 100644 --- a/src/include/utils/evtcache.h +++ b/src/include/utils/evtcache.h @@ -20,7 +20,8 @@ typedef enum { EVT_DDLCommandStart, EVT_DDLCommandEnd, - EVT_SQLDrop + EVT_SQLDrop, + EVT_TableRewrite } EventTriggerEvent; typedef struct diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index d4723b223d..5934cf74b1 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -294,3 +294,41 @@ SELECT * FROM dropped_objects WHERE type = 'schema'; DROP ROLE regression_bob; DROP EVENT TRIGGER regress_event_trigger_drop_objects; DROP EVENT TRIGGER undroppable; +-- 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 +-- test Table Rewrite Event Trigger +CREATE OR REPLACE FUNCTION test_evtrig_no_rewrite() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'I''m sorry Sir, No Rewrite Allowed.'; +END; +$$; +create event trigger no_rewrite_allowed on table_rewrite + execute procedure test_evtrig_no_rewrite(); +create table rewriteme (id serial primary key, foo float); +insert into rewriteme + select x * 1.001 from generate_series(1, 500) as t(x); +alter table rewriteme alter column foo type numeric; +ERROR: I'm sorry Sir, No Rewrite Allowed. +alter table rewriteme add column baz int default 0; +ERROR: I'm sorry Sir, No Rewrite Allowed. +-- test with more than one reason to rewrite a single table +CREATE OR REPLACE FUNCTION test_evtrig_no_rewrite() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +BEGIN + RAISE NOTICE 'Table ''%'' is being rewritten (reason = %)', + pg_event_trigger_table_rewrite_oid()::regclass, + pg_event_trigger_table_rewrite_reason(); +END; +$$; +alter table rewriteme + add column onemore int default 0, + add column another int default -1, + alter column foo type numeric(10,4); +NOTICE: Table 'rewriteme' is being rewritten (reason = 6) +-- shouldn't trigger a table_rewrite event +alter table rewriteme alter column foo type numeric(12,4); +drop table rewriteme; +drop event trigger no_rewrite_allowed; +drop function test_evtrig_no_rewrite(); diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index e1afd4bf86..62cc198564 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -92,8 +92,6 @@ test: alter_generic misc psql async # rules cannot run concurrently with any test that creates a view test: rules -# event triggers cannot run concurrently with any test that runs DDL -test: event_trigger # ---------- # Another group of parallel tests @@ -106,5 +104,8 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo # ---------- test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid conversion truncate alter_table sequence polymorphism rowtypes returning largeobject with xml +# event triggers cannot run concurrently with any test that runs DDL +test: event_trigger + # run stats by itself because its delay may be insufficient under heavy load test: stats diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule index e609ab0785..07fc827329 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -111,7 +111,6 @@ test: misc test: psql test: async test: rules -test: event_trigger test: select_views test: portals_p2 test: foreign_key @@ -150,4 +149,5 @@ test: returning test: largeobject test: with test: xml +test: event_trigger test: stats diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql index 11d2ce5c13..02b93c9e1f 100644 --- a/src/test/regress/sql/event_trigger.sql +++ b/src/test/regress/sql/event_trigger.sql @@ -206,3 +206,45 @@ DROP ROLE regression_bob; DROP EVENT TRIGGER regress_event_trigger_drop_objects; DROP EVENT TRIGGER undroppable; + +-- only allowed from within an event trigger function, should fail +select pg_event_trigger_table_rewrite_oid(); + +-- test Table Rewrite Event Trigger +CREATE OR REPLACE FUNCTION test_evtrig_no_rewrite() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +BEGIN + RAISE EXCEPTION 'I''m sorry Sir, No Rewrite Allowed.'; +END; +$$; + +create event trigger no_rewrite_allowed on table_rewrite + execute procedure test_evtrig_no_rewrite(); + +create table rewriteme (id serial primary key, foo float); +insert into rewriteme + select x * 1.001 from generate_series(1, 500) as t(x); +alter table rewriteme alter column foo type numeric; +alter table rewriteme add column baz int default 0; + +-- test with more than one reason to rewrite a single table +CREATE OR REPLACE FUNCTION test_evtrig_no_rewrite() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +BEGIN + RAISE NOTICE 'Table ''%'' is being rewritten (reason = %)', + pg_event_trigger_table_rewrite_oid()::regclass, + pg_event_trigger_table_rewrite_reason(); +END; +$$; + +alter table rewriteme + add column onemore int default 0, + add column another int default -1, + alter column foo type numeric(10,4); + +-- shouldn't trigger a table_rewrite event +alter table rewriteme alter column foo type numeric(12,4); + +drop table rewriteme; +drop event trigger no_rewrite_allowed; +drop function test_evtrig_no_rewrite();