diff options
author | Robert Haas <rhaas@postgresql.org> | 2012-07-20 11:38:47 -0400 |
---|---|---|
committer | Robert Haas <rhaas@postgresql.org> | 2012-07-20 11:39:01 -0400 |
commit | 3a0e4d36ebd7f477822d5bae41ba121a40d22ccc (patch) | |
tree | 323cd89ebb88c51b796b86f17f6efa1db76a761e /src | |
parent | be86e3dd5b42c33387ae976c014e6276c9439f7f (diff) | |
download | postgresql-3a0e4d36ebd7f477822d5bae41ba121a40d22ccc.tar.gz postgresql-3a0e4d36ebd7f477822d5bae41ba121a40d22ccc.zip |
Make new event trigger facility actually do something.
Commit 3855968f328918b6cd1401dd11d109d471a54d40 added syntax, pg_dump,
psql support, and documentation, but the triggers didn't actually fire.
With this commit, they now do. This is still a pretty basic facility
overall because event triggers do not get a whole lot of information
about what the user is trying to do unless you write them in C; and
there's still no option to fire them anywhere except at the very
beginning of the execution sequence, but it's better than nothing,
and a good building block for future work.
Along the way, add a regression test for ALTER LARGE OBJECT, since
testing of event triggers reveals that we haven't got one.
Dimitri Fontaine and Robert Haas
Diffstat (limited to 'src')
25 files changed, 1010 insertions, 182 deletions
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 9d36b0c9316..d725360b864 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -13,6 +13,7 @@ */ #include "postgres.h" +#include "access/xact.h" #include "catalog/dependency.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" @@ -28,6 +29,7 @@ #include "miscadmin.h" #include "utils/acl.h" #include "utils/builtins.h" +#include "utils/evtcache.h" #include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -39,54 +41,62 @@ typedef struct { const char *obtypename; - ObjectType obtype; bool supported; } event_trigger_support_data; +typedef enum +{ + EVENT_TRIGGER_COMMAND_TAG_OK, + EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED, + EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED +} event_trigger_command_tag_check_result; + static event_trigger_support_data event_trigger_support[] = { - { "AGGREGATE", OBJECT_AGGREGATE, true }, - { "CAST", OBJECT_CAST, true }, - { "CONSTRAINT", OBJECT_CONSTRAINT, true }, - { "COLLATION", OBJECT_COLLATION, true }, - { "CONVERSION", OBJECT_CONVERSION, true }, - { "DATABASE", OBJECT_DATABASE, false }, - { "DOMAIN", OBJECT_DOMAIN, true }, - { "EXTENSION", OBJECT_EXTENSION, true }, - { "EVENT TRIGGER", OBJECT_EVENT_TRIGGER, false }, - { "FOREIGN DATA WRAPPER", OBJECT_FDW, true }, - { "FOREIGN SERVER", OBJECT_FOREIGN_SERVER, true }, - { "FOREIGN TABLE", OBJECT_FOREIGN_TABLE, true }, - { "FUNCTION", OBJECT_FUNCTION, true }, - { "INDEX", OBJECT_INDEX, true }, - { "LANGUAGE", OBJECT_LANGUAGE, true }, - { "OPERATOR", OBJECT_OPERATOR, true }, - { "OPERATOR CLASS", OBJECT_OPCLASS, true }, - { "OPERATOR FAMILY", OBJECT_OPFAMILY, true }, - { "ROLE", OBJECT_ROLE, false }, - { "RULE", OBJECT_RULE, true }, - { "SCHEMA", OBJECT_SCHEMA, true }, - { "SEQUENCE", OBJECT_SEQUENCE, true }, - { "TABLE", OBJECT_TABLE, true }, - { "TABLESPACE", OBJECT_TABLESPACE, false}, - { "TRIGGER", OBJECT_TRIGGER, true }, - { "TEXT SEARCH CONFIGURATION", OBJECT_TSCONFIGURATION, true }, - { "TEXT SEARCH DICTIONARY", OBJECT_TSDICTIONARY, true }, - { "TEXT SEARCH PARSER", OBJECT_TSPARSER, true }, - { "TEXT SEARCH TEMPLATE", OBJECT_TSTEMPLATE, true }, - { "TYPE", OBJECT_TYPE, true }, - { "VIEW", OBJECT_VIEW, true }, - { NULL, (ObjectType) 0, false } + { "AGGREGATE", true }, + { "CAST", true }, + { "CONSTRAINT", true }, + { "COLLATION", true }, + { "CONVERSION", true }, + { "DATABASE", false }, + { "DOMAIN", true }, + { "EXTENSION", true }, + { "EVENT TRIGGER", false }, + { "FOREIGN DATA WRAPPER", true }, + { "FOREIGN TABLE", true }, + { "FUNCTION", true }, + { "INDEX", true }, + { "LANGUAGE", true }, + { "OPERATOR", true }, + { "OPERATOR CLASS", true }, + { "OPERATOR FAMILY", true }, + { "ROLE", false }, + { "RULE", true }, + { "SCHEMA", true }, + { "SEQUENCE", true }, + { "SERVER", true }, + { "TABLE", true }, + { "TABLESPACE", false}, + { "TRIGGER", true }, + { "TEXT SEARCH CONFIGURATION", true }, + { "TEXT SEARCH DICTIONARY", true }, + { "TEXT SEARCH PARSER", true }, + { "TEXT SEARCH TEMPLATE", true }, + { "TYPE", true }, + { "USER MAPPING", true }, + { "VIEW", true }, + { NULL, false } }; static void AlterEventTriggerOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId); +static event_trigger_command_tag_check_result check_ddl_tag(const char *tag); static void error_duplicate_filter_variable(const char *defname); -static void error_unrecognized_filter_value(const char *var, const char *val); static Datum filter_list_to_array(List *filterlist); static void insert_event_trigger_tuple(char *trigname, char *eventname, Oid evtOwner, Oid funcoid, List *tags); static void validate_ddl_tags(const char *filtervar, List *taglist); +static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata); /* * Create an event trigger. @@ -177,38 +187,15 @@ validate_ddl_tags(const char *filtervar, List *taglist) foreach (lc, taglist) { const char *tag = strVal(lfirst(lc)); - const char *obtypename = NULL; - event_trigger_support_data *etsd; - - /* - * As a special case, SELECT INTO is considered DDL, since it creates - * a table. - */ - if (strcmp(tag, "SELECT INTO") == 0) - continue; - + event_trigger_command_tag_check_result result; - /* - * Otherwise, it should be CREATE, ALTER, or DROP. - */ - if (pg_strncasecmp(tag, "CREATE ", 7) == 0) - obtypename = tag + 7; - else if (pg_strncasecmp(tag, "ALTER ", 6) == 0) - obtypename = tag + 6; - else if (pg_strncasecmp(tag, "DROP ", 5) == 0) - obtypename = tag + 5; - if (obtypename == NULL) - error_unrecognized_filter_value(filtervar, tag); - - /* - * ...and the object type should be something recognizable. - */ - for (etsd = event_trigger_support; etsd->obtypename != NULL; etsd++) - if (pg_strcasecmp(etsd->obtypename, obtypename) == 0) - break; - if (etsd->obtypename == NULL) - error_unrecognized_filter_value(filtervar, tag); - if (!etsd->supported) + result = check_ddl_tag(tag); + if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("filter value \"%s\" not recognized for filter variable \"%s\"", + tag, filtervar))); + if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), /* translator: %s represents an SQL statement name */ @@ -217,6 +204,46 @@ validate_ddl_tags(const char *filtervar, List *taglist) } } +static event_trigger_command_tag_check_result +check_ddl_tag(const char *tag) +{ + const char *obtypename; + event_trigger_support_data *etsd; + + /* + * Handle some idiosyncratic special cases. + */ + if (pg_strcasecmp(tag, "CREATE TABLE AS") == 0 || + pg_strcasecmp(tag, "SELECT INTO") == 0 || + pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 || + pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0) + return EVENT_TRIGGER_COMMAND_TAG_OK; + + /* + * Otherwise, command should be CREATE, ALTER, or DROP. + */ + if (pg_strncasecmp(tag, "CREATE ", 7) == 0) + obtypename = tag + 7; + else if (pg_strncasecmp(tag, "ALTER ", 6) == 0) + obtypename = tag + 6; + else if (pg_strncasecmp(tag, "DROP ", 5) == 0) + obtypename = tag + 5; + else + return EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED; + + /* + * ...and the object type should be something recognizable. + */ + for (etsd = event_trigger_support; etsd->obtypename != NULL; etsd++) + if (pg_strcasecmp(etsd->obtypename, obtypename) == 0) + break; + if (etsd->obtypename == NULL) + return EVENT_TRIGGER_COMMAND_TAG_NOT_RECOGNIZED; + if (!etsd->supported) + return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED; + return EVENT_TRIGGER_COMMAND_TAG_OK; +} + /* * Complain about a duplicate filter variable. */ @@ -230,18 +257,6 @@ error_duplicate_filter_variable(const char *defname) } /* - * Complain about an invalid filter value. - */ -static void -error_unrecognized_filter_value(const char *var, const char *val) -{ - ereport(ERROR, - (errcode(ERRCODE_SYNTAX_ERROR), - errmsg("filter value \"%s\" not recognized for filter variable \"%s\"", - val, var))); -} - -/* * Insert the new pg_event_trigger row and record dependencies. */ static void @@ -537,3 +552,171 @@ get_event_trigger_oid(const char *trigname, bool missing_ok) errmsg("event trigger \"%s\" does not exist", trigname))); return oid; } + +/* + * Fire ddl_command_start triggers. + */ +void +EventTriggerDDLCommandStart(Node *parsetree) +{ + List *cachelist; + List *runlist = NIL; + ListCell *lc; + const char *tag; + EventTriggerData trigdata; + + /* + * We want the list of command tags for which this procedure is actually + * invoked to match up exactly with the list that CREATE EVENT TRIGGER + * accepts. This debugging cross-check will throw an error if this + * function is invoked for a command tag that CREATE EVENT TRIGGER won't + * accept. (Unfortunately, there doesn't seem to be any simple, automated + * way to verify that CREATE EVENT TRIGGER doesn't accept extra stuff that + * never reaches this control point.) + * + * If this cross-check fails for you, you probably need to either adjust + * standard_ProcessUtility() not to invoke event triggers for the command + * type in question, or you need to adjust check_ddl_tag to accept the + * relevant command tag. + */ +#ifdef USE_ASSERT_CHECKING + if (assert_enabled) + { + const char *dbgtag; + + dbgtag = CreateCommandTag(parsetree); + if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK) + elog(ERROR, "unexpected command tag \"%s\"", dbgtag); + } +#endif + + /* Use cache to find triggers for this event; fast exit if none. */ + cachelist = EventCacheLookup(EVT_DDLCommandStart); + if (cachelist == NULL) + return; + + /* Get the command tag. */ + tag = CreateCommandTag(parsetree); + + /* + * 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. + */ + foreach (lc, cachelist) + { + EventTriggerCacheItem *item = lfirst(lc); + + /* Filter by session replication role. */ + if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA) + { + if (item->enabled == TRIGGER_FIRES_ON_ORIGIN) + continue; + } + else + { + if (item->enabled == TRIGGER_FIRES_ON_REPLICA) + continue; + } + + /* Filter by tags, if any were specified. */ + if (item->ntags != 0 && bsearch(&tag, item->tag, + item->ntags, sizeof(char *), + pg_qsort_strcmp) == NULL) + continue; + + /* We must plan to fire this trigger. */ + runlist = lappend_oid(runlist, item->fnoid); + } + + /* Construct event trigger data. */ + trigdata.type = T_EventTriggerData; + trigdata.event = "ddl_command_start"; + trigdata.parsetree = parsetree; + trigdata.tag = tag; + + /* Run the triggers. */ + EventTriggerInvoke(runlist, &trigdata); + + /* Cleanup. */ + list_free(runlist); +} + +/* + * Invoke each event trigger in a list of event triggers. + */ +static void +EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata) +{ + MemoryContext context; + MemoryContext oldcontext; + ListCell *lc; + + /* + * Let's evaluate event triggers in their own memory context, so + * that any leaks get cleaned up promptly. + */ + context = AllocSetContextCreate(CurrentMemoryContext, + "event trigger context", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + oldcontext = MemoryContextSwitchTo(context); + + /* Call each event trigger. */ + foreach (lc, fn_oid_list) + { + Oid fnoid = lfirst_oid(lc); + FmgrInfo flinfo; + FunctionCallInfoData fcinfo; + PgStat_FunctionCallUsage fcusage; + + /* Look up the function */ + fmgr_info(fnoid, &flinfo); + + /* Call the function, passing no arguments but setting a context. */ + InitFunctionCallInfoData(fcinfo, &flinfo, 0, + InvalidOid, (Node *) trigdata, NULL); + pgstat_init_function_usage(&fcinfo, &fcusage); + FunctionCallInvoke(&fcinfo); + pgstat_end_function_usage(&fcusage, true); + + /* Reclaim memory. */ + MemoryContextReset(context); + + /* + * We want each event trigger to be able to see the results of + * the previous event trigger's action, and we want the main + * command to be able to see the results of all event triggers. + */ + CommandCounterIncrement(); + } + + /* Restore old memory context and delete the temporary one. */ + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(context); +} + +/* + * Do event triggers support this object type? + */ +bool +EventTriggerSupportsObjectType(ObjectType obtype) +{ + switch (obtype) + { + case OBJECT_DATABASE: + case OBJECT_TABLESPACE: + case OBJECT_ROLE: + /* no support for global objects */ + return false; + case OBJECT_EVENT_TRIGGER: + /* no support for event triggers on event triggers */ + return false; + default: + break; + } + return true; +} diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c index 5d6bc7a118e..f1948f4d7c2 100644 --- a/src/backend/commands/extension.c +++ b/src/backend/commands/extension.c @@ -749,9 +749,9 @@ execute_sql_string(const char *sql, const char *filename) ProcessUtility(stmt, sql, NULL, - false, /* not top level */ dest, - NULL); + NULL, + PROCESS_UTILITY_QUERY); } PopActiveSnapshot(); diff --git a/src/backend/commands/schemacmds.c b/src/backend/commands/schemacmds.c index 6745af501d4..4974025cc33 100644 --- a/src/backend/commands/schemacmds.c +++ b/src/backend/commands/schemacmds.c @@ -132,9 +132,9 @@ CreateSchemaCommand(CreateSchemaStmt *stmt, const char *queryString) ProcessUtility(stmt, queryString, NULL, - false, /* not top level */ None_Receiver, - NULL); + NULL, + PROCESS_UTILITY_SUBCOMMAND); /* make sure later steps can see the object created here */ CommandCounterIncrement(); } diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index faa7f0c4c50..1d5951ad3da 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -1026,7 +1026,7 @@ ConvertTriggerToFK(CreateTrigStmt *stmt, Oid funcoid) /* ... and execute it */ ProcessUtility((Node *) atstmt, "(generated ALTER TABLE ADD FOREIGN KEY command)", - NULL, false, None_Receiver, NULL); + NULL, None_Receiver, NULL, PROCESS_UTILITY_GENERATED); /* Remove the matched item from the list */ info_list = list_delete_ptr(info_list, info); diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c index bf2f5c68829..fc0bcb4bced 100644 --- a/src/backend/executor/functions.c +++ b/src/backend/executor/functions.c @@ -783,9 +783,9 @@ postquel_getnext(execution_state *es, SQLFunctionCachePtr fcache) es->qd->utilitystmt), fcache->src, es->qd->params, - false, /* not top level */ es->qd->dest, - NULL); + NULL, + PROCESS_UTILITY_QUERY); result = true; /* never stops early */ } else diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c index e222365d111..7c0da8873a7 100644 --- a/src/backend/executor/spi.c +++ b/src/backend/executor/spi.c @@ -1912,9 +1912,9 @@ _SPI_execute_plan(SPIPlanPtr plan, ParamListInfo paramLI, ProcessUtility(stmt, plansource->query_string, paramLI, - false, /* not top level */ dest, - completionTag); + completionTag, + PROCESS_UTILITY_QUERY); /* Update "processed" if stmt returned tuples */ if (_SPI_current->tuptable) diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index d0db7ad62c2..2cb9a8ee2fa 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -1186,9 +1186,10 @@ PortalRunUtility(Portal portal, Node *utilityStmt, bool isTopLevel, ProcessUtility(utilityStmt, portal->sourceText, portal->portalParams, - isTopLevel, dest, - completionTag); + completionTag, + isTopLevel ? + PROCESS_UTILITY_TOPLEVEL : PROCESS_UTILITY_QUERY); /* Some utility statements may change context on us */ MemoryContextSwitchTo(PortalGetHeapMemory(portal)); diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index 4438a3daf8a..ce50560dd6b 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -320,9 +320,9 @@ void ProcessUtility(Node *parsetree, const char *queryString, ParamListInfo params, - bool isTopLevel, DestReceiver *dest, - char *completionTag) + char *completionTag, + ProcessUtilityContext context) { Assert(queryString != NULL); /* required as of 8.4 */ @@ -333,20 +333,23 @@ ProcessUtility(Node *parsetree, */ if (ProcessUtility_hook) (*ProcessUtility_hook) (parsetree, queryString, params, - isTopLevel, dest, completionTag); + dest, completionTag, context); else standard_ProcessUtility(parsetree, queryString, params, - isTopLevel, dest, completionTag); + dest, completionTag, context); } void standard_ProcessUtility(Node *parsetree, const char *queryString, ParamListInfo params, - bool isTopLevel, DestReceiver *dest, - char *completionTag) + char *completionTag, + ProcessUtilityContext context) { + bool isTopLevel = (context == PROCESS_UTILITY_TOPLEVEL); + bool isCompleteQuery = (context <= PROCESS_UTILITY_QUERY); + check_xact_readonly(parsetree); if (completionTag) @@ -503,6 +506,8 @@ standard_ProcessUtility(Node *parsetree, * relation and attribute manipulation */ case T_CreateSchemaStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateSchemaCommand((CreateSchemaStmt *) parsetree, queryString); break; @@ -514,6 +519,9 @@ standard_ProcessUtility(Node *parsetree, ListCell *l; Oid relOid; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + /* Run parse analysis ... */ stmts = transformCreateStmt((CreateStmt *) parsetree, queryString); @@ -565,9 +573,9 @@ standard_ProcessUtility(Node *parsetree, ProcessUtility(stmt, queryString, params, - false, None_Receiver, - NULL); + NULL, + PROCESS_UTILITY_GENERATED); } /* Need CCI between commands */ @@ -578,79 +586,110 @@ standard_ProcessUtility(Node *parsetree, break; case T_CreateTableSpaceStmt: + /* no event triggers for global objects */ PreventTransactionChain(isTopLevel, "CREATE TABLESPACE"); CreateTableSpace((CreateTableSpaceStmt *) parsetree); break; case T_DropTableSpaceStmt: + /* no event triggers for global objects */ PreventTransactionChain(isTopLevel, "DROP TABLESPACE"); DropTableSpace((DropTableSpaceStmt *) parsetree); break; case T_AlterTableSpaceOptionsStmt: + /* no event triggers for global objects */ AlterTableSpaceOptions((AlterTableSpaceOptionsStmt *) parsetree); break; case T_CreateExtensionStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateExtension((CreateExtensionStmt *) parsetree); break; case T_AlterExtensionStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); ExecAlterExtensionStmt((AlterExtensionStmt *) parsetree); break; case T_AlterExtensionContentsStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); ExecAlterExtensionContentsStmt((AlterExtensionContentsStmt *) parsetree); break; case T_CreateFdwStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateForeignDataWrapper((CreateFdwStmt *) parsetree); break; case T_AlterFdwStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterForeignDataWrapper((AlterFdwStmt *) parsetree); break; case T_CreateForeignServerStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateForeignServer((CreateForeignServerStmt *) parsetree); break; case T_AlterForeignServerStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterForeignServer((AlterForeignServerStmt *) parsetree); break; case T_CreateUserMappingStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateUserMapping((CreateUserMappingStmt *) parsetree); break; case T_AlterUserMappingStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterUserMapping((AlterUserMappingStmt *) parsetree); break; case T_DropUserMappingStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); RemoveUserMapping((DropUserMappingStmt *) parsetree); break; case T_DropStmt: - switch (((DropStmt *) parsetree)->removeType) { - case OBJECT_INDEX: - if (((DropStmt *) parsetree)->concurrent) - PreventTransactionChain(isTopLevel, - "DROP INDEX CONCURRENTLY"); - /* fall through */ + DropStmt *stmt = (DropStmt *) parsetree; - case OBJECT_TABLE: - case OBJECT_SEQUENCE: - case OBJECT_VIEW: - case OBJECT_FOREIGN_TABLE: - RemoveRelations((DropStmt *) parsetree); - break; - default: - RemoveObjects((DropStmt *) parsetree); - break; + if (isCompleteQuery + && EventTriggerSupportsObjectType(stmt->removeType)) + EventTriggerDDLCommandStart(parsetree); + + 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: + case OBJECT_FOREIGN_TABLE: + RemoveRelations((DropStmt *) parsetree); + break; + default: + RemoveObjects((DropStmt *) parsetree); + break; + } + break; } - break; case T_TruncateStmt: ExecuteTruncate((TruncateStmt *) parsetree); @@ -695,16 +734,40 @@ standard_ProcessUtility(Node *parsetree, * schema */ case T_RenameStmt: - ExecRenameStmt((RenameStmt *) parsetree); - break; + { + RenameStmt *stmt; + + stmt = (RenameStmt *) parsetree; + if (isCompleteQuery && + EventTriggerSupportsObjectType(stmt->renameType)) + EventTriggerDDLCommandStart(parsetree); + ExecRenameStmt(stmt); + break; + } case T_AlterObjectSchemaStmt: - ExecAlterObjectSchemaStmt((AlterObjectSchemaStmt *) parsetree); - break; + { + AlterObjectSchemaStmt *stmt; + + stmt = (AlterObjectSchemaStmt *) parsetree; + if (isCompleteQuery && + EventTriggerSupportsObjectType(stmt->objectType)) + EventTriggerDDLCommandStart(parsetree); + ExecAlterObjectSchemaStmt(stmt); + break; + } case T_AlterOwnerStmt: - ExecAlterOwnerStmt((AlterOwnerStmt *) parsetree); - break; + { + AlterOwnerStmt *stmt; + + stmt = (AlterOwnerStmt *) parsetree; + if (isCompleteQuery && + EventTriggerSupportsObjectType(stmt->objectType)) + EventTriggerDDLCommandStart(parsetree); + ExecAlterOwnerStmt(stmt); + break; + } case T_AlterTableStmt: { @@ -714,6 +777,9 @@ standard_ProcessUtility(Node *parsetree, ListCell *l; LOCKMODE lockmode; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + /* * Figure out lock mode, and acquire lock. This also does * basic permissions checks, so that we won't wait for a lock @@ -744,9 +810,9 @@ standard_ProcessUtility(Node *parsetree, ProcessUtility(stmt, queryString, params, - false, None_Receiver, - NULL); + NULL, + PROCESS_UTILITY_GENERATED); } /* Need CCI between commands */ @@ -765,6 +831,9 @@ standard_ProcessUtility(Node *parsetree, { AlterDomainStmt *stmt = (AlterDomainStmt *) parsetree; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + /* * Some or all of these functions are recursive to cover * inherited things, so permission checks are done there. @@ -819,6 +888,8 @@ standard_ProcessUtility(Node *parsetree, break; case T_AlterDefaultPrivilegesStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); ExecAlterDefaultPrivilegesStmt((AlterDefaultPrivilegesStmt *) parsetree); break; @@ -829,6 +900,9 @@ standard_ProcessUtility(Node *parsetree, { DefineStmt *stmt = (DefineStmt *) parsetree; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + switch (stmt->kind) { case OBJECT_AGGREGATE: @@ -875,19 +949,28 @@ standard_ProcessUtility(Node *parsetree, { CompositeTypeStmt *stmt = (CompositeTypeStmt *) parsetree; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); + DefineCompositeType(stmt->typevar, stmt->coldeflist); } break; case T_CreateEnumStmt: /* CREATE TYPE AS ENUM */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineEnum((CreateEnumStmt *) parsetree); break; case T_CreateRangeStmt: /* CREATE TYPE AS RANGE */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineRange((CreateRangeStmt *) parsetree); break; case T_AlterEnumStmt: /* ALTER TYPE (enum) */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); /* * We disallow this in transaction blocks, because we can't cope @@ -899,14 +982,20 @@ standard_ProcessUtility(Node *parsetree, break; case T_ViewStmt: /* CREATE VIEW */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineView((ViewStmt *) parsetree, queryString); break; case T_CreateFunctionStmt: /* CREATE FUNCTION */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateFunction((CreateFunctionStmt *) parsetree, queryString); break; case T_AlterFunctionStmt: /* ALTER FUNCTION */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterFunction((AlterFunctionStmt *) parsetree); break; @@ -914,6 +1003,8 @@ standard_ProcessUtility(Node *parsetree, { IndexStmt *stmt = (IndexStmt *) parsetree; + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); if (stmt->concurrent) PreventTransactionChain(isTopLevel, "CREATE INDEX CONCURRENTLY"); @@ -934,14 +1025,20 @@ standard_ProcessUtility(Node *parsetree, break; case T_RuleStmt: /* CREATE RULE */ + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineRule((RuleStmt *) parsetree, queryString); break; case T_CreateSeqStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineSequence((CreateSeqStmt *) parsetree); break; case T_AlterSeqStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterSequence((AlterSeqStmt *) parsetree); break; @@ -950,15 +1047,18 @@ standard_ProcessUtility(Node *parsetree, break; case T_CreatedbStmt: + /* no event triggers for global objects */ PreventTransactionChain(isTopLevel, "CREATE DATABASE"); createdb((CreatedbStmt *) parsetree); break; case T_AlterDatabaseStmt: + /* no event triggers for global objects */ AlterDatabase((AlterDatabaseStmt *) parsetree, isTopLevel); break; case T_AlterDatabaseSetStmt: + /* no event triggers for global objects */ AlterDatabaseSet((AlterDatabaseSetStmt *) parsetree); break; @@ -966,6 +1066,7 @@ standard_ProcessUtility(Node *parsetree, { DropdbStmt *stmt = (DropdbStmt *) parsetree; + /* no event triggers for global objects */ PreventTransactionChain(isTopLevel, "DROP DATABASE"); dropdb(stmt->dbname, stmt->missing_ok); } @@ -1032,6 +1133,8 @@ standard_ProcessUtility(Node *parsetree, break; case T_CreateTableAsStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); ExecCreateTableAs((CreateTableAsStmt *) parsetree, queryString, params, completionTag); break; @@ -1055,19 +1158,25 @@ standard_ProcessUtility(Node *parsetree, break; case T_CreateTrigStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); (void) CreateTrigger((CreateTrigStmt *) parsetree, queryString, InvalidOid, InvalidOid, false); break; case T_CreateEventTrigStmt: + /* no event triggers on event triggers */ CreateEventTrigger((CreateEventTrigStmt *) parsetree); break; case T_AlterEventTrigStmt: + /* no event triggers on event triggers */ AlterEventTrigger((AlterEventTrigStmt *) parsetree); break; case T_CreatePLangStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateProceduralLanguage((CreatePLangStmt *) parsetree); break; @@ -1075,6 +1184,8 @@ standard_ProcessUtility(Node *parsetree, * ******************************** DOMAIN statements **** */ case T_CreateDomainStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineDomain((CreateDomainStmt *) parsetree); break; @@ -1082,26 +1193,32 @@ standard_ProcessUtility(Node *parsetree, * ******************************** ROLE statements **** */ case T_CreateRoleStmt: + /* no event triggers for global objects */ CreateRole((CreateRoleStmt *) parsetree); break; case T_AlterRoleStmt: + /* no event triggers for global objects */ AlterRole((AlterRoleStmt *) parsetree); break; case T_AlterRoleSetStmt: + /* no event triggers for global objects */ AlterRoleSet((AlterRoleSetStmt *) parsetree); break; case T_DropRoleStmt: + /* no event triggers for global objects */ DropRole((DropRoleStmt *) parsetree); break; case T_DropOwnedStmt: + /* no event triggers for global objects */ DropOwnedObjects((DropOwnedStmt *) parsetree); break; case T_ReassignOwnedStmt: + /* no event triggers for global objects */ ReassignOwnedObjects((ReassignOwnedStmt *) parsetree); break; @@ -1173,30 +1290,44 @@ standard_ProcessUtility(Node *parsetree, break; case T_CreateConversionStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateConversionCommand((CreateConversionStmt *) parsetree); break; case T_CreateCastStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); CreateCast((CreateCastStmt *) parsetree); break; case T_CreateOpClassStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineOpClass((CreateOpClassStmt *) parsetree); break; case T_CreateOpFamilyStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); DefineOpFamily((CreateOpFamilyStmt *) parsetree); break; case T_AlterOpFamilyStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterOpFamily((AlterOpFamilyStmt *) parsetree); break; case T_AlterTSDictionaryStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterTSDictionary((AlterTSDictionaryStmt *) parsetree); break; case T_AlterTSConfigurationStmt: + if (isCompleteQuery) + EventTriggerDDLCommandStart(parsetree); AlterTSConfiguration((AlterTSConfigurationStmt *) parsetree); break; diff --git a/src/backend/tsearch/ts_utils.c b/src/backend/tsearch/ts_utils.c index 6a4888e5f46..a2f920c656a 100644 --- a/src/backend/tsearch/ts_utils.c +++ b/src/backend/tsearch/ts_utils.c @@ -59,12 +59,6 @@ get_tsearch_config_filename(const char *basename, return result; } -static int -comparestr(const void *a, const void *b) -{ - return strcmp(*(char *const *) a, *(char *const *) b); -} - /* * Reads a stop-word file. Each word is run through 'wordop' * function, if given. wordop may either modify the input in-place, @@ -140,7 +134,7 @@ readstoplist(const char *fname, StopList *s, char *(*wordop) (const char *)) /* Sort to allow binary searching */ if (s->stop && s->len > 0) - qsort(s->stop, s->len, sizeof(char *), comparestr); + qsort(s->stop, s->len, sizeof(char *), pg_qsort_strcmp); } bool @@ -148,5 +142,5 @@ searchstoplist(StopList *s, char *key) { return (s->stop && s->len > 0 && bsearch(&key, s->stop, s->len, - sizeof(char *), comparestr)) ? true : false; + sizeof(char *), pg_qsort_strcmp)) ? true : false; } diff --git a/src/backend/utils/cache/Makefile b/src/backend/utils/cache/Makefile index a1a539383b4..32d722e34f6 100644 --- a/src/backend/utils/cache/Makefile +++ b/src/backend/utils/cache/Makefile @@ -12,7 +12,7 @@ subdir = src/backend/utils/cache top_builddir = ../../../.. include $(top_builddir)/src/Makefile.global -OBJS = attoptcache.o catcache.o inval.o plancache.o relcache.o relmapper.o \ - spccache.o syscache.o lsyscache.o typcache.o ts_cache.o +OBJS = attoptcache.o catcache.o evtcache.o inval.o plancache.o relcache.o \ + relmapper.o spccache.o syscache.o lsyscache.o typcache.o ts_cache.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/utils/cache/evtcache.c b/src/backend/utils/cache/evtcache.c new file mode 100644 index 00000000000..565dc449d16 --- /dev/null +++ b/src/backend/utils/cache/evtcache.c @@ -0,0 +1,242 @@ +/*------------------------------------------------------------------------- + * + * evtcache.c + * Special-purpose cache for event trigger data. + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/cache/evtcache.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "catalog/pg_event_trigger.h" +#include "catalog/indexing.h" +#include "catalog/pg_type.h" +#include "commands/trigger.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/evtcache.h" +#include "utils/inval.h" +#include "utils/memutils.h" +#include "utils/hsearch.h" +#include "utils/rel.h" +#include "utils/snapmgr.h" +#include "utils/syscache.h" + +typedef struct +{ + EventTriggerEvent event; + List *triggerlist; +} EventTriggerCacheEntry; + +static HTAB *EventTriggerCache; +static MemoryContext EventTriggerCacheContext; + +static void BuildEventTriggerCache(void); +static void InvalidateEventCacheCallback(Datum arg, + int cacheid, uint32 hashvalue); +static int DecodeTextArrayToCString(Datum array, char ***cstringp); + +/* + * Search the event cache by trigger event. + * + * Note that the caller had better copy any data it wants to keep around + * across any operation that might touch a system catalog into some other + * memory context, since a cache reset could blow the return value away. + */ +List * +EventCacheLookup(EventTriggerEvent event) +{ + EventTriggerCacheEntry *entry; + + if (EventTriggerCache == NULL) + BuildEventTriggerCache(); + entry = hash_search(EventTriggerCache, &event, HASH_FIND, NULL); + return entry != NULL ? entry->triggerlist : NULL; +} + +/* + * Rebuild the event trigger cache. + */ +static void +BuildEventTriggerCache(void) +{ + HASHCTL ctl; + HTAB *cache; + MemoryContext oldcontext; + Relation rel; + Relation irel; + SysScanDesc scan; + + if (EventTriggerCacheContext != NULL) + { + /* + * The cache has been previously built, and subsequently invalidated, + * and now we're trying to rebuild it. Normally, there won't be + * anything in the context at this point, because the invalidation + * will have already reset it. But if the previous attempt to rebuild + * the cache failed, then there might be some junk lying around + * that we want to reclaim. + */ + MemoryContextReset(EventTriggerCacheContext); + } + else + { + /* + * This is our first time attempting to build the cache, so we need + * to set up the memory context and register a syscache callback to + * capture future invalidation events. + */ + if (CacheMemoryContext == NULL) + CreateCacheMemoryContext(); + EventTriggerCacheContext = + AllocSetContextCreate(CacheMemoryContext, + "EventTriggerCache", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + CacheRegisterSyscacheCallback(EVENTTRIGGEROID, + InvalidateEventCacheCallback, + (Datum) 0); + } + + /* Switch to correct memory context. */ + oldcontext = MemoryContextSwitchTo(EventTriggerCacheContext); + + /* + * Create a new hash table, but don't assign it to the global variable + * until it accurately represents the state of the catalogs, so that + * if we fail midway through this we won't end up with incorrect cache + * contents. + */ + MemSet(&ctl, 0, sizeof(ctl)); + ctl.keysize = sizeof(EventTriggerEvent); + ctl.entrysize = sizeof(EventTriggerCacheEntry); + ctl.hash = tag_hash; + cache = hash_create("Event Trigger Cache", 32, &ctl, + HASH_ELEM | HASH_FUNCTION); + + /* + * Prepare to scan pg_event_trigger in name order. We use an MVCC + * snapshot to avoid getting inconsistent results if the table is + * being concurrently updated. + */ + rel = relation_open(EventTriggerRelationId, AccessShareLock); + irel = index_open(EventTriggerNameIndexId, AccessShareLock); + scan = systable_beginscan_ordered(rel, irel, GetLatestSnapshot(), 0, NULL); + + /* + * Build a cache item for each pg_event_trigger tuple, and append each + * one to the appropriate cache entry. + */ + for (;;) + { + HeapTuple tup; + Form_pg_event_trigger form; + char *evtevent; + EventTriggerEvent event; + EventTriggerCacheItem *item; + Datum evttags; + bool evttags_isnull; + EventTriggerCacheEntry *entry; + bool found; + + /* Get next tuple. */ + tup = systable_getnext_ordered(scan, ForwardScanDirection); + if (!HeapTupleIsValid(tup)) + break; + + /* Skip trigger if disabled. */ + form = (Form_pg_event_trigger) GETSTRUCT(tup); + if (form->evtenabled == TRIGGER_DISABLED) + continue; + + /* Decode event name. */ + evtevent = NameStr(form->evtevent); + if (strcmp(evtevent, "ddl_command_start") == 0) + event = EVT_DDLCommandStart; + else + continue; + + /* Allocate new cache item. */ + item = palloc0(sizeof(EventTriggerCacheItem)); + item->fnoid = form->evtfoid; + item->enabled = form->evtenabled; + + /* Decode and sort tags array. */ + evttags = heap_getattr(tup, Anum_pg_event_trigger_evttags, + RelationGetDescr(rel), &evttags_isnull); + if (!evttags_isnull) + { + item->ntags = DecodeTextArrayToCString(evttags, &item->tag); + qsort(item->tag, item->ntags, sizeof(char *), pg_qsort_strcmp); + } + + /* Add to cache entry. */ + entry = hash_search(cache, &event, HASH_ENTER, &found); + if (found) + entry->triggerlist = lappend(entry->triggerlist, item); + else + entry->triggerlist = list_make1(item); + } + + /* Done with pg_event_trigger scan. */ + systable_endscan_ordered(scan); + index_close(irel, AccessShareLock); + relation_close(rel, AccessShareLock); + + /* Restore previous memory context. */ + MemoryContextSwitchTo(oldcontext); + + /* Cache is now valid. */ + EventTriggerCache = cache; +} + +/* + * Decode text[] to an array of C strings. + * + * We could avoid a bit of overhead here if we were willing to duplicate some + * of the logic from deconstruct_array, but it doesn't seem worth the code + * complexity. + */ +static int +DecodeTextArrayToCString(Datum array, char ***cstringp) +{ + ArrayType *arr = DatumGetArrayTypeP(array); + Datum *elems; + char **cstring; + int i; + int nelems; + + if (ARR_NDIM(arr) != 1 || ARR_HASNULL(arr) || ARR_ELEMTYPE(arr) != TEXTOID) + elog(ERROR, "expected 1-D text array"); + deconstruct_array(arr, TEXTOID, -1, false, 'i', &elems, NULL, &nelems); + + cstring = palloc(nelems * sizeof(char *)); + for (i = 0; i < nelems; ++i) + cstring[i] = TextDatumGetCString(elems[i]); + + pfree(elems); + *cstringp = cstring; + return nelems; +} + +/* + * Flush all cache entries when pg_event_trigger is updated. + * + * This should be rare enough that we don't need to be very granular about + * it, so we just blow away everything, which also avoids the possibility of + * memory leaks. + */ +static void +InvalidateEventCacheCallback(Datum arg, int cacheid, uint32 hashvalue) +{ + MemoryContextReset(EventTriggerCacheContext); + EventTriggerCache = NULL; +} diff --git a/src/include/commands/event_trigger.h b/src/include/commands/event_trigger.h index 3ebb374939a..459d27fbbef 100644 --- a/src/include/commands/event_trigger.h +++ b/src/include/commands/event_trigger.h @@ -16,6 +16,21 @@ #include "catalog/pg_event_trigger.h" #include "nodes/parsenodes.h" +typedef struct EventTriggerData +{ + NodeTag type; + char *event; /* event name */ + Node *parsetree; /* parse tree */ + const char *tag; /* command tag */ +} EventTriggerData; + +/* + * EventTriggerData is the node type that is passed as fmgr "context" info + * when a function is called by the event trigger manager. + */ +#define CALLED_AS_EVENT_TRIGGER(fcinfo) \ + ((fcinfo)->context != NULL && IsA((fcinfo)->context, EventTriggerData)) + extern void CreateEventTrigger(CreateEventTrigStmt *stmt); extern void RemoveEventTriggerById(Oid ctrigOid); extern Oid get_event_trigger_oid(const char *trigname, bool missing_ok); @@ -25,4 +40,7 @@ extern void RenameEventTrigger(const char* trigname, const char *newname); extern void AlterEventTriggerOwner(const char *name, Oid newOwnerId); extern void AlterEventTriggerOwner_oid(Oid, Oid newOwnerId); +extern bool EventTriggerSupportsObjectType(ObjectType obtype); +extern void EventTriggerDDLCommandStart(Node *parsetree); + #endif /* EVENT_TRIGGER_H */ diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index a51657df0d4..a4c61f63076 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -415,6 +415,7 @@ typedef enum NodeTag * pass multiple object types through the same pointer). */ T_TriggerData = 950, /* in commands/trigger.h */ + T_EventTriggerData, /* in commands/event_trigger.h */ T_ReturnSetInfo, /* in nodes/execnodes.h */ T_WindowObjectData, /* private in nodeWindowAgg.c */ T_TIDBitmap, /* in nodes/tidbitmap.h */ diff --git a/src/include/port.h b/src/include/port.h index 25c4e9883d0..c429c77cd6d 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -443,6 +443,7 @@ extern int pqGethostbyname(const char *name, extern void pg_qsort(void *base, size_t nel, size_t elsize, int (*cmp) (const void *, const void *)); +extern int pg_qsort_strcmp(const void *a, const void *b); #define qsort(a,b,c,d) pg_qsort(a,b,c,d) diff --git a/src/include/tcop/utility.h b/src/include/tcop/utility.h index 54190b2f6ce..fb6b568809b 100644 --- a/src/include/tcop/utility.h +++ b/src/include/tcop/utility.h @@ -16,19 +16,27 @@ #include "tcop/tcopprot.h" +typedef enum +{ + PROCESS_UTILITY_TOPLEVEL, /* toplevel interactive command */ + PROCESS_UTILITY_QUERY, /* a complete query, but not toplevel */ + PROCESS_UTILITY_SUBCOMMAND, /* a piece of a query */ + PROCESS_UTILITY_GENERATED /* internally generated node query node */ +} ProcessUtilityContext; /* Hook for plugins to get control in ProcessUtility() */ typedef void (*ProcessUtility_hook_type) (Node *parsetree, - const char *queryString, ParamListInfo params, bool isTopLevel, - DestReceiver *dest, char *completionTag); + const char *queryString, ParamListInfo params, + DestReceiver *dest, char *completionTag, + ProcessUtilityContext context); extern PGDLLIMPORT ProcessUtility_hook_type ProcessUtility_hook; extern void ProcessUtility(Node *parsetree, const char *queryString, - ParamListInfo params, bool isTopLevel, - DestReceiver *dest, char *completionTag); + ParamListInfo params, DestReceiver *dest, char *completionTag, + ProcessUtilityContext context); extern void standard_ProcessUtility(Node *parsetree, const char *queryString, - ParamListInfo params, bool isTopLevel, - DestReceiver *dest, char *completionTag); + ParamListInfo params, DestReceiver *dest, + char *completionTag, ProcessUtilityContext context); extern bool UtilityReturnsTuples(Node *parsetree); diff --git a/src/include/utils/evtcache.h b/src/include/utils/evtcache.h new file mode 100644 index 00000000000..004b92ae153 --- /dev/null +++ b/src/include/utils/evtcache.h @@ -0,0 +1,34 @@ +/*------------------------------------------------------------------------- + * + * evtcache.c + * Special-purpose cache for event trigger data. + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/utils/cache/evtcache.c + * + *------------------------------------------------------------------------- + */ +#ifndef EVTCACHE_H +#define EVTCACHE_H + +#include "nodes/pg_list.h" + +typedef enum +{ + EVT_DDLCommandStart +} EventTriggerEvent; + +typedef struct +{ + Oid fnoid; /* function to be called */ + char enabled; /* as SESSION_REPLICATION_ROLE_* */ + int ntags; /* number of command tags */ + char **tag; /* command tags in SORTED order */ +} EventTriggerCacheItem; + +extern List *EventCacheLookup(EventTriggerEvent event); + +#endif /* EVTCACHE_H */ diff --git a/src/pl/plpgsql/src/pl_comp.c b/src/pl/plpgsql/src/pl_comp.c index 5d2f818dacb..0dc0e0b37ed 100644 --- a/src/pl/plpgsql/src/pl_comp.c +++ b/src/pl/plpgsql/src/pl_comp.c @@ -263,7 +263,8 @@ do_compile(FunctionCallInfo fcinfo, bool forValidator) { Form_pg_proc procStruct = (Form_pg_proc) GETSTRUCT(procTup); - bool is_trigger = CALLED_AS_TRIGGER(fcinfo); + bool is_dml_trigger = CALLED_AS_TRIGGER(fcinfo); + bool is_event_trigger = CALLED_AS_EVENT_TRIGGER(fcinfo); Datum prosrcdatum; bool isnull; char *proc_source; @@ -345,12 +346,18 @@ do_compile(FunctionCallInfo fcinfo, function->fn_oid = fcinfo->flinfo->fn_oid; function->fn_xmin = HeapTupleHeaderGetXmin(procTup->t_data); function->fn_tid = procTup->t_self; - function->fn_is_trigger = is_trigger; function->fn_input_collation = fcinfo->fncollation; function->fn_cxt = func_cxt; function->out_param_varno = -1; /* set up for no OUT param */ function->resolve_option = plpgsql_variable_conflict; + if (is_dml_trigger) + function->fn_is_trigger = PLPGSQL_DML_TRIGGER; + else if (is_event_trigger) + function->fn_is_trigger = PLPGSQL_EVENT_TRIGGER; + else + function->fn_is_trigger = PLPGSQL_NOT_TRIGGER; + /* * Initialize the compiler, particularly the namespace stack. The * outermost namespace contains function parameters and other special @@ -367,9 +374,9 @@ do_compile(FunctionCallInfo fcinfo, sizeof(PLpgSQL_datum *) * datums_alloc); datums_last = 0; - switch (is_trigger) + switch (function->fn_is_trigger) { - case false: + case PLPGSQL_NOT_TRIGGER: /* * Fetch info about the procedure's parameters. Allocations aren't @@ -529,7 +536,7 @@ do_compile(FunctionCallInfo fcinfo, if (rettypeid == VOIDOID || rettypeid == RECORDOID) /* okay */ ; - else if (rettypeid == TRIGGEROID) + else if (rettypeid == TRIGGEROID || rettypeid == EVTTRIGGEROID) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("trigger functions can only be called as triggers"))); @@ -568,7 +575,7 @@ do_compile(FunctionCallInfo fcinfo, ReleaseSysCache(typeTup); break; - case true: + case PLPGSQL_DML_TRIGGER: /* Trigger procedure's return type is unknown yet */ function->fn_rettype = InvalidOid; function->fn_retbyval = false; @@ -672,8 +679,39 @@ do_compile(FunctionCallInfo fcinfo, break; + case PLPGSQL_EVENT_TRIGGER: + function->fn_rettype = VOIDOID; + function->fn_retbyval = false; + function->fn_retistuple = true; + function->fn_retset = false; + + /* shouldn't be any declared arguments */ + if (procStruct->pronargs != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_FUNCTION_DEFINITION), + errmsg("event trigger functions cannot have declared arguments"))); + + /* Add the variable tg_event */ + var = plpgsql_build_variable("tg_event", 0, + plpgsql_build_datatype(TEXTOID, + -1, + function->fn_input_collation), + true); + function->tg_event_varno = var->dno; + + /* Add the variable tg_tag */ + var = plpgsql_build_variable("tg_tag", 0, + plpgsql_build_datatype(TEXTOID, + -1, + function->fn_input_collation), + true); + function->tg_tag_varno = var->dno; + + break; + default: - elog(ERROR, "unrecognized function typecode: %d", (int) is_trigger); + elog(ERROR, "unrecognized function typecode: %d", + (int) function->fn_is_trigger); break; } @@ -803,7 +841,7 @@ plpgsql_compile_inline(char *proc_source) compile_tmp_cxt = MemoryContextSwitchTo(func_cxt); function->fn_signature = pstrdup(func_name); - function->fn_is_trigger = false; + function->fn_is_trigger = PLPGSQL_NOT_TRIGGER; function->fn_input_collation = InvalidOid; function->fn_cxt = func_cxt; function->out_param_varno = -1; /* set up for no OUT param */ diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c index 11a56c9a8f1..8b649a4e5dc 100644 --- a/src/pl/plpgsql/src/pl_exec.c +++ b/src/pl/plpgsql/src/pl_exec.c @@ -773,6 +773,99 @@ plpgsql_exec_trigger(PLpgSQL_function *func, return rettup; } +void +plpgsql_exec_event_trigger(PLpgSQL_function *func, EventTriggerData *trigdata) +{ + PLpgSQL_execstate estate; + ErrorContextCallback plerrcontext; + int i; + int rc; + PLpgSQL_var *var; + + /* + * Setup the execution state + */ + plpgsql_estate_setup(&estate, func, NULL); + + /* + * Setup error traceback support for ereport() + */ + plerrcontext.callback = plpgsql_exec_error_callback; + plerrcontext.arg = &estate; + plerrcontext.previous = error_context_stack; + error_context_stack = &plerrcontext; + + /* + * Make local execution copies of all the datums + */ + estate.err_text = gettext_noop("during initialization of execution state"); + for (i = 0; i < estate.ndatums; i++) + estate.datums[i] = copy_plpgsql_datum(func->datums[i]); + + /* + * Assign the special tg_ variables + */ + var = (PLpgSQL_var *) (estate.datums[func->tg_event_varno]); + var->value = CStringGetTextDatum(trigdata->event); + var->isnull = false; + var->freeval = true; + + var = (PLpgSQL_var *) (estate.datums[func->tg_tag_varno]); + var->value = CStringGetTextDatum(trigdata->tag); + var->isnull = false; + var->freeval = true; + + /* + * Let the instrumentation plugin peek at this function + */ + if (*plugin_ptr && (*plugin_ptr)->func_beg) + ((*plugin_ptr)->func_beg) (&estate, func); + + /* + * Now call the toplevel block of statements + */ + estate.err_text = NULL; + estate.err_stmt = (PLpgSQL_stmt *) (func->action); + rc = exec_stmt_block(&estate, func->action); + if (rc != PLPGSQL_RC_RETURN) + { + estate.err_stmt = NULL; + estate.err_text = NULL; + + /* + * Provide a more helpful message if a CONTINUE or RAISE has been used + * outside the context it can work in. + */ + if (rc == PLPGSQL_RC_CONTINUE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("CONTINUE cannot be used outside a loop"))); + else + ereport(ERROR, + (errcode(ERRCODE_S_R_E_FUNCTION_EXECUTED_NO_RETURN_STATEMENT), + errmsg("control reached end of trigger procedure without RETURN"))); + } + + estate.err_stmt = NULL; + estate.err_text = gettext_noop("during function exit"); + + /* + * Let the instrumentation plugin peek at this function + */ + if (*plugin_ptr && (*plugin_ptr)->func_end) + ((*plugin_ptr)->func_end) (&estate, func); + + /* Clean up any leftover temporary memory */ + plpgsql_destroy_econtext(&estate); + exec_eval_cleanup(&estate); + + /* + * Pop the error context stack + */ + error_context_stack = plerrcontext.previous; + + return; +} /* * error context callback to let us supply a call-stack traceback diff --git a/src/pl/plpgsql/src/pl_handler.c b/src/pl/plpgsql/src/pl_handler.c index 022ec3f334c..2adf166164e 100644 --- a/src/pl/plpgsql/src/pl_handler.c +++ b/src/pl/plpgsql/src/pl_handler.c @@ -91,7 +91,7 @@ plpgsql_call_handler(PG_FUNCTION_ARGS) { PLpgSQL_function *func; PLpgSQL_execstate *save_cur_estate; - Datum retval; + Datum retval = 0; /* make compiler happy */ int rc; /* @@ -118,6 +118,9 @@ plpgsql_call_handler(PG_FUNCTION_ARGS) if (CALLED_AS_TRIGGER(fcinfo)) retval = PointerGetDatum(plpgsql_exec_trigger(func, (TriggerData *) fcinfo->context)); + else if (CALLED_AS_EVENT_TRIGGER(fcinfo)) + plpgsql_exec_event_trigger(func, + (EventTriggerData *) fcinfo->context); else retval = plpgsql_exec_function(func, fcinfo); } @@ -224,7 +227,8 @@ plpgsql_validator(PG_FUNCTION_ARGS) Oid *argtypes; char **argnames; char *argmodes; - bool istrigger = false; + bool is_dml_trigger = false; + bool is_event_trigger = false; int i; /* Get the new function's pg_proc entry */ @@ -242,7 +246,9 @@ plpgsql_validator(PG_FUNCTION_ARGS) /* we assume OPAQUE with no arguments means a trigger */ if (proc->prorettype == TRIGGEROID || (proc->prorettype == OPAQUEOID && proc->pronargs == 0)) - istrigger = true; + is_dml_trigger = true; + else if (proc->prorettype == EVTTRIGGEROID) + is_event_trigger = true; else if (proc->prorettype != RECORDOID && proc->prorettype != VOIDOID && !IsPolymorphicType(proc->prorettype)) @@ -273,7 +279,6 @@ plpgsql_validator(PG_FUNCTION_ARGS) { FunctionCallInfoData fake_fcinfo; FmgrInfo flinfo; - TriggerData trigdata; int rc; /* @@ -291,12 +296,20 @@ plpgsql_validator(PG_FUNCTION_ARGS) fake_fcinfo.flinfo = &flinfo; flinfo.fn_oid = funcoid; flinfo.fn_mcxt = CurrentMemoryContext; - if (istrigger) + if (is_dml_trigger) { + TriggerData trigdata; MemSet(&trigdata, 0, sizeof(trigdata)); trigdata.type = T_TriggerData; fake_fcinfo.context = (Node *) &trigdata; } + else if (is_event_trigger) + { + EventTriggerData trigdata; + MemSet(&trigdata, 0, sizeof(trigdata)); + trigdata.type = T_EventTriggerData; + fake_fcinfo.context = (Node *) &trigdata; + } /* Test-compile the function */ plpgsql_compile(&fake_fcinfo, true); diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h index b63f336825d..dcf80743b88 100644 --- a/src/pl/plpgsql/src/plpgsql.h +++ b/src/pl/plpgsql/src/plpgsql.h @@ -19,6 +19,7 @@ #include "postgres.h" #include "access/xact.h" +#include "commands/event_trigger.h" #include "commands/trigger.h" #include "executor/spi.h" @@ -675,6 +676,12 @@ typedef struct PLpgSQL_func_hashkey Oid argtypes[FUNC_MAX_ARGS]; } PLpgSQL_func_hashkey; +typedef enum PLpgSQL_trigtype +{ + PLPGSQL_DML_TRIGGER, + PLPGSQL_EVENT_TRIGGER, + PLPGSQL_NOT_TRIGGER +} PLpgSQL_trigtype; typedef struct PLpgSQL_function { /* Complete compiled function */ @@ -682,7 +689,7 @@ typedef struct PLpgSQL_function Oid fn_oid; TransactionId fn_xmin; ItemPointerData fn_tid; - bool fn_is_trigger; + PLpgSQL_trigtype fn_is_trigger; Oid fn_input_collation; PLpgSQL_func_hashkey *fn_hashkey; /* back-link to hashtable key */ MemoryContext fn_cxt; @@ -713,6 +720,10 @@ typedef struct PLpgSQL_function int tg_nargs_varno; int tg_argv_varno; + /* for event triggers */ + int tg_event_varno; + int tg_tag_varno; + PLpgSQL_resolve_option resolve_option; int ndatums; @@ -920,6 +931,8 @@ extern Datum plpgsql_exec_function(PLpgSQL_function *func, FunctionCallInfo fcinfo); extern HeapTuple plpgsql_exec_trigger(PLpgSQL_function *func, TriggerData *trigdata); +extern void plpgsql_exec_event_trigger(PLpgSQL_function *func, + EventTriggerData *trigdata); extern void plpgsql_xact_cb(XactEvent event, void *arg); extern void plpgsql_subxact_cb(SubXactEvent event, SubTransactionId mySubid, SubTransactionId parentSubid, void *arg); diff --git a/src/port/qsort.c b/src/port/qsort.c index 49d8fa7ab6c..2747df3c5a6 100644 --- a/src/port/qsort.c +++ b/src/port/qsort.c @@ -193,3 +193,12 @@ loop:SWAPINIT(a, es); } /* qsort(pn - r, r / es, es, cmp);*/ } + +/* + * qsort wrapper for strcmp. + */ +int +pg_qsort_strcmp(const void *a, const void *b) +{ + return strcmp(*(char *const *) a, *(char *const *) b); +} diff --git a/src/test/regress/expected/event_trigger.out b/src/test/regress/expected/event_trigger.out index 8073b0c3d79..5c8f323ed47 100644 --- a/src/test/regress/expected/event_trigger.out +++ b/src/test/regress/expected/event_trigger.out @@ -3,47 +3,48 @@ create event trigger regress_event_trigger on ddl_command_start execute procedure pg_backend_pid(); ERROR: function "pg_backend_pid" must return type "event_trigger" --- cheesy hack for testing purposes -create function fake_event_trigger() - returns event_trigger - language internal - as 'pg_backend_pid'; +-- OK +create function test_event_trigger() returns event_trigger as $$ +BEGIN + RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; +END +$$ language plpgsql; -- should fail, no elephant_bootstrap entry point create event trigger regress_event_trigger on elephant_bootstrap - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: unrecognized event name "elephant_bootstrap" -- OK create event trigger regress_event_trigger on ddl_command_start - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, food is not a valid filter variable create event trigger regress_event_trigger2 on ddl_command_start when food in ('sandwhich') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: unrecognized filter variable "food" -- should fail, sandwhich is not a valid command tag create event trigger regress_event_trigger2 on ddl_command_start when tag in ('sandwhich') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: filter value "sandwhich" not recognized for filter variable "tag" -- should fail, create skunkcabbage is not a valid comand tag create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table', 'create skunkcabbage') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: filter value "create skunkcabbage" not recognized for filter variable "tag" -- should fail, can't have event triggers on event triggers create event trigger regress_event_trigger2 on ddl_command_start when tag in ('DROP EVENT TRIGGER') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: event triggers are not supported for "DROP EVENT TRIGGER" -- should fail, can't have same filter variable twice create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table') and tag in ('CREATE FUNCTION') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: filter variable "tag" specified more than once -- OK create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table', 'CREATE FUNCTION') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- OK comment on event trigger regress_event_trigger is 'test comment'; -- should fail, event triggers are not schema objects @@ -53,15 +54,20 @@ ERROR: event trigger name cannot be qualified create role regression_bob; set role regression_bob; create event trigger regress_event_trigger_noperms on ddl_command_start - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); ERROR: permission denied to create event trigger "regress_event_trigger_noperms" HINT: Must be superuser to create an event trigger. reset role; -- all OK -alter event trigger regress_event_trigger disable; alter event trigger regress_event_trigger enable replica; alter event trigger regress_event_trigger enable always; alter event trigger regress_event_trigger enable; +alter event trigger regress_event_trigger disable; +-- regress_event_trigger2 should fire, but not regress_event_trigger +create table event_trigger_fire1 (a int); +NOTICE: test_event_trigger: ddl_command_start CREATE TABLE +-- but nothing should fire here +drop table event_trigger_fire1; -- alter owner to non-superuser should fail alter event trigger regress_event_trigger owner to regression_bob; ERROR: permission denied to change owner of event trigger "regress_event_trigger" @@ -86,5 +92,5 @@ 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 function fake_event_trigger(); +drop function test_event_trigger(); drop role regression_bob; diff --git a/src/test/regress/input/largeobject.source b/src/test/regress/input/largeobject.source index 807cfd7cc46..17d64a2fc38 100644 --- a/src/test/regress/input/largeobject.source +++ b/src/test/regress/input/largeobject.source @@ -12,6 +12,21 @@ CREATE TABLE lotest_stash_values (loid oid, fd integer); -- returns the large object id INSERT INTO lotest_stash_values (loid) SELECT lo_creat(42); +-- Test ALTER LARGE OBJECT +CREATE ROLE regresslo; +DO $$ + BEGIN + EXECUTE 'ALTER LARGE OBJECT ' || (select loid from lotest_stash_values) + || ' OWNER TO regresslo'; + END +$$; +SELECT + rol.rolname +FROM + lotest_stash_values s + JOIN pg_largeobject_metadata lo ON s.loid = lo.oid + JOIN pg_authid rol ON lo.lomowner = rol.oid; + -- NOTE: large objects require transactions BEGIN; @@ -163,3 +178,4 @@ SELECT lo_unlink(loid) FROM lotest_stash_values; \lo_unlink :newloid TRUNCATE lotest_stash_values; +DROP ROLE regresslo; diff --git a/src/test/regress/output/largeobject.source b/src/test/regress/output/largeobject.source index d7468bb5131..14ae4c6cdc8 100644 --- a/src/test/regress/output/largeobject.source +++ b/src/test/regress/output/largeobject.source @@ -9,6 +9,25 @@ CREATE TABLE lotest_stash_values (loid oid, fd integer); -- The mode arg to lo_creat is unused, some vestigal holdover from ancient times -- returns the large object id INSERT INTO lotest_stash_values (loid) SELECT lo_creat(42); +-- Test ALTER LARGE OBJECT +CREATE ROLE regresslo; +DO $$ + BEGIN + EXECUTE 'ALTER LARGE OBJECT ' || (select loid from lotest_stash_values) + || ' OWNER TO regresslo'; + END +$$; +SELECT + rol.rolname +FROM + lotest_stash_values s + JOIN pg_largeobject_metadata lo ON s.loid = lo.oid + JOIN pg_authid rol ON lo.lomowner = rol.oid; + rolname +----------- + regresslo +(1 row) + -- NOTE: large objects require transactions BEGIN; -- lo_open(lobjId oid, mode integer) returns integer @@ -284,3 +303,4 @@ SELECT lo_unlink(loid) FROM lotest_stash_values; \lo_unlink :newloid TRUNCATE lotest_stash_values; +DROP ROLE regresslo; diff --git a/src/test/regress/sql/event_trigger.sql b/src/test/regress/sql/event_trigger.sql index 1480a426e70..699e092cb10 100644 --- a/src/test/regress/sql/event_trigger.sql +++ b/src/test/regress/sql/event_trigger.sql @@ -3,49 +3,50 @@ create event trigger regress_event_trigger on ddl_command_start execute procedure pg_backend_pid(); --- cheesy hack for testing purposes -create function fake_event_trigger() - returns event_trigger - language internal - as 'pg_backend_pid'; +-- OK +create function test_event_trigger() returns event_trigger as $$ +BEGIN + RAISE NOTICE 'test_event_trigger: % %', tg_event, tg_tag; +END +$$ language plpgsql; -- should fail, no elephant_bootstrap entry point create event trigger regress_event_trigger on elephant_bootstrap - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- OK create event trigger regress_event_trigger on ddl_command_start - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, food is not a valid filter variable create event trigger regress_event_trigger2 on ddl_command_start when food in ('sandwhich') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, sandwhich is not a valid command tag create event trigger regress_event_trigger2 on ddl_command_start when tag in ('sandwhich') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, create skunkcabbage is not a valid comand tag create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table', 'create skunkcabbage') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, can't have event triggers on event triggers create event trigger regress_event_trigger2 on ddl_command_start when tag in ('DROP EVENT TRIGGER') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- should fail, can't have same filter variable twice create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table') and tag in ('CREATE FUNCTION') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- OK create event trigger regress_event_trigger2 on ddl_command_start when tag in ('create table', 'CREATE FUNCTION') - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); -- OK comment on event trigger regress_event_trigger is 'test comment'; @@ -57,14 +58,20 @@ comment on event trigger wrong.regress_event_trigger is 'test comment'; create role regression_bob; set role regression_bob; create event trigger regress_event_trigger_noperms on ddl_command_start - execute procedure fake_event_trigger(); + execute procedure test_event_trigger(); reset role; -- all OK -alter event trigger regress_event_trigger disable; alter event trigger regress_event_trigger enable replica; alter event trigger regress_event_trigger enable always; alter event trigger regress_event_trigger enable; +alter event trigger regress_event_trigger disable; + +-- regress_event_trigger2 should fire, but not regress_event_trigger +create table event_trigger_fire1 (a int); + +-- but nothing should fire here +drop table event_trigger_fire1; -- alter owner to non-superuser should fail alter event trigger regress_event_trigger owner to regression_bob; @@ -89,5 +96,5 @@ drop role regression_bob; drop event trigger if exists regress_event_trigger2; drop event trigger if exists regress_event_trigger2; drop event trigger regress_event_trigger3; -drop function fake_event_trigger(); +drop function test_event_trigger(); drop role regression_bob; |