diff options
author | Alvaro Herrera <alvherre@alvh.no-ip.org> | 2013-03-27 16:02:10 -0300 |
---|---|---|
committer | Alvaro Herrera <alvherre@alvh.no-ip.org> | 2013-03-28 13:05:48 -0300 |
commit | 473ab40c8bb3fcb1a7645f6a7443a0424d70fbaf (patch) | |
tree | ae120d646f087838e447c764027b3f6415427472 /src/backend/commands/event_trigger.c | |
parent | 593c39d156fd13e07dbfc43e63b53a3e89e82aa4 (diff) | |
download | postgresql-473ab40c8bb3fcb1a7645f6a7443a0424d70fbaf.tar.gz postgresql-473ab40c8bb3fcb1a7645f6a7443a0424d70fbaf.zip |
Add sql_drop event for event triggers
This event takes place just before ddl_command_end, and is fired if and
only if at least one object has been dropped by the command. (For
instance, DROP TABLE IF EXISTS of a table that does not in fact exist
will not lead to such a trigger firing). Commands that drop multiple
objects (such as DROP SCHEMA or DROP OWNED BY) will cause a single event
to fire. Some firings might be surprising, such as
ALTER TABLE DROP COLUMN.
The trigger is fired after the drop has taken place, because that has
been deemed the safest design, to avoid exposing possibly-inconsistent
internal state (system catalogs as well as current transaction) to the
user function code. This means that careful tracking of object
identification is required during the object removal phase.
Like other currently existing events, there is support for tag
filtering.
To support the new event, add a new pg_event_trigger_dropped_objects()
set-returning function, which returns a set of rows comprising the
objects affected by the command. This is to be used within the user
function code, and is mostly modelled after the recently introduced
pg_identify_object() function.
Catalog version bumped due to the new function.
Dimitri Fontaine and Álvaro Herrera
Review by Robert Haas, Tom Lane
Diffstat (limited to 'src/backend/commands/event_trigger.c')
-rw-r--r-- | src/backend/commands/event_trigger.c | 518 |
1 files changed, 440 insertions, 78 deletions
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index fbe8f49a9e7..ed5240d63b0 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -19,14 +19,17 @@ #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/pg_event_trigger.h" +#include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #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 "lib/ilist.h" #include "miscadmin.h" #include "utils/acl.h" #include "utils/builtins.h" @@ -39,6 +42,17 @@ #include "utils/syscache.h" #include "tcop/utility.h" + +typedef struct EventTriggerQueryState +{ + slist_head SQLDropList; + bool in_sql_drop; + MemoryContext cxt; + struct EventTriggerQueryState *previous; +} EventTriggerQueryState; + +EventTriggerQueryState *currentEventTriggerState = NULL; + typedef struct { const char *obtypename; @@ -89,6 +103,17 @@ static event_trigger_support_data event_trigger_support[] = { { NULL, false } }; +/* Support for dropped objects */ +typedef struct SQLDropObject +{ + ObjectAddress address; + const char *schemaname; + const char *objname; + const char *objidentity; + const char *objecttype; + slist_node next; +} SQLDropObject; + static void AlterEventTriggerOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId); @@ -127,7 +152,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, "ddl_command_end") != 0 && + strcmp(stmt->eventname, "sql_drop") != 0) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized event name \"%s\"", @@ -151,7 +177,10 @@ 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 || + strcmp(stmt->eventname, "sql_drop") == 0) + && tags != NULL) validate_ddl_tags("tag", tags); /* @@ -220,7 +249,8 @@ check_ddl_tag(const char *tag) pg_strcasecmp(tag, "SELECT INTO") == 0 || pg_strcasecmp(tag, "REFRESH MATERIALIZED VIEW") == 0 || pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 || - pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0) + pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0 || + pg_strcasecmp(tag, "DROP OWNED") == 0) return EVENT_TRIGGER_COMMAND_TAG_OK; /* @@ -568,35 +598,19 @@ filter_event_trigger(const char **tag, EventTriggerCacheItem *item) } /* - * Fire ddl_command_start triggers. + * Setup for running triggers for the given event. Return value is an OID list + * of functions to run; if there are any, trigdata is filled with an + * appropriate EventTriggerData for them to receive. */ -void -EventTriggerDDLCommandStart(Node *parsetree) +static List * +EventTriggerCommonSetup(Node *parsetree, + EventTriggerEvent event, const char *eventstr, + EventTriggerData *trigdata) { + const char *tag; List *cachelist; - List *runlist = NIL; ListCell *lc; - const char *tag; - EventTriggerData trigdata; - - /* - * 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; + List *runlist = NIL; /* * We want the list of command tags for which this procedure is actually @@ -624,9 +638,9 @@ EventTriggerDDLCommandStart(Node *parsetree) #endif /* Use cache to find triggers for this event; fast exit if none. */ - cachelist = EventCacheLookup(EVT_DDLCommandStart); - if (cachelist == NULL) - return; + cachelist = EventCacheLookup(event); + if (cachelist == NIL) + return NIL; /* Get the command tag. */ tag = CreateCommandTag(parsetree); @@ -649,11 +663,51 @@ EventTriggerDDLCommandStart(Node *parsetree) } } - /* Construct event trigger data. */ - trigdata.type = T_EventTriggerData; - trigdata.event = "ddl_command_start"; - trigdata.parsetree = parsetree; - trigdata.tag = tag; + /* don't spend any more time on this if no functions to run */ + if (runlist == NIL) + return NIL; + + trigdata->type = T_EventTriggerData; + trigdata->event = eventstr; + trigdata->parsetree = parsetree; + trigdata->tag = tag; + + return runlist; +} + +/* + * Fire ddl_command_start triggers. + */ +void +EventTriggerDDLCommandStart(Node *parsetree) +{ + List *runlist; + EventTriggerData trigdata; + + /* + * 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_DDLCommandStart, "ddl_command_start", + &trigdata); + if (runlist == NIL) + return; /* Run the triggers. */ EventTriggerInvoke(runlist, &trigdata); @@ -674,10 +728,7 @@ EventTriggerDDLCommandStart(Node *parsetree) void EventTriggerDDLCommandEnd(Node *parsetree) { - List *cachelist; - List *runlist = NIL; - ListCell *lc; - const char *tag; + List *runlist; EventTriggerData trigdata; /* @@ -687,53 +738,61 @@ EventTriggerDDLCommandEnd(Node *parsetree) if (!IsUnderPostmaster) return; + runlist = EventTriggerCommonSetup(parsetree, + EVT_DDLCommandEnd, "ddl_command_end", + &trigdata); + if (runlist == NIL) + return; + /* - * See EventTriggerDDLCommandStart for a discussion about why this check is - * important. - * + * Make sure anything the main command did will be visible to the + * event triggers. */ -#ifdef USE_ASSERT_CHECKING - if (assert_enabled) - { - const char *dbgtag; + CommandCounterIncrement(); - dbgtag = CreateCommandTag(parsetree); - if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK) - elog(ERROR, "unexpected command tag \"%s\"", dbgtag); - } -#endif + /* Run the triggers. */ + EventTriggerInvoke(runlist, &trigdata); - /* Use cache to find triggers for this event; fast exit if none. */ - cachelist = EventCacheLookup(EVT_DDLCommandEnd); - if (cachelist == NULL) - return; + /* Cleanup. */ + list_free(runlist); +} - /* Get the command tag. */ - tag = CreateCommandTag(parsetree); +/* + * Fire sql_drop triggers. + */ +void +EventTriggerSQLDrop(Node *parsetree) +{ + List *runlist; + EventTriggerData trigdata; /* - * Filter list of event triggers by command tag, and copy them into - * our memory context. Once we start running the command trigers, or - * indeed once we do anything at all that touches the catalogs, an - * invalidation might leave cachelist pointing at garbage, so we must - * do this before we can do much else. + * See EventTriggerDDLCommandStart for a discussion about why event + * triggers are disabled in single user mode. */ - foreach (lc, cachelist) - { - EventTriggerCacheItem *item = lfirst(lc); + if (!IsUnderPostmaster) + return; - if (filter_event_trigger(&tag, item)) - { - /* We must plan to fire this trigger. */ - runlist = lappend_oid(runlist, item->fnoid); - } - } + /* + * Use current state to determine whether this event fires at all. If there + * are no triggers for the sql_drop event, then we don't have anything to do + * here. Note that dropped object collection is disabled if this is the case, + * so even if we were to try to run, the list would be empty. + */ + if (!currentEventTriggerState || + slist_is_empty(¤tEventTriggerState->SQLDropList)) + return; - /* Construct event trigger data. */ - trigdata.type = T_EventTriggerData; - trigdata.event = "ddl_command_end"; - trigdata.parsetree = parsetree; - trigdata.tag = tag; + runlist = EventTriggerCommonSetup(parsetree, + EVT_SQLDrop, "sql_drop", + &trigdata); + /* + * Nothing to do if run list is empty. Note this shouldn't happen, because + * if there are no sql_drop events, then objects-to-drop wouldn't have been + * collected in the first place and we would have quitted above. + */ + if (runlist == NIL) + return; /* * Make sure anything the main command did will be visible to the @@ -741,8 +800,27 @@ EventTriggerDDLCommandEnd(Node *parsetree) */ CommandCounterIncrement(); + /* + * Make sure pg_event_trigger_dropped_objects only works when running these + * triggers. Use PG_TRY to ensure in_sql_drop 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->in_sql_drop = true; + /* Run the triggers. */ - EventTriggerInvoke(runlist, &trigdata); + PG_TRY(); + { + EventTriggerInvoke(runlist, &trigdata); + } + PG_CATCH(); + { + currentEventTriggerState->in_sql_drop = false; + PG_RE_THROW(); + } + PG_END_TRY(); + currentEventTriggerState->in_sql_drop = false; /* Cleanup. */ list_free(runlist); @@ -832,3 +910,287 @@ EventTriggerSupportsObjectType(ObjectType obtype) } return true; } + +/* + * Prepare event trigger state for a new complete query to run, if necessary; + * returns whether this was done. If it was, EventTriggerEndCompleteQuery must + * be called when the query is done, regardless of whether it succeeds or fails + * -- so use of a PG_TRY block is mandatory. + */ +bool +EventTriggerBeginCompleteQuery(void) +{ + EventTriggerQueryState *state; + 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. + */ + if (!trackDroppedObjectsNeeded()) + return false; + + cxt = AllocSetContextCreate(TopMemoryContext, + "event trigger state", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + state = MemoryContextAlloc(cxt, sizeof(EventTriggerQueryState)); + state->cxt = cxt; + slist_init(&(state->SQLDropList)); + state->in_sql_drop = false; + + state->previous = currentEventTriggerState; + currentEventTriggerState = state; + + return true; +} + +/* + * Query completed (or errored out) -- clean up local state, return to previous + * one. + * + * Note: it's an error to call this routine if EventTriggerBeginCompleteQuery + * returned false previously. + * + * Note: this might be called in the PG_CATCH block of a failing transaction, + * so be wary of running anything unnecessary. (In particular, it's probably + * unwise to try to allocate memory.) + */ +void +EventTriggerEndCompleteQuery(void) +{ + EventTriggerQueryState *prevstate; + + prevstate = currentEventTriggerState->previous; + + /* this avoids the need for retail pfree of SQLDropList items: */ + MemoryContextDelete(currentEventTriggerState->cxt); + + currentEventTriggerState = prevstate; +} + +/* + * Do we need to keep close track of objects being dropped? + * + * This is useful because there is a cost to running with them enabled. + */ +bool +trackDroppedObjectsNeeded(void) +{ + /* true if any sql_drop event trigger exists */ + return list_length(EventCacheLookup(EVT_SQLDrop)) > 0; +} + +/* + * Support for dropped objects information on event trigger functions. + * + * We keep the list of objects dropped by the current command in current + * state's SQLDropList (comprising SQLDropObject items). Each time a new + * command is to start, a clean EventTriggerQueryState is created; commands + * that drop objects do the dependency.c dance to drop objects, which + * populates the current state's SQLDropList; when the event triggers are + * invoked they can consume the list via pg_event_trigger_dropped_objects(). + * When the command finishes, the EventTriggerQueryState is cleared, and + * the one from the previous command is restored (when no command is in + * execution, the current state is NULL). + * + * All this lets us support the case that an event trigger function drops + * objects "reentrantly". + */ + +/* + * Register one object as being dropped by the current command. + */ +void +EventTriggerSQLDropAddObject(ObjectAddress *object) +{ + SQLDropObject *obj; + MemoryContext oldcxt; + + if (!currentEventTriggerState) + return; + + Assert(EventTriggerSupportsObjectType(getObjectClass(object))); + + /* don't report temp schemas */ + if (object->classId == NamespaceRelationId && + isAnyTempNamespace(object->objectId)) + return; + + oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt); + + obj = palloc0(sizeof(SQLDropObject)); + obj->address = *object; + + /* + * Obtain schema names from the object's catalog tuple, if one exists; + * this lets us skip objects in temp schemas. We trust that ObjectProperty + * contains all object classes that can be schema-qualified. + */ + if (is_objectclass_supported(object->classId)) + { + Relation catalog; + HeapTuple tuple; + + catalog = heap_open(obj->address.classId, AccessShareLock); + tuple = get_catalog_object_by_oid(catalog, obj->address.objectId); + + if (tuple) + { + AttrNumber attnum; + Datum datum; + bool isnull; + + attnum = get_object_attnum_namespace(obj->address.classId); + if (attnum != InvalidAttrNumber) + { + datum = heap_getattr(tuple, attnum, + RelationGetDescr(catalog), &isnull); + if (!isnull) + { + Oid namespaceId; + + namespaceId = DatumGetObjectId(datum); + /* Don't report objects in temp namespaces */ + if (isAnyTempNamespace(namespaceId)) + { + pfree(obj); + heap_close(catalog, AccessShareLock); + MemoryContextSwitchTo(oldcxt); + return; + } + + obj->schemaname = get_namespace_name(namespaceId); + } + } + + if (get_object_namensp_unique(obj->address.classId) && + obj->address.objectSubId == 0) + { + attnum = get_object_attnum_name(obj->address.classId); + if (attnum != InvalidAttrNumber) + { + datum = heap_getattr(tuple, attnum, + RelationGetDescr(catalog), &isnull); + if (!isnull) + obj->objname = pstrdup(NameStr(*DatumGetName(datum))); + } + } + } + + heap_close(catalog, AccessShareLock); + } + + /* object identity */ + obj->objidentity = getObjectIdentity(&obj->address); + + /* and object type, too */ + obj->objecttype = getObjectTypeDescription(&obj->address); + + slist_push_head(&(currentEventTriggerState->SQLDropList), &obj->next); + + MemoryContextSwitchTo(oldcxt); +} + +/* + * 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; + slist_iter iter; + + /* + * Protect this function from being called out of context + */ + if (!currentEventTriggerState || + !currentEventTriggerState->in_sql_drop) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s can only be called in a sql_drop event trigger function", + "pg_event_trigger_dropped_objects()"))); + + /* 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); + + slist_foreach(iter, &(currentEventTriggerState->SQLDropList)) + { + SQLDropObject *obj; + int i = 0; + Datum values[7]; + bool nulls[7]; + + obj = slist_container(SQLDropObject, next, iter.cur); + + MemSet(values, 0, sizeof(values)); + MemSet(nulls, 0, sizeof(nulls)); + + /* classid */ + values[i++] = ObjectIdGetDatum(obj->address.classId); + + /* objid */ + values[i++] = ObjectIdGetDatum(obj->address.objectId); + + /* objsubid */ + values[i++] = Int32GetDatum(obj->address.objectSubId); + + /* object_type */ + values[i++] = CStringGetTextDatum(obj->objecttype); + + /* schema_name */ + if (obj->schemaname) + values[i++] = CStringGetTextDatum(obj->schemaname); + else + nulls[i++] = true; + + /* object_name */ + if (obj->objname) + values[i++] = CStringGetTextDatum(obj->objname); + else + nulls[i++] = true; + + /* object_identity */ + if (obj->objidentity) + values[i++] = CStringGetTextDatum(obj->objidentity); + else + nulls[i++] = true; + + tuplestore_putvalues(tupstore, tupdesc, values, nulls); + } + + /* clean up and return the tuplestore */ + tuplestore_donestoring(tupstore); + + return (Datum) 0; +} |