Dimitri Fontaine escribió: > And already a v1. > > Álvaro did spot a line I did remove by mistake in the docs, and some > extra whitespace changes that pgindent will change anyway and that as > such I shouldn't force you to read and discard.
The bigger change I mentioned was the stuff in dependency.c -- I wasn't too happy about exposing the whole ObjectAddresses stuff to the outside world. The attached version only exposes simple accessors to let an external user of that to iterate on such arrays. Some more commentary is probably needed on those new functions. Also, if we're going to extend things in this way we probably need to get "extra" out alongside the object array itself. A larger issue with the patch is handling of subxacts. A quick test doesn't reveal any obvious misbehavior, but having the list of objects dropped by a global variable might be problematic. What if, say, the event trigger function does something funny in an EXCEPTION block and it fails? Some clever test case is needed here, I think. Also, if we reset the variable at EOXact, do we also need to do something at EOSubXact? -- Álvaro Herrera http://www.2ndQuadrant.com/ PostgreSQL Development, 24x7 Support, Training & Services
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 @@ <para> 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 <literal>ddl_command_start</> - and <literal>ddl_command_end</>. Support for additional events may be - added in future releases. + supported events + are <literal>ddl_command_start</>, <literal>ddl_command_end</>. Support + for additional events may be added in future releases. </para> <para> @@ -46,6 +46,14 @@ </para> <para> + To list all objects that have been deleted as part of executing a + command, use the Set Returning + Function <literal>pg_event_trigger_dropped_objects()</> from + your <literal>ddl_command_end</> event trigger code. Note that happens + after the objects have been deleted, so no catalog lookup is possible. + </para> + + <para> Event triggers (like other functions) cannot be executed in an aborted transaction. Thus, if a DDL command fails with an error, any associated <literal>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. </para> - <para> + <para> For more information about creating triggers, see <xref linkend="SQL-CREATETRIGGER">. </para> </sect1> + + <sect1 id="functions-trigger"> + <title>Event Trigger Functions</title> + + <indexterm> + <primary>pg_dropped_objects</primary> + </indexterm> + + <para> + Currently <productname>PostgreSQL</> provides one built in event trigger + helper function, <function>pg_event_trigger_dropped_objects</>, which + will list all object dropped by a <listeral>DROP</> command. That + listing includes multiple targets of the command, as in <command>DROP + TABLE a, b, c;</command> and objects dropped because of + a <literal>CASCADE</> dependency. + </para> + + <para> + The <function>pg_event_trigger_dropped_objects</> function can be used + in an event trigger like this: +<programlisting> +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; +</programlisting> + </para> + + <para> + For more information about event triggers, + see <xref linkend="event-triggers">. + </para> + </sect1> + </chapter> 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;
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers