diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/catalog/dependency.c | 65 | ||||
-rw-r--r-- | src/backend/commands/event_trigger.c | 518 | ||||
-rw-r--r-- | src/backend/parser/gram.y | 1 | ||||
-rw-r--r-- | src/backend/tcop/utility.c | 64 | ||||
-rw-r--r-- | src/backend/utils/adt/regproc.c | 2 | ||||
-rw-r--r-- | src/backend/utils/cache/evtcache.c | 2 | ||||
-rw-r--r-- | src/include/catalog/catversion.h | 2 | ||||
-rw-r--r-- | src/include/catalog/pg_proc.h | 3 | ||||
-rw-r--r-- | src/include/commands/event_trigger.h | 9 | ||||
-rw-r--r-- | src/include/utils/builtins.h | 3 | ||||
-rw-r--r-- | src/include/utils/evtcache.h | 3 | ||||
-rw-r--r-- | src/test/regress/expected/event_trigger.out | 197 | ||||
-rw-r--r-- | src/test/regress/sql/event_trigger.sql | 106 |
13 files changed, 865 insertions, 110 deletions
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c index ddf199049ef..286137c2513 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -191,6 +191,44 @@ static bool stack_address_present_add_flags(const ObjectAddress *object, /* + * Go through the objects given running the final actions on them, and execute + * the actual deletion. + */ +static void +deleteObjectsInList(ObjectAddresses *targetObjects, Relation *depRel, + int flags) +{ + int i; + + /* + * Keep track of objects for event triggers, if necessary. + */ + if (trackDroppedObjectsNeeded()) + { + for (i = 0; i < targetObjects->numrefs; i++) + { + ObjectAddress *thisobj = targetObjects->refs + i; + + if ((!(flags & PERFORM_DELETION_INTERNAL)) && + EventTriggerSupportsObjectType(getObjectClass(thisobj))) + { + EventTriggerSQLDropAddObject(thisobj); + } + } + } + + /* + * Delete all the objects in the proper order. + */ + for (i = 0; i < targetObjects->numrefs; i++) + { + ObjectAddress *thisobj = targetObjects->refs + i; + + deleteOneObject(thisobj, depRel, flags); + } +} + +/* * performDeletion: attempt to drop the specified object. If CASCADE * behavior is specified, also drop any dependent objects (recursively). * If RESTRICT behavior is specified, error out if there are any dependent @@ -215,7 +253,6 @@ performDeletion(const ObjectAddress *object, { Relation depRel; ObjectAddresses *targetObjects; - int i; /* * We save some cycles by opening pg_depend just once and passing the @@ -250,15 +287,8 @@ performDeletion(const ObjectAddress *object, NOTICE, object); - /* - * Delete all the objects in the proper order. - */ - for (i = 0; i < targetObjects->numrefs; i++) - { - ObjectAddress *thisobj = targetObjects->refs + i; - - deleteOneObject(thisobj, &depRel, flags); - } + /* do the deed */ + deleteObjectsInList(targetObjects, &depRel, flags); /* And clean up */ free_object_addresses(targetObjects); @@ -332,15 +362,8 @@ performMultipleDeletions(const ObjectAddresses *objects, NOTICE, (objects->numrefs == 1 ? objects->refs : NULL)); - /* - * Delete all the objects in the proper order. - */ - for (i = 0; i < targetObjects->numrefs; i++) - { - ObjectAddress *thisobj = targetObjects->refs + i; - - deleteOneObject(thisobj, &depRel, flags); - } + /* do the deed */ + deleteObjectsInList(targetObjects, &depRel, flags); /* And clean up */ free_object_addresses(targetObjects); @@ -356,6 +379,10 @@ performMultipleDeletions(const ObjectAddresses *objects, * This is currently used only to clean out the contents of a schema * (namespace): the passed object is a namespace. We normally want this * to be done silently, so there's an option to suppress NOTICE messages. + * + * Note we don't fire object drop event triggers here; it would be wrong to do + * so for the current only use of this function, but if more callers are added + * this might need to be reconsidered. */ void deleteWhatDependsOn(const ObjectAddress *object, 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; +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0d82141fef6..ed8502c6e4e 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -4478,7 +4478,6 @@ DropTrigStmt: * * QUERIES : * CREATE EVENT TRIGGER ... - * DROP EVENT TRIGGER ... * ALTER EVENT TRIGGER ... * *****************************************************************************/ diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index a1c03f1f763..77b4e5368e7 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -346,12 +346,13 @@ ProcessUtility(Node *parsetree, do { \ if (isCompleteQuery) \ { \ - EventTriggerDDLCommandStart(parsetree); \ + EventTriggerDDLCommandStart(parsetree); \ } \ fncall; \ if (isCompleteQuery) \ { \ - EventTriggerDDLCommandEnd(parsetree); \ + EventTriggerSQLDrop(parsetree); \ + EventTriggerDDLCommandEnd(parsetree); \ } \ } while (0) @@ -366,10 +367,48 @@ ProcessUtility(Node *parsetree, fncall; \ if (_supported) \ { \ + EventTriggerSQLDrop(parsetree); \ EventTriggerDDLCommandEnd(parsetree); \ } \ } while (0) +/* + * UTILITY_BEGIN_QUERY and UTILITY_END_QUERY are a pair of macros to enclose + * execution of a single DDL command, to ensure the event trigger environment + * is appropriately set up before starting, and tore down after completion or + * error. + */ +#define UTILITY_BEGIN_QUERY(isComplete) \ + do { \ + bool _needCleanup = false; \ + \ + if (isComplete) \ + { \ + _needCleanup = EventTriggerBeginCompleteQuery(); \ + } \ + \ + PG_TRY(); \ + { \ + /* avoid empty statement when followed by a semicolon */ \ + (void) 0 + +#define UTILITY_END_QUERY() \ + } \ + PG_CATCH(); \ + { \ + if (_needCleanup) \ + { \ + EventTriggerEndCompleteQuery(); \ + } \ + PG_RE_THROW(); \ + } \ + PG_END_TRY(); \ + if (_needCleanup) \ + { \ + EventTriggerEndCompleteQuery(); \ + } \ + } while (0) + void standard_ProcessUtility(Node *parsetree, const char *queryString, @@ -386,6 +425,8 @@ standard_ProcessUtility(Node *parsetree, if (completionTag) completionTag[0] = '\0'; + UTILITY_BEGIN_QUERY(isCompleteQuery); + switch (nodeTag(parsetree)) { /* @@ -615,7 +656,10 @@ standard_ProcessUtility(Node *parsetree, } if (isCompleteQuery) + { + EventTriggerSQLDrop(parsetree); EventTriggerDDLCommandEnd(parsetree); + } } break; @@ -726,7 +770,10 @@ standard_ProcessUtility(Node *parsetree, if (isCompleteQuery && EventTriggerSupportsObjectType(stmt->removeType)) + { + EventTriggerSQLDrop(parsetree); EventTriggerDDLCommandEnd(parsetree); + } break; } @@ -856,6 +903,12 @@ standard_ProcessUtility(Node *parsetree, ereport(NOTICE, (errmsg("relation \"%s\" does not exist, skipping", atstmt->relation->relname))); + + if (isCompleteQuery) + { + EventTriggerSQLDrop(parsetree); + EventTriggerDDLCommandEnd(parsetree); + } } break; @@ -1248,8 +1301,9 @@ standard_ProcessUtility(Node *parsetree, break; case T_DropOwnedStmt: - /* no event triggers for global objects */ - DropOwnedObjects((DropOwnedStmt *) parsetree); + InvokeDDLCommandEventTriggers( + parsetree, + DropOwnedObjects((DropOwnedStmt *) parsetree)); break; case T_ReassignOwnedStmt: @@ -1372,6 +1426,8 @@ standard_ProcessUtility(Node *parsetree, (int) nodeTag(parsetree)); break; } + + UTILITY_END_QUERY(); } /* diff --git a/src/backend/utils/adt/regproc.c b/src/backend/utils/adt/regproc.c index 94599aa44ba..700247e4741 100644 --- a/src/backend/utils/adt/regproc.c +++ b/src/backend/utils/adt/regproc.c @@ -345,7 +345,7 @@ format_procedure_internal(Oid procedure_oid, bool force_qualify) /* * Would this proc be found (given the right args) by regprocedurein? - * If not, we need to qualify it. + * If not, or if caller requests it, we need to qualify it. */ if (!force_qualify && FunctionIsVisible(procedure_oid)) nspname = NULL; diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c index 34c61280c27..bbd3ae369d3 100644 --- a/src/backend/utils/cache/evtcache.c +++ b/src/backend/utils/cache/evtcache.c @@ -169,6 +169,8 @@ BuildEventTriggerCache(void) event = EVT_DDLCommandStart; else if (strcmp(evtevent, "ddl_command_end") == 0) event = EVT_DDLCommandEnd; + else if (strcmp(evtevent, "sql_drop") == 0) + event = EVT_SQLDrop; else continue; diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index cbc4673d1b4..0eb8eefb8ff 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201303201 +#define CATALOG_VERSION_NO 201303271 #endif diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 4aee00233a9..151987ec1d3 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4693,6 +4693,9 @@ DATA(insert OID = 3473 ( spg_range_quad_leaf_consistent PGNSP PGUID 12 1 0 0 0 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,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"); /* * 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 74c150bd084..8ea99c19c99 100644 --- a/src/include/commands/event_trigger.h +++ b/src/include/commands/event_trigger.h @@ -13,13 +13,14 @@ #ifndef EVENT_TRIGGER_H #define EVENT_TRIGGER_H +#include "catalog/objectaddress.h" #include "catalog/pg_event_trigger.h" #include "nodes/parsenodes.h" typedef struct EventTriggerData { NodeTag type; - char *event; /* event name */ + const char *event; /* event name */ Node *parsetree; /* parse tree */ const char *tag; /* command tag */ } EventTriggerData; @@ -42,5 +43,11 @@ extern void AlterEventTriggerOwner_oid(Oid, Oid newOwnerId); extern bool EventTriggerSupportsObjectType(ObjectType obtype); extern void EventTriggerDDLCommandStart(Node *parsetree); extern void EventTriggerDDLCommandEnd(Node *parsetree); +extern void EventTriggerSQLDrop(Node *parsetree); + +extern bool EventTriggerBeginCompleteQuery(void); +extern void EventTriggerEndCompleteQuery(void); +extern bool trackDroppedObjectsNeeded(void); +extern void EventTriggerSQLDropAddObject(ObjectAddress *object); #endif /* EVENT_TRIGGER_H */ diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h index cd8ac9462b5..e71876502e1 100644 --- a/src/include/utils/builtins.h +++ b/src/include/utils/builtins.h @@ -1151,6 +1151,9 @@ extern Datum pg_identify_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/include/utils/evtcache.h b/src/include/utils/evtcache.h index c230995212d..945e5b53cb8 100644 --- a/src/include/utils/evtcache.h +++ b/src/include/utils/evtcache.h @@ -19,7 +19,8 @@ typedef enum { EVT_DDLCommandStart, - EVT_DDLCommandEnd + EVT_DDLCommandEnd, + EVT_SQLDrop } EventTriggerEvent; typedef struct diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index bf020de16f6..060cd722bef 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -93,11 +93,204 @@ 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 +-- cleanup before next test -- these are all OK; the second one should emit a NOTICE 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 role regression_bob; +-- test support for dropped objects +CREATE SCHEMA schema_one authorization regression_bob; +CREATE SCHEMA schema_two authorization regression_bob; +CREATE SCHEMA audit_tbls authorization regression_bob; +SET SESSION AUTHORIZATION regression_bob; +CREATE TABLE schema_one.table_one(a int); +CREATE TABLE schema_one."table two"(a int); +CREATE TABLE schema_one.table_three(a int); +CREATE TABLE audit_tbls.schema_one_table_two(the_value text); +CREATE TABLE schema_two.table_two(a int); +CREATE TABLE schema_two.table_three(a int, b text); +CREATE TABLE audit_tbls.schema_two_table_three(the_value text); +CREATE OR REPLACE FUNCTION schema_two.add(int, int) RETURNS int LANGUAGE plpgsql + CALLED ON NULL INPUT + AS $$ BEGIN RETURN coalesce($1,0) + coalesce($2,0); END; $$; +CREATE AGGREGATE schema_two.newton + (BASETYPE = int, SFUNC = schema_two.add, STYPE = int); +RESET SESSION AUTHORIZATION; +CREATE TABLE undroppable_objs ( + object_type text, + object_identity text +); +INSERT INTO undroppable_objs VALUES +('table', 'schema_one.table_three'), +('table', 'audit_tbls.schema_two_table_three'); +CREATE TABLE dropped_objects ( + type text, + schema text, + object text +); +-- This tests errors raised within event triggers; the one in audit_tbls +-- uses 2nd-level recursive invocation via test_evtrig_dropped_objects(). +CREATE OR REPLACE FUNCTION undroppable() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +DECLARE + obj record; +BEGIN + PERFORM 1 FROM pg_tables WHERE tablename = 'undroppable_objs'; + IF NOT FOUND THEN + RAISE NOTICE 'table undroppable_objs not found, skipping'; + RETURN; + END IF; + FOR obj IN + SELECT * FROM pg_event_trigger_dropped_objects() JOIN + undroppable_objs USING (object_type, object_identity) + LOOP + RAISE EXCEPTION 'object % of type % cannot be dropped', + obj.object_identity, obj.object_type; + END LOOP; +END; +$$; +CREATE EVENT TRIGGER undroppable ON sql_drop + EXECUTE PROCEDURE undroppable(); +CREATE OR REPLACE FUNCTION test_evtrig_dropped_objects() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +DECLARE + obj record; +BEGIN + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + IF obj.object_type = 'table' THEN + EXECUTE format('DROP TABLE IF EXISTS audit_tbls.%I', + format('%s_%s', obj.schema_name, obj.object_name)); + END IF; + + INSERT INTO dropped_objects + (type, schema, object) VALUES + (obj.object_type, obj.schema_name, obj.object_identity); + END LOOP; +END +$$; +CREATE EVENT TRIGGER regress_event_trigger_drop_objects ON sql_drop + WHEN TAG IN ('drop table', 'drop function', 'drop view', + 'drop owned', 'drop schema', 'alter table') + EXECUTE PROCEDURE test_evtrig_dropped_objects(); +ALTER TABLE schema_one.table_one DROP COLUMN a; +DROP SCHEMA schema_one, schema_two CASCADE; +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to table schema_two.table_two +drop cascades to table schema_two.table_three +drop cascades to function schema_two.add(integer,integer) +drop cascades to function schema_two.newton(integer) +drop cascades to table schema_one.table_one +drop cascades to table schema_one."table two" +drop cascades to table schema_one.table_three +NOTICE: table "schema_two_table_two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_two" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "audit_tbls_schema_two_table_three" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.audit_tbls_schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +ERROR: object audit_tbls.schema_two_table_three of type table cannot be dropped +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +DELETE FROM undroppable_objs WHERE object_identity = 'audit_tbls.schema_two_table_three'; +DROP SCHEMA schema_one, schema_two CASCADE; +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to table schema_two.table_two +drop cascades to table schema_two.table_three +drop cascades to function schema_two.add(integer,integer) +drop cascades to function schema_two.newton(integer) +drop cascades to table schema_one.table_one +drop cascades to table schema_one."table two" +drop cascades to table schema_one.table_three +NOTICE: table "schema_two_table_two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_two" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "audit_tbls_schema_two_table_three" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.audit_tbls_schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table_one" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_one_table_one" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls."schema_one_table two"" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table_three" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_one_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +ERROR: object schema_one.table_three of type table cannot be dropped +DELETE FROM undroppable_objs WHERE object_identity = 'schema_one.table_three'; +DROP SCHEMA schema_one, schema_two CASCADE; +NOTICE: drop cascades to 7 other objects +DETAIL: drop cascades to table schema_two.table_two +drop cascades to table schema_two.table_three +drop cascades to function schema_two.add(integer,integer) +drop cascades to function schema_two.newton(integer) +drop cascades to table schema_one.table_one +drop cascades to table schema_one."table two" +drop cascades to table schema_one.table_three +NOTICE: table "schema_two_table_two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_two" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "audit_tbls_schema_two_table_three" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.audit_tbls_schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_two_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table_one" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_one_table_one" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls."schema_one_table two"" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +NOTICE: table "schema_one_table_three" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.schema_one_table_three" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +SELECT * FROM dropped_objects WHERE schema IS NULL OR schema <> 'pg_toast'; + type | schema | object +--------------+------------+------------------------------------- + table column | schema_one | schema_one.table_one.a + schema | | schema_two + table | schema_two | schema_two.table_two + type | schema_two | schema_two.table_two + type | schema_two | schema_two.table_two[] + table | audit_tbls | audit_tbls.schema_two_table_three + type | audit_tbls | audit_tbls.schema_two_table_three + type | audit_tbls | audit_tbls.schema_two_table_three[] + table | schema_two | schema_two.table_three + type | schema_two | schema_two.table_three + type | schema_two | schema_two.table_three[] + function | schema_two | schema_two.add(integer,integer) + aggregate | schema_two | schema_two.newton(integer) + schema | | schema_one + table | schema_one | schema_one.table_one + type | schema_one | schema_one.table_one + type | schema_one | schema_one.table_one[] + table | schema_one | schema_one."table two" + type | schema_one | schema_one."table two" + type | schema_one | schema_one."table two"[] + table | schema_one | schema_one.table_three + type | schema_one | schema_one.table_three + type | schema_one | schema_one.table_three[] +(23 rows) + +DROP OWNED BY regression_bob; +NOTICE: table "audit_tbls_schema_one_table_two" does not exist, skipping +CONTEXT: SQL statement "DROP TABLE IF EXISTS audit_tbls.audit_tbls_schema_one_table_two" +PL/pgSQL function test_evtrig_dropped_objects() line 8 at EXECUTE statement +SELECT * FROM dropped_objects WHERE type = 'schema'; + type | schema | object +--------+--------+------------ + schema | | schema_two + schema | | schema_one + schema | | audit_tbls +(3 rows) + +DROP ROLE regression_bob; +DROP EVENT TRIGGER regress_event_trigger_drop_objects; +DROP EVENT TRIGGER undroppable; diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql index a07dcd75546..11d2ce5c133 100644 --- a/src/test/regress/sql/event_trigger.sql +++ b/src/test/regress/sql/event_trigger.sql @@ -97,10 +97,112 @@ drop event trigger regress_event_trigger; -- should fail, regression_bob owns regress_event_trigger2/3 drop role regression_bob; +-- cleanup before next test -- these are all OK; the second one should emit a NOTICE 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 role regression_bob; + +-- test support for dropped objects +CREATE SCHEMA schema_one authorization regression_bob; +CREATE SCHEMA schema_two authorization regression_bob; +CREATE SCHEMA audit_tbls authorization regression_bob; +SET SESSION AUTHORIZATION regression_bob; + +CREATE TABLE schema_one.table_one(a int); +CREATE TABLE schema_one."table two"(a int); +CREATE TABLE schema_one.table_three(a int); +CREATE TABLE audit_tbls.schema_one_table_two(the_value text); + +CREATE TABLE schema_two.table_two(a int); +CREATE TABLE schema_two.table_three(a int, b text); +CREATE TABLE audit_tbls.schema_two_table_three(the_value text); + +CREATE OR REPLACE FUNCTION schema_two.add(int, int) RETURNS int LANGUAGE plpgsql + CALLED ON NULL INPUT + AS $$ BEGIN RETURN coalesce($1,0) + coalesce($2,0); END; $$; +CREATE AGGREGATE schema_two.newton + (BASETYPE = int, SFUNC = schema_two.add, STYPE = int); + +RESET SESSION AUTHORIZATION; + +CREATE TABLE undroppable_objs ( + object_type text, + object_identity text +); +INSERT INTO undroppable_objs VALUES +('table', 'schema_one.table_three'), +('table', 'audit_tbls.schema_two_table_three'); + +CREATE TABLE dropped_objects ( + type text, + schema text, + object text +); + +-- This tests errors raised within event triggers; the one in audit_tbls +-- uses 2nd-level recursive invocation via test_evtrig_dropped_objects(). +CREATE OR REPLACE FUNCTION undroppable() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +DECLARE + obj record; +BEGIN + PERFORM 1 FROM pg_tables WHERE tablename = 'undroppable_objs'; + IF NOT FOUND THEN + RAISE NOTICE 'table undroppable_objs not found, skipping'; + RETURN; + END IF; + FOR obj IN + SELECT * FROM pg_event_trigger_dropped_objects() JOIN + undroppable_objs USING (object_type, object_identity) + LOOP + RAISE EXCEPTION 'object % of type % cannot be dropped', + obj.object_identity, obj.object_type; + END LOOP; +END; +$$; + +CREATE EVENT TRIGGER undroppable ON sql_drop + EXECUTE PROCEDURE undroppable(); + +CREATE OR REPLACE FUNCTION test_evtrig_dropped_objects() RETURNS event_trigger +LANGUAGE plpgsql AS $$ +DECLARE + obj record; +BEGIN + FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects() + LOOP + IF obj.object_type = 'table' THEN + EXECUTE format('DROP TABLE IF EXISTS audit_tbls.%I', + format('%s_%s', obj.schema_name, obj.object_name)); + END IF; + + INSERT INTO dropped_objects + (type, schema, object) VALUES + (obj.object_type, obj.schema_name, obj.object_identity); + END LOOP; +END +$$; + +CREATE EVENT TRIGGER regress_event_trigger_drop_objects ON sql_drop + WHEN TAG IN ('drop table', 'drop function', 'drop view', + 'drop owned', 'drop schema', 'alter table') + EXECUTE PROCEDURE test_evtrig_dropped_objects(); + +ALTER TABLE schema_one.table_one DROP COLUMN a; +DROP SCHEMA schema_one, schema_two CASCADE; +DELETE FROM undroppable_objs WHERE object_identity = 'audit_tbls.schema_two_table_three'; +DROP SCHEMA schema_one, schema_two CASCADE; +DELETE FROM undroppable_objs WHERE object_identity = 'schema_one.table_three'; +DROP SCHEMA schema_one, schema_two CASCADE; + +SELECT * FROM dropped_objects WHERE schema IS NULL OR schema <> 'pg_toast'; + +DROP OWNED BY regression_bob; +SELECT * FROM dropped_objects WHERE type = 'schema'; + +DROP ROLE regression_bob; + +DROP EVENT TRIGGER regress_event_trigger_drop_objects; +DROP EVENT TRIGGER undroppable; |