diff --git a/doc/src/sgml/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml index 71241c8..bcb8bee 100644 --- a/doc/src/sgml/event-trigger.sgml +++ b/doc/src/sgml/event-trigger.sgml @@ -27,9 +27,9 @@ An event trigger fires whenever the event with which it is associated occurs in the database in which it is defined. Currently, the only - supported events are ddl_command_start - and ddl_command_end. Support for additional events may be - added in future releases. + supported events + are ddl_command_start, ddl_command_end. Support + for additional events may be added in future releases. @@ -46,6 +46,14 @@ + To list all objects that have been deleted as part of executing a + command, use the Set Returning + Function pg_event_trigger_dropped_objects() from + your ddl_command_end event trigger code. Note that happens + after the objects have been deleted, so no catalog lookup is possible. + + + Event triggers (like other functions) cannot be executed in an aborted transaction. Thus, if a DDL command fails with an error, any associated ddl_command_end triggers will not be executed. Conversely, diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 92a79d3..687dd94 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -15688,9 +15688,55 @@ FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger(); choose a trigger name that comes after the name of any other trigger you might have on the table. - + For more information about creating triggers, see . + + + Event Trigger Functions + + + pg_dropped_objects + + + + Currently PostgreSQL provides one built in event trigger + helper function, pg_event_trigger_dropped_objects, which + will list all object dropped by a DROP command. That + listing includes multiple targets of the command, as in DROP + TABLE a, b, c; and objects dropped because of + a CASCADE dependency. + + + + The pg_event_trigger_dropped_objects function can be used + in an event trigger like this: + +create function test_event_trigger_for_sql_drop() + returns event_trigger as $$ +DECLARE + obj record; +BEGIN + RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + RAISE NOTICE '% dropped object: % % % %', + tg_tag, + obj.classId::regclass, + obj.classId, obj.objid, obj.objsubid; + END LOOP; +END +$$ language plpgsql; + + + + + For more information about event triggers, + see . + + + diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c index 81d2687..ab0f13c 100644 --- a/src/backend/access/transam/xact.c +++ b/src/backend/access/transam/xact.c @@ -30,6 +30,7 @@ #include "catalog/namespace.h" #include "catalog/storage.h" #include "commands/async.h" +#include "commands/event_trigger.h" #include "commands/tablecmds.h" #include "commands/trigger.h" #include "executor/spi.h" @@ -1955,6 +1956,7 @@ CommitTransaction(void) AtEOXact_HashTables(true); AtEOXact_PgStat(true); AtEOXact_Snapshot(true); + AtEOXact_EventTrigger(true); pgstat_report_xact_timestamp(0); CurrentResourceOwner = NULL; @@ -2208,6 +2210,7 @@ PrepareTransaction(void) AtEOXact_HashTables(true); /* don't call AtEOXact_PgStat here */ AtEOXact_Snapshot(true); + AtEOXact_EventTrigger(true); CurrentResourceOwner = NULL; ResourceOwnerDelete(TopTransactionResourceOwner); @@ -2382,6 +2385,7 @@ CleanupTransaction(void) */ AtCleanup_Portals(); /* now safe to release portal memory */ AtEOXact_Snapshot(false); /* and release the transaction's snapshots */ + AtEOXact_EventTrigger(false); /* and reset Event Trigger internal state */ CurrentResourceOwner = NULL; /* and resource owner */ if (TopTransactionResourceOwner) diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index d203725..0543076 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -347,7 +347,15 @@ performMultipleDeletions(const ObjectAddresses *objects, */ for (i = 0; i < targetObjects->numrefs; i++) { - ObjectAddress *thisobj = targetObjects->refs + i; + ObjectAddress *thisobj; + + thisobj = targetObjects->refs + i; + + if (EventTriggerSQLDropInProgress && + EventTriggerSupportsObjectType(getObjectClass(thisobj))) + { + add_exact_object_address(thisobj, EventTriggerSQLDropList); + } deleteOneObject(thisobj, &depRel, flags); } @@ -2175,6 +2183,18 @@ record_object_address_dependencies(const ObjectAddress *depender, behavior); } +int +get_object_addresses_numelements(const ObjectAddresses *addresses) +{ + return addresses->numrefs; +} + +ObjectAddress * +get_object_addresses_element(const ObjectAddresses *addresses, int i) +{ + return addresses->refs + i; +} + /* * Clean up when done with an ObjectAddresses array. */ diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 18b3753..9ed5715 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -25,6 +25,7 @@ #include "commands/dbcommands.h" #include "commands/event_trigger.h" #include "commands/trigger.h" +#include "funcapi.h" #include "parser/parse_func.h" #include "pgstat.h" #include "miscadmin.h" @@ -39,6 +40,10 @@ #include "utils/syscache.h" #include "tcop/utility.h" +/* Globally visible state variables */ +bool EventTriggerSQLDropInProgress = false; +ObjectAddresses *EventTriggerSQLDropList = NULL; + typedef struct { const char *obtypename; @@ -150,8 +155,12 @@ CreateEventTrigger(CreateEventTrigStmt *stmt) } /* Validate tag list, if any. */ - if (strcmp(stmt->eventname, "ddl_command_start") == 0 && tags != NULL) + if ((strcmp(stmt->eventname, "ddl_command_start") == 0 || + strcmp(stmt->eventname, "ddl_command_end") == 0) + && tags != NULL) + { validate_ddl_tags("tag", tags); + } /* * Give user a nice error message if an event trigger of the same name @@ -739,6 +748,14 @@ EventTriggerDDLCommandEnd(Node *parsetree) /* Cleanup. */ list_free(runlist); + + if (EventTriggerSQLDropInProgress) + { + free_object_addresses(EventTriggerSQLDropList); + + EventTriggerSQLDropInProgress = false; + EventTriggerSQLDropList = NULL; + } } /* @@ -825,3 +842,107 @@ EventTriggerSupportsObjectType(ObjectType obtype) } return true; } + +/* + * SQL DROP event support functions + */ +void +EventTriggerInitDropList(void) +{ + EventTriggerSQLDropInProgress = true; + EventTriggerSQLDropList = new_object_addresses(); +} + +/* + * AtEOXact_EventTrigger + * Event Trigger's cleanup function for end of transaction + */ +void +AtEOXact_EventTrigger(bool isCommit) +{ + /* even on success we want to reset EventTriggerSQLDropInProgress */ + EventTriggerSQLDropInProgress = false; +} + +/* + * pg_event_trigger_dropped_objects + * + * Make the list of dropped objects available to the user function run by the + * Event Trigger. + */ +Datum +pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS) +{ + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + TupleDesc tupdesc; + Tuplestorestate *tupstore; + MemoryContext per_query_ctx; + MemoryContext oldcontext; + int i; + + /* + * This function is meant to be called from within an event trigger in + * order to get the list of objects dropped, if any. + */ + if (!EventTriggerSQLDropInProgress) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("pg_dropped_objects() can only be called from an event trigger function"))); + + /* check to see if caller supports us returning a tuplestore */ + if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsinfo->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not allowed in this context"))); + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + /* Build tuplestore to hold the result rows */ + per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; + oldcontext = MemoryContextSwitchTo(per_query_ctx); + + tupstore = tuplestore_begin_heap(true, false, work_mem); + rsinfo->returnMode = SFRM_Materialize; + rsinfo->setResult = tupstore; + rsinfo->setDesc = tupdesc; + + MemoryContextSwitchTo(oldcontext); + + for (i = 0; i < get_object_addresses_numelements(EventTriggerSQLDropList); i++) + { + ObjectAddress *object; + Datum values[3]; + bool nulls[3]; + + /* Emit result row */ + object = get_object_addresses_element(EventTriggerSQLDropList, i); + + MemSet(values, 0, sizeof(values)); + MemSet(nulls, 0, sizeof(nulls)); + + /* classid */ + values[0] = ObjectIdGetDatum(object->classId); + + /* objid */ + values[1] = ObjectIdGetDatum(object->objectId); + + /* objsubid */ + if (OidIsValid(object->objectSubId)) + values[2] = ObjectIdGetDatum(object->objectSubId); + else + nulls[2] = true; + + tuplestore_putvalues(tupstore, tupdesc, values, nulls); + } + + /* clean up and return the tuplestore */ + tuplestore_donestoring(tupstore); + + return (Datum) 0; +} diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 8904c6f..7ed05d3 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -698,18 +698,33 @@ standard_ProcessUtility(Node *parsetree, { DropStmt *stmt = (DropStmt *) parsetree; - if (isCompleteQuery - && EventTriggerSupportsObjectType(stmt->removeType)) + /* + * don't run any event trigger when we require not to have open + * a transaction + */ + if (stmt->removeType == OBJECT_INDEX && stmt->concurrent) + PreventTransactionChain(isTopLevel, + "DROP INDEX CONCURRENTLY"); + + if (isCompleteQuery && + EventTriggerSupportsObjectType(stmt->removeType)) + { EventTriggerDDLCommandStart(parsetree); + /* + * cater with multiple targets and cascading drops. + * + * Initialize that after having called the + * ddl_command_start triggers so that + * EventTriggerSQLDropInProgress is still false there, as + * that protects pg_dropped_objects() calls. + */ + EventTriggerInitDropList(); + } + switch (stmt->removeType) { case OBJECT_INDEX: - if (stmt->concurrent) - PreventTransactionChain(isTopLevel, - "DROP INDEX CONCURRENTLY"); - /* fall through */ - case OBJECT_TABLE: case OBJECT_SEQUENCE: case OBJECT_VIEW: @@ -723,8 +738,9 @@ standard_ProcessUtility(Node *parsetree, if (isCompleteQuery && EventTriggerSupportsObjectType(stmt->removeType)) + { EventTriggerDDLCommandEnd(parsetree); - + } break; } diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 8e0837f..846726c 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -191,6 +191,11 @@ extern void record_object_address_dependencies(const ObjectAddress *depender, ObjectAddresses *referenced, DependencyType behavior); +extern int get_object_addresses_numelements(const ObjectAddresses *addresses); + +extern ObjectAddress *get_object_addresses_element(const ObjectAddresses *addresses, + int i); + extern void free_object_addresses(ObjectAddresses *addrs); /* in pg_depend.c */ diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 028e168..3981513 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4679,7 +4679,9 @@ DESCR("SP-GiST support for quad tree over range"); DATA(insert OID = 3473 ( spg_range_quad_leaf_consistent PGNSP PGUID 12 1 0 0 0 f f f f t f i 2 0 16 "2281 2281" _null_ _null_ _null_ _null_ spg_range_quad_leaf_consistent _null_ _null_ _null_ )); DESCR("SP-GiST support for quad tree over range"); - +/* 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,26}" "{o,o,o}" "{classid, objid, objsubid}" _null_ pg_event_trigger_dropped_objects _null_ _null_ _null_ )); +DESCR("list an extension's version update paths"); /* * Symbolic values for provolatile column: these indicate whether the result * of a function is dependent *only* on the values of its explicit arguments, diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h index 74c150b..7ed92b0 100644 --- a/src/include/commands/event_trigger.h +++ b/src/include/commands/event_trigger.h @@ -13,9 +13,23 @@ #ifndef EVENT_TRIGGER_H #define EVENT_TRIGGER_H +#include "catalog/dependency.h" +#include "catalog/objectaddress.h" #include "catalog/pg_event_trigger.h" #include "nodes/parsenodes.h" +/* + * Global objects that we need to keep track of for benefits of Event Triggers. + * + * The EventTriggerSQLDropList is a list of ObjectAddress filled in from + * dependency.c doDeletion() function. Only objects that are supported as in + * EventTriggerSupportsObjectType() get appended here. ProcessUtility is + * responsible for resetting this list to NIL at the beginning of any DROP + * operation. + */ +extern bool EventTriggerSQLDropInProgress; +extern ObjectAddresses *EventTriggerSQLDropList; + typedef struct EventTriggerData { NodeTag type; @@ -43,4 +57,11 @@ extern bool EventTriggerSupportsObjectType(ObjectType obtype); extern void EventTriggerDDLCommandStart(Node *parsetree); extern void EventTriggerDDLCommandEnd(Node *parsetree); +extern void EventTriggerInitDropList(void); +extern List *EventTriggerAppendToDropList(ObjectAddress *object); +extern void EventTriggerSQLDrop(Node *parsetree); + +extern void AtEOXact_EventTrigger(bool isCommit); + + #endif /* EVENT_TRIGGER_H */ diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index 533539c..d51b829 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -1146,6 +1146,9 @@ extern Datum pg_describe_object(PG_FUNCTION_ARGS); /* commands/constraint.c */ extern Datum unique_key_recheck(PG_FUNCTION_ARGS); +/* commands/event_trigger.c */ +extern Datum pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS); + /* commands/extension.c */ extern Datum pg_available_extensions(PG_FUNCTION_ARGS); extern Datum pg_available_extension_versions(PG_FUNCTION_ARGS); diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index bf020de..75d4ee7 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -93,11 +93,74 @@ ERROR: event trigger "regress_event_trigger" does not exist drop role regression_bob; ERROR: role "regression_bob" cannot be dropped because some objects depend on it DETAIL: owner of event trigger regress_event_trigger3 --- these are all OK; the second one should emit a NOTICE +-- now test pg_event_trigger_dropped_objects() +create function test_event_trigger_dropped_objects() returns event_trigger as $$ +DECLARE + obj record; +BEGIN + RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + -- we can't output the full data that we have here because the OID + -- would change each time we run the regression tests. + -- + -- obj.classId, obj.objid, obj.objsubid; + RAISE NOTICE '% dropped object: %', tg_tag, obj.classId::regclass; + END LOOP; +END +$$ language plpgsql; +NOTICE: test_event_trigger: ddl_command_start CREATE FUNCTION +NOTICE: test_event_trigger: ddl_command_end CREATE FUNCTION +-- OK +create event trigger regress_event_trigger_drop_objects on ddl_command_end + when tag in ('drop table', 'drop function', 'drop view') + execute procedure test_event_trigger_dropped_objects(); +-- a simple enough test: cascade +create table evt_a(id serial); +NOTICE: test_event_trigger: ddl_command_start CREATE TABLE +NOTICE: test_event_trigger: ddl_command_end CREATE TABLE +create view evt_a_v as select id from evt_a; +NOTICE: test_event_trigger: ddl_command_end CREATE VIEW +drop table evt_a cascade; +NOTICE: drop cascades to view evt_a_v +NOTICE: test_event_trigger: ddl_command_end DROP TABLE +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_rewrite +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: test_event_trigger: ddl_command_end DROP TABLE +-- another test with multiple targets +create table evt_a(id serial); +NOTICE: test_event_trigger: ddl_command_start CREATE TABLE +NOTICE: test_event_trigger: ddl_command_end CREATE TABLE +create table evt_b(id serial); +NOTICE: test_event_trigger: ddl_command_start CREATE TABLE +NOTICE: test_event_trigger: ddl_command_end CREATE TABLE +drop table evt_a, evt_b; +NOTICE: test_event_trigger: ddl_command_end DROP TABLE +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_type +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: DROP TABLE dropped object: pg_class +NOTICE: test_event_trigger: ddl_command_end DROP TABLE +-- these are all OK; the third one should emit a NOTICE +drop event trigger if exists regress_event_trigger_drop_objects; drop event trigger if exists regress_event_trigger2; drop event trigger if exists regress_event_trigger2; NOTICE: event trigger "regress_event_trigger2" does not exist, skipping drop event trigger regress_event_trigger3; drop event trigger regress_event_trigger_end; -drop function test_event_trigger(); +drop function test_event_trigger_dropped_objects(); drop role regression_bob; diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql index a07dcd7..4d92071 100644 --- a/src/test/regress/sql/event_trigger.sql +++ b/src/test/regress/sql/event_trigger.sql @@ -97,10 +97,45 @@ drop event trigger regress_event_trigger; -- should fail, regression_bob owns regress_event_trigger2/3 drop role regression_bob; --- these are all OK; the second one should emit a NOTICE +-- now test pg_event_trigger_dropped_objects() +create function test_event_trigger_dropped_objects() returns event_trigger as $$ +DECLARE + obj record; +BEGIN + RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; + + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + -- we can't output the full data that we have here because the OID + -- would change each time we run the regression tests. + -- + -- obj.classId, obj.objid, obj.objsubid; + RAISE NOTICE '% dropped object: %', tg_tag, obj.classId::regclass; + END LOOP; +END +$$ language plpgsql; + +-- OK +create event trigger regress_event_trigger_drop_objects on ddl_command_end + when tag in ('drop table', 'drop function', 'drop view') + execute procedure test_event_trigger_dropped_objects(); + +-- a simple enough test: cascade +create table evt_a(id serial); +create view evt_a_v as select id from evt_a; +drop table evt_a cascade; + +-- another test with multiple targets +create table evt_a(id serial); +create table evt_b(id serial); +drop table evt_a, evt_b; + +-- these are all OK; the third one should emit a NOTICE +drop event trigger if exists regress_event_trigger_drop_objects; drop event trigger if exists regress_event_trigger2; drop event trigger if exists regress_event_trigger2; drop event trigger regress_event_trigger3; drop event trigger regress_event_trigger_end; -drop function test_event_trigger(); +drop function test_event_trigger_dropped_objects(); + drop role regression_bob;