postgresql/src/backend/commands/event_trigger.c

2374 lines
65 KiB
C

/*-------------------------------------------------------------------------
*
* event_trigger.c
* PostgreSQL EVENT TRIGGER support code.
*
* Portions Copyright (c) 1996-2019, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* IDENTIFICATION
* src/backend/commands/event_trigger.c
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "access/htup_details.h"
#include "access/table.h"
#include "access/xact.h"
#include "catalog/catalog.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_event_trigger.h"
#include "catalog/pg_namespace.h"
#include "catalog/pg_opclass.h"
#include "catalog/pg_opfamily.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_trigger.h"
#include "catalog/pg_ts_config.h"
#include "catalog/pg_type.h"
#include "commands/dbcommands.h"
#include "commands/event_trigger.h"
#include "commands/extension.h"
#include "commands/trigger.h"
#include "funcapi.h"
#include "lib/ilist.h"
#include "miscadmin.h"
#include "parser/parse_func.h"
#include "pgstat.h"
#include "tcop/deparse_utility.h"
#include "tcop/utility.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"
#include "utils/rel.h"
#include "utils/syscache.h"
typedef struct EventTriggerQueryState
{
/* memory context for this state's objects */
MemoryContext cxt;
/* sql_drop */
slist_head SQLDropList;
bool in_sql_drop;
/* table_rewrite */
Oid table_rewrite_oid; /* InvalidOid, or set for table_rewrite
* event */
int table_rewrite_reason; /* AT_REWRITE reason */
/* Support for command collection */
bool commandCollectionInhibited;
CollectedCommand *currentCommand;
List *commandList; /* list of CollectedCommand; see
* deparse_utility.h */
struct EventTriggerQueryState *previous;
} EventTriggerQueryState;
static EventTriggerQueryState *currentEventTriggerState = NULL;
typedef struct
{
const char *obtypename;
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;
/* XXX merge this with ObjectTypeMap? */
static const event_trigger_support_data event_trigger_support[] = {
{"ACCESS METHOD", true},
{"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},
{"MATERIALIZED VIEW", true},
{"OPERATOR", true},
{"OPERATOR CLASS", true},
{"OPERATOR FAMILY", true},
{"POLICY", true},
{"PROCEDURE", true},
{"PUBLICATION", true},
{"ROLE", false},
{"ROUTINE", true},
{"RULE", true},
{"SCHEMA", true},
{"SEQUENCE", true},
{"SERVER", true},
{"STATISTICS", true},
{"SUBSCRIPTION", true},
{"TABLE", true},
{"TABLESPACE", false},
{"TRANSFORM", true},
{"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}
};
/* Support for dropped objects */
typedef struct SQLDropObject
{
ObjectAddress address;
const char *schemaname;
const char *objname;
const char *objidentity;
const char *objecttype;
List *addrnames;
List *addrargs;
bool original;
bool normal;
bool istemp;
slist_node next;
} SQLDropObject;
static void AlterEventTriggerOwner_internal(Relation rel,
HeapTuple tup,
Oid newOwnerId);
static event_trigger_command_tag_check_result check_ddl_tag(const char *tag);
static event_trigger_command_tag_check_result check_table_rewrite_ddl_tag(const char *tag);
static void error_duplicate_filter_variable(const char *defname);
static Datum filter_list_to_array(List *filterlist);
static Oid insert_event_trigger_tuple(const char *trigname, const char *eventname,
Oid evtOwner, Oid funcoid, List *tags);
static void validate_ddl_tags(const char *filtervar, List *taglist);
static void validate_table_rewrite_tags(const char *filtervar, List *taglist);
static void EventTriggerInvoke(List *fn_oid_list, EventTriggerData *trigdata);
static const char *stringify_grant_objtype(ObjectType objtype);
static const char *stringify_adefprivs_objtype(ObjectType objtype);
/*
* Create an event trigger.
*/
Oid
CreateEventTrigger(CreateEventTrigStmt *stmt)
{
HeapTuple tuple;
Oid funcoid;
Oid funcrettype;
Oid fargtypes[1]; /* dummy */
Oid evtowner = GetUserId();
ListCell *lc;
List *tags = NULL;
/*
* It would be nice to allow database owners or even regular users to do
* this, but there are obvious privilege escalation risks which would have
* to somehow be plugged first.
*/
if (!superuser())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to create event trigger \"%s\"",
stmt->trigname),
errhint("Must be superuser to create an event trigger.")));
/* Validate event name. */
if (strcmp(stmt->eventname, "ddl_command_start") != 0 &&
strcmp(stmt->eventname, "ddl_command_end") != 0 &&
strcmp(stmt->eventname, "sql_drop") != 0 &&
strcmp(stmt->eventname, "table_rewrite") != 0)
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized event name \"%s\"",
stmt->eventname)));
/* Validate filter conditions. */
foreach(lc, stmt->whenclause)
{
DefElem *def = (DefElem *) lfirst(lc);
if (strcmp(def->defname, "tag") == 0)
{
if (tags != NULL)
error_duplicate_filter_variable(def->defname);
tags = (List *) def->arg;
}
else
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("unrecognized filter variable \"%s\"", def->defname)));
}
/* Validate tag list, if any. */
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);
else if (strcmp(stmt->eventname, "table_rewrite") == 0
&& tags != NULL)
validate_table_rewrite_tags("tag", tags);
/*
* Give user a nice error message if an event trigger of the same name
* already exists.
*/
tuple = SearchSysCache1(EVENTTRIGGERNAME, CStringGetDatum(stmt->trigname));
if (HeapTupleIsValid(tuple))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("event trigger \"%s\" already exists",
stmt->trigname)));
/* Find and validate the trigger function. */
funcoid = LookupFuncName(stmt->funcname, 0, fargtypes, false);
funcrettype = get_func_rettype(funcoid);
if (funcrettype != EVTTRIGGEROID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
errmsg("function %s must return type %s",
NameListToString(stmt->funcname), "event_trigger")));
/* Insert catalog entries. */
return insert_event_trigger_tuple(stmt->trigname, stmt->eventname,
evtowner, funcoid, tags);
}
/*
* Validate DDL command tags.
*/
static void
validate_ddl_tags(const char *filtervar, List *taglist)
{
ListCell *lc;
foreach(lc, taglist)
{
const char *tag = strVal(lfirst(lc));
event_trigger_command_tag_check_result result;
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 */
errmsg("event triggers are not supported for %s",
tag)));
}
}
static event_trigger_command_tag_check_result
check_ddl_tag(const char *tag)
{
const char *obtypename;
const 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, "REFRESH MATERIALIZED VIEW") == 0 ||
pg_strcasecmp(tag, "ALTER DEFAULT PRIVILEGES") == 0 ||
pg_strcasecmp(tag, "ALTER LARGE OBJECT") == 0 ||
pg_strcasecmp(tag, "COMMENT") == 0 ||
pg_strcasecmp(tag, "GRANT") == 0 ||
pg_strcasecmp(tag, "REVOKE") == 0 ||
pg_strcasecmp(tag, "DROP OWNED") == 0 ||
pg_strcasecmp(tag, "IMPORT FOREIGN SCHEMA") == 0 ||
pg_strcasecmp(tag, "SECURITY LABEL") == 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;
}
/*
* Validate DDL command tags for event table_rewrite.
*/
static void
validate_table_rewrite_tags(const char *filtervar, List *taglist)
{
ListCell *lc;
foreach(lc, taglist)
{
const char *tag = strVal(lfirst(lc));
event_trigger_command_tag_check_result result;
result = check_table_rewrite_ddl_tag(tag);
if (result == EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
/* translator: %s represents an SQL statement name */
errmsg("event triggers are not supported for %s",
tag)));
}
}
static event_trigger_command_tag_check_result
check_table_rewrite_ddl_tag(const char *tag)
{
if (pg_strcasecmp(tag, "ALTER TABLE") == 0 ||
pg_strcasecmp(tag, "ALTER TYPE") == 0)
return EVENT_TRIGGER_COMMAND_TAG_OK;
return EVENT_TRIGGER_COMMAND_TAG_NOT_SUPPORTED;
}
/*
* Complain about a duplicate filter variable.
*/
static void
error_duplicate_filter_variable(const char *defname)
{
ereport(ERROR,
(errcode(ERRCODE_SYNTAX_ERROR),
errmsg("filter variable \"%s\" specified more than once",
defname)));
}
/*
* Insert the new pg_event_trigger row and record dependencies.
*/
static Oid
insert_event_trigger_tuple(const char *trigname, const char *eventname, Oid evtOwner,
Oid funcoid, List *taglist)
{
Relation tgrel;
Oid trigoid;
HeapTuple tuple;
Datum values[Natts_pg_trigger];
bool nulls[Natts_pg_trigger];
NameData evtnamedata,
evteventdata;
ObjectAddress myself,
referenced;
/* Open pg_event_trigger. */
tgrel = table_open(EventTriggerRelationId, RowExclusiveLock);
/* Build the new pg_trigger tuple. */
trigoid = GetNewOidWithIndex(tgrel, EventTriggerOidIndexId,
Anum_pg_event_trigger_oid);
values[Anum_pg_event_trigger_oid - 1] = ObjectIdGetDatum(trigoid);
memset(nulls, false, sizeof(nulls));
namestrcpy(&evtnamedata, trigname);
values[Anum_pg_event_trigger_evtname - 1] = NameGetDatum(&evtnamedata);
namestrcpy(&evteventdata, eventname);
values[Anum_pg_event_trigger_evtevent - 1] = NameGetDatum(&evteventdata);
values[Anum_pg_event_trigger_evtowner - 1] = ObjectIdGetDatum(evtOwner);
values[Anum_pg_event_trigger_evtfoid - 1] = ObjectIdGetDatum(funcoid);
values[Anum_pg_event_trigger_evtenabled - 1] =
CharGetDatum(TRIGGER_FIRES_ON_ORIGIN);
if (taglist == NIL)
nulls[Anum_pg_event_trigger_evttags - 1] = true;
else
values[Anum_pg_event_trigger_evttags - 1] =
filter_list_to_array(taglist);
/* Insert heap tuple. */
tuple = heap_form_tuple(tgrel->rd_att, values, nulls);
CatalogTupleInsert(tgrel, tuple);
heap_freetuple(tuple);
/* Depend on owner. */
recordDependencyOnOwner(EventTriggerRelationId, trigoid, evtOwner);
/* Depend on event trigger function. */
myself.classId = EventTriggerRelationId;
myself.objectId = trigoid;
myself.objectSubId = 0;
referenced.classId = ProcedureRelationId;
referenced.objectId = funcoid;
referenced.objectSubId = 0;
recordDependencyOn(&myself, &referenced, DEPENDENCY_NORMAL);
/* Depend on extension, if any. */
recordDependencyOnCurrentExtension(&myself, false);
/* Post creation hook for new event trigger */
InvokeObjectPostCreateHook(EventTriggerRelationId, trigoid, 0);
/* Close pg_event_trigger. */
table_close(tgrel, RowExclusiveLock);
return trigoid;
}
/*
* In the parser, a clause like WHEN tag IN ('cmd1', 'cmd2') is represented
* by a DefElem whose value is a List of String nodes; in the catalog, we
* store the list of strings as a text array. This function transforms the
* former representation into the latter one.
*
* For cleanliness, we store command tags in the catalog as text. It's
* possible (although not currently anticipated) that we might have
* a case-sensitive filter variable in the future, in which case this would
* need some further adjustment.
*/
static Datum
filter_list_to_array(List *filterlist)
{
ListCell *lc;
Datum *data;
int i = 0,
l = list_length(filterlist);
data = (Datum *) palloc(l * sizeof(Datum));
foreach(lc, filterlist)
{
const char *value = strVal(lfirst(lc));
char *result,
*p;
result = pstrdup(value);
for (p = result; *p; p++)
*p = pg_ascii_toupper((unsigned char) *p);
data[i++] = PointerGetDatum(cstring_to_text(result));
pfree(result);
}
return PointerGetDatum(construct_array(data, l, TEXTOID, -1, false, 'i'));
}
/*
* Guts of event trigger deletion.
*/
void
RemoveEventTriggerById(Oid trigOid)
{
Relation tgrel;
HeapTuple tup;
tgrel = table_open(EventTriggerRelationId, RowExclusiveLock);
tup = SearchSysCache1(EVENTTRIGGEROID, ObjectIdGetDatum(trigOid));
if (!HeapTupleIsValid(tup))
elog(ERROR, "cache lookup failed for event trigger %u", trigOid);
CatalogTupleDelete(tgrel, &tup->t_self);
ReleaseSysCache(tup);
table_close(tgrel, RowExclusiveLock);
}
/*
* ALTER EVENT TRIGGER foo ENABLE|DISABLE|ENABLE ALWAYS|REPLICA
*/
Oid
AlterEventTrigger(AlterEventTrigStmt *stmt)
{
Relation tgrel;
HeapTuple tup;
Oid trigoid;
Form_pg_event_trigger evtForm;
char tgenabled = stmt->tgenabled;
tgrel = table_open(EventTriggerRelationId, RowExclusiveLock);
tup = SearchSysCacheCopy1(EVENTTRIGGERNAME,
CStringGetDatum(stmt->trigname));
if (!HeapTupleIsValid(tup))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("event trigger \"%s\" does not exist",
stmt->trigname)));
evtForm = (Form_pg_event_trigger) GETSTRUCT(tup);
trigoid = evtForm->oid;
if (!pg_event_trigger_ownercheck(trigoid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EVENT_TRIGGER,
stmt->trigname);
/* tuple is a copy, so we can modify it below */
evtForm->evtenabled = tgenabled;
CatalogTupleUpdate(tgrel, &tup->t_self, tup);
InvokeObjectPostAlterHook(EventTriggerRelationId,
trigoid, 0);
/* clean up */
heap_freetuple(tup);
table_close(tgrel, RowExclusiveLock);
return trigoid;
}
/*
* Change event trigger's owner -- by name
*/
ObjectAddress
AlterEventTriggerOwner(const char *name, Oid newOwnerId)
{
Oid evtOid;
HeapTuple tup;
Form_pg_event_trigger evtForm;
Relation rel;
ObjectAddress address;
rel = table_open(EventTriggerRelationId, RowExclusiveLock);
tup = SearchSysCacheCopy1(EVENTTRIGGERNAME, CStringGetDatum(name));
if (!HeapTupleIsValid(tup))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("event trigger \"%s\" does not exist", name)));
evtForm = (Form_pg_event_trigger) GETSTRUCT(tup);
evtOid = evtForm->oid;
AlterEventTriggerOwner_internal(rel, tup, newOwnerId);
ObjectAddressSet(address, EventTriggerRelationId, evtOid);
heap_freetuple(tup);
table_close(rel, RowExclusiveLock);
return address;
}
/*
* Change event trigger owner, by OID
*/
void
AlterEventTriggerOwner_oid(Oid trigOid, Oid newOwnerId)
{
HeapTuple tup;
Relation rel;
rel = table_open(EventTriggerRelationId, RowExclusiveLock);
tup = SearchSysCacheCopy1(EVENTTRIGGEROID, ObjectIdGetDatum(trigOid));
if (!HeapTupleIsValid(tup))
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("event trigger with OID %u does not exist", trigOid)));
AlterEventTriggerOwner_internal(rel, tup, newOwnerId);
heap_freetuple(tup);
table_close(rel, RowExclusiveLock);
}
/*
* Internal workhorse for changing an event trigger's owner
*/
static void
AlterEventTriggerOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId)
{
Form_pg_event_trigger form;
form = (Form_pg_event_trigger) GETSTRUCT(tup);
if (form->evtowner == newOwnerId)
return;
if (!pg_event_trigger_ownercheck(form->oid, GetUserId()))
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EVENT_TRIGGER,
NameStr(form->evtname));
/* New owner must be a superuser */
if (!superuser_arg(newOwnerId))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to change owner of event trigger \"%s\"",
NameStr(form->evtname)),
errhint("The owner of an event trigger must be a superuser.")));
form->evtowner = newOwnerId;
CatalogTupleUpdate(rel, &tup->t_self, tup);
/* Update owner dependency reference */
changeDependencyOnOwner(EventTriggerRelationId,
form->oid,
newOwnerId);
InvokeObjectPostAlterHook(EventTriggerRelationId,
form->oid, 0);
}
/*
* get_event_trigger_oid - Look up an event trigger by name to find its OID.
*
* If missing_ok is false, throw an error if trigger not found. If
* true, just return InvalidOid.
*/
Oid
get_event_trigger_oid(const char *trigname, bool missing_ok)
{
Oid oid;
oid = GetSysCacheOid1(EVENTTRIGGERNAME, Anum_pg_event_trigger_oid,
CStringGetDatum(trigname));
if (!OidIsValid(oid) && !missing_ok)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("event trigger \"%s\" does not exist", trigname)));
return oid;
}
/*
* Return true when we want to fire given Event Trigger and false otherwise,
* filtering on the session replication role and the event trigger registered
* tags matching.
*/
static bool
filter_event_trigger(const char **tag, EventTriggerCacheItem *item)
{
/*
* Filter by session replication role, knowing that we never see disabled
* items down here.
*/
if (SessionReplicationRole == SESSION_REPLICATION_ROLE_REPLICA)
{
if (item->enabled == TRIGGER_FIRES_ON_ORIGIN)
return false;
}
else
{
if (item->enabled == TRIGGER_FIRES_ON_REPLICA)
return false;
}
/* Filter by tags, if any were specified. */
if (item->ntags != 0 && bsearch(tag, item->tag,
item->ntags, sizeof(char *),
pg_qsort_strcmp) == NULL)
return false;
/* if we reach that point, we're not filtering out this item */
return true;
}
/*
* 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.
*/
static List *
EventTriggerCommonSetup(Node *parsetree,
EventTriggerEvent event, const char *eventstr,
EventTriggerData *trigdata)
{
const char *tag;
List *cachelist;
ListCell *lc;
List *runlist = NIL;
/*
* 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
{
const char *dbgtag;
dbgtag = CreateCommandTag(parsetree);
if (event == EVT_DDLCommandStart ||
event == EVT_DDLCommandEnd ||
event == EVT_SQLDrop)
{
if (check_ddl_tag(dbgtag) != EVENT_TRIGGER_COMMAND_TAG_OK)
elog(ERROR, "unexpected command tag \"%s\"", dbgtag);
}
else if (event == EVT_TableRewrite)
{
if (check_table_rewrite_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(event);
if (cachelist == NIL)
return NIL;
/* 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 triggers, 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);
if (filter_event_trigger(&tag, item))
{
/* We must plan to fire this trigger. */
runlist = lappend_oid(runlist, item->fnoid);
}
}
/* 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);
/* Cleanup. */
list_free(runlist);
/*
* Make sure anything the event triggers did will be visible to the main
* command.
*/
CommandCounterIncrement();
}
/*
* Fire ddl_command_end triggers.
*/
void
EventTriggerDDLCommandEnd(Node *parsetree)
{
List *runlist;
EventTriggerData trigdata;
/*
* See EventTriggerDDLCommandStart for a discussion about why event
* triggers are disabled in single user mode.
*/
if (!IsUnderPostmaster)
return;
/*
* Also do nothing if our state isn't set up, which it won't be if there
* weren't any relevant event triggers at the start of the current DDL
* command. This test might therefore seem optional, but it's important
* because EventTriggerCommonSetup might find triggers that didn't exist
* at the time the command started. Although this function itself
* wouldn't crash, the event trigger functions would presumably call
* pg_event_trigger_ddl_commands which would fail. Better to do nothing
* until the next command.
*/
if (!currentEventTriggerState)
return;
runlist = EventTriggerCommonSetup(parsetree,
EVT_DDLCommandEnd, "ddl_command_end",
&trigdata);
if (runlist == NIL)
return;
/*
* Make sure anything the main command did will be visible to the event
* triggers.
*/
CommandCounterIncrement();
/* Run the triggers. */
EventTriggerInvoke(runlist, &trigdata);
/* Cleanup. */
list_free(runlist);
}
/*
* Fire sql_drop triggers.
*/
void
EventTriggerSQLDrop(Node *parsetree)
{
List *runlist;
EventTriggerData trigdata;
/*
* See EventTriggerDDLCommandStart for a discussion about why event
* triggers are disabled in single user mode.
*/
if (!IsUnderPostmaster)
return;
/*
* 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(&currentEventTriggerState->SQLDropList))
return;
runlist = EventTriggerCommonSetup(parsetree,
EVT_SQLDrop, "sql_drop",
&trigdata);
/*
* Nothing to do if run list is empty. Note this typically can'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 quit above.
* But it could occur if event triggers were dropped partway through.
*/
if (runlist == NIL)
return;
/*
* Make sure anything the main command did will be visible to the event
* triggers.
*/
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. */
PG_TRY();
{
EventTriggerInvoke(runlist, &trigdata);
}
PG_FINALLY();
{
currentEventTriggerState->in_sql_drop = false;
}
PG_END_TRY();
/* Cleanup. */
list_free(runlist);
}
/*
* Fire table_rewrite triggers.
*/
void
EventTriggerTableRewrite(Node *parsetree, Oid tableOid, int reason)
{
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;
/*
* Also do nothing if our state isn't set up, which it won't be if there
* weren't any relevant event triggers at the start of the current DDL
* command. This test might therefore seem optional, but it's
* *necessary*, because EventTriggerCommonSetup might find triggers that
* didn't exist at the time the command started.
*/
if (!currentEventTriggerState)
return;
runlist = EventTriggerCommonSetup(parsetree,
EVT_TableRewrite,
"table_rewrite",
&trigdata);
if (runlist == NIL)
return;
/*
* Make sure pg_event_trigger_table_rewrite_oid only works when running
* these triggers. Use PG_TRY to ensure table_rewrite_oid 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->table_rewrite_oid = tableOid;
currentEventTriggerState->table_rewrite_reason = reason;
/* Run the triggers. */
PG_TRY();
{
EventTriggerInvoke(runlist, &trigdata);
}
PG_FINALLY();
{
currentEventTriggerState->table_rewrite_oid = InvalidOid;
currentEventTriggerState->table_rewrite_reason = 0;
}
PG_END_TRY();
/* Cleanup. */
list_free(runlist);
/*
* Make sure anything the event triggers did will be visible to the main
* command.
*/
CommandCounterIncrement();
}
/*
* 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;
bool first = true;
/* Guard against stack overflow due to recursive event trigger */
check_stack_depth();
/*
* 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_SIZES);
oldcontext = MemoryContextSwitchTo(context);
/* Call each event trigger. */
foreach(lc, fn_oid_list)
{
LOCAL_FCINFO(fcinfo, 0);
Oid fnoid = lfirst_oid(lc);
FmgrInfo flinfo;
PgStat_FunctionCallUsage fcusage;
elog(DEBUG1, "EventTriggerInvoke %u", fnoid);
/*
* We want each event trigger to be able to see the results of the
* previous event trigger's action. Caller is responsible for any
* command-counter increment that is needed between the event trigger
* and anything else in the transaction.
*/
if (first)
first = false;
else
CommandCounterIncrement();
/* 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);
}
/* 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;
case OBJECT_ACCESS_METHOD:
case OBJECT_AGGREGATE:
case OBJECT_AMOP:
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
case OBJECT_COLUMN:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFACL:
case OBJECT_DEFAULT:
case OBJECT_DOMAIN:
case OBJECT_DOMCONSTRAINT:
case OBJECT_EXTENSION:
case OBJECT_FDW:
case OBJECT_FOREIGN_SERVER:
case OBJECT_FOREIGN_TABLE:
case OBJECT_FUNCTION:
case OBJECT_INDEX:
case OBJECT_LANGUAGE:
case OBJECT_LARGEOBJECT:
case OBJECT_MATVIEW:
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_POLICY:
case OBJECT_PROCEDURE:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROUTINE:
case OBJECT_RULE:
case OBJECT_SCHEMA:
case OBJECT_SEQUENCE:
case OBJECT_SUBSCRIPTION:
case OBJECT_STATISTIC_EXT:
case OBJECT_TABCONSTRAINT:
case OBJECT_TABLE:
case OBJECT_TRANSFORM:
case OBJECT_TRIGGER:
case OBJECT_TSCONFIGURATION:
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
case OBJECT_TYPE:
case OBJECT_USER_MAPPING:
case OBJECT_VIEW:
return true;
/*
* There's intentionally no default: case here; we want the
* compiler to warn if a new ObjectType hasn't been handled above.
*/
}
/* Shouldn't get here, but if we do, say "no support" */
return false;
}
/*
* Do event triggers support this object class?
*/
bool
EventTriggerSupportsObjectClass(ObjectClass objclass)
{
switch (objclass)
{
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
/* no support for global objects */
return false;
case OCLASS_EVENT_TRIGGER:
/* no support for event triggers on event triggers */
return false;
case OCLASS_CLASS:
case OCLASS_PROC:
case OCLASS_TYPE:
case OCLASS_CAST:
case OCLASS_COLLATION:
case OCLASS_CONSTRAINT:
case OCLASS_CONVERSION:
case OCLASS_DEFAULT:
case OCLASS_LANGUAGE:
case OCLASS_LARGEOBJECT:
case OCLASS_OPERATOR:
case OCLASS_OPCLASS:
case OCLASS_OPFAMILY:
case OCLASS_AM:
case OCLASS_AMOP:
case OCLASS_AMPROC:
case OCLASS_REWRITE:
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_STATISTIC_EXT:
case OCLASS_TSPARSER:
case OCLASS_TSDICT:
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_FDW:
case OCLASS_FOREIGN_SERVER:
case OCLASS_USER_MAPPING:
case OCLASS_DEFACL:
case OCLASS_EXTENSION:
case OCLASS_POLICY:
case OCLASS_PUBLICATION:
case OCLASS_PUBLICATION_REL:
case OCLASS_SUBSCRIPTION:
case OCLASS_TRANSFORM:
return true;
/*
* There's intentionally no default: case here; we want the
* compiler to warn if a new OCLASS hasn't been handled above.
*/
}
/* Shouldn't get here, but if we do, say "no support" */
return false;
}
/*
* 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, table_rewrite, ddl_command_end 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_SIZES);
state = MemoryContextAlloc(cxt, sizeof(EventTriggerQueryState));
state->cxt = cxt;
slist_init(&(state->SQLDropList));
state->in_sql_drop = false;
state->table_rewrite_oid = InvalidOid;
state->commandCollectionInhibited = currentEventTriggerState ?
currentEventTriggerState->commandCollectionInhibited : false;
state->currentCommand = NULL;
state->commandList = NIL;
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, table_rewrite, ddl_command_end event trigger
* exists
*/
return list_length(EventCacheLookup(EVT_SQLDrop)) > 0 ||
list_length(EventCacheLookup(EVT_TableRewrite)) > 0 ||
list_length(EventCacheLookup(EVT_DDLCommandEnd)) > 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(const ObjectAddress *object, bool original, bool normal)
{
SQLDropObject *obj;
MemoryContext oldcxt;
if (!currentEventTriggerState)
return;
Assert(EventTriggerSupportsObjectClass(getObjectClass(object)));
/* don't report temp schemas except my own */
if (object->classId == NamespaceRelationId &&
(isAnyTempNamespace(object->objectId) &&
!isTempNamespace(object->objectId)))
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
obj = palloc0(sizeof(SQLDropObject));
obj->address = *object;
obj->original = original;
obj->normal = normal;
/*
* 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 = table_open(obj->address.classId, AccessShareLock);
tuple = get_catalog_object_by_oid(catalog,
get_object_attnum_oid(object->classId),
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);
/* temp objects are only reported if they are my own */
if (isTempNamespace(namespaceId))
{
obj->schemaname = "pg_temp";
obj->istemp = true;
}
else if (isAnyTempNamespace(namespaceId))
{
pfree(obj);
table_close(catalog, AccessShareLock);
MemoryContextSwitchTo(oldcxt);
return;
}
else
{
obj->schemaname = get_namespace_name(namespaceId);
obj->istemp = false;
}
}
}
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)));
}
}
}
table_close(catalog, AccessShareLock);
}
else
{
if (object->classId == NamespaceRelationId &&
isTempNamespace(object->objectId))
obj->istemp = true;
}
/* object identity, objname and objargs */
obj->objidentity =
getObjectIdentityParts(&obj->address, &obj->addrnames, &obj->addrargs);
/* object type */
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_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
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[12];
bool nulls[12];
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);
/* original */
values[i++] = BoolGetDatum(obj->original);
/* normal */
values[i++] = BoolGetDatum(obj->normal);
/* is_temporary */
values[i++] = BoolGetDatum(obj->istemp);
/* 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;
/* address_names and address_args */
if (obj->addrnames)
{
values[i++] = PointerGetDatum(strlist_to_textarray(obj->addrnames));
if (obj->addrargs)
values[i++] = PointerGetDatum(strlist_to_textarray(obj->addrargs));
else
values[i++] = PointerGetDatum(construct_empty_array(TEXTOID));
}
else
{
nulls[i++] = true;
nulls[i++] = true;
}
tuplestore_putvalues(tupstore, tupdesc, values, nulls);
}
/* clean up and return the tuplestore */
tuplestore_donestoring(tupstore);
return (Datum) 0;
}
/*
* pg_event_trigger_table_rewrite_oid
*
* Make the Oid of the table going to be rewritten available to the user
* function run by the Event Trigger.
*/
Datum
pg_event_trigger_table_rewrite_oid(PG_FUNCTION_ARGS)
{
/*
* Protect this function from being called out of context
*/
if (!currentEventTriggerState ||
currentEventTriggerState->table_rewrite_oid == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
errmsg("%s can only be called in a table_rewrite event trigger function",
"pg_event_trigger_table_rewrite_oid()")));
PG_RETURN_OID(currentEventTriggerState->table_rewrite_oid);
}
/*
* pg_event_trigger_table_rewrite_reason
*
* Make the rewrite reason available to the user.
*/
Datum
pg_event_trigger_table_rewrite_reason(PG_FUNCTION_ARGS)
{
/*
* Protect this function from being called out of context
*/
if (!currentEventTriggerState ||
currentEventTriggerState->table_rewrite_reason == 0)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
errmsg("%s can only be called in a table_rewrite event trigger function",
"pg_event_trigger_table_rewrite_reason()")));
PG_RETURN_INT32(currentEventTriggerState->table_rewrite_reason);
}
/*-------------------------------------------------------------------------
* Support for DDL command deparsing
*
* The routines below enable an event trigger function to obtain a list of
* DDL commands as they are executed. There are three main pieces to this
* feature:
*
* 1) Within ProcessUtilitySlow, or some sub-routine thereof, each DDL command
* adds a struct CollectedCommand representation of itself to the command list,
* using the routines below.
*
* 2) Some time after that, ddl_command_end fires and the command list is made
* available to the event trigger function via pg_event_trigger_ddl_commands();
* the complete command details are exposed as a column of type pg_ddl_command.
*
* 3) An extension can install a function capable of taking a value of type
* pg_ddl_command and transform it into some external, user-visible and/or
* -modifiable representation.
*-------------------------------------------------------------------------
*/
/*
* Inhibit DDL command collection.
*/
void
EventTriggerInhibitCommandCollection(void)
{
if (!currentEventTriggerState)
return;
currentEventTriggerState->commandCollectionInhibited = true;
}
/*
* Re-establish DDL command collection.
*/
void
EventTriggerUndoInhibitCommandCollection(void)
{
if (!currentEventTriggerState)
return;
currentEventTriggerState->commandCollectionInhibited = false;
}
/*
* EventTriggerCollectSimpleCommand
* Save data about a simple DDL command that was just executed
*
* address identifies the object being operated on. secondaryObject is an
* object address that was related in some way to the executed command; its
* meaning is command-specific.
*
* For instance, for an ALTER obj SET SCHEMA command, objtype is the type of
* object being moved, objectId is its OID, and secondaryOid is the OID of the
* old schema. (The destination schema OID can be obtained by catalog lookup
* of the object.)
*/
void
EventTriggerCollectSimpleCommand(ObjectAddress address,
ObjectAddress secondaryObject,
Node *parsetree)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc(sizeof(CollectedCommand));
command->type = SCT_Simple;
command->in_extension = creating_extension;
command->d.simple.address = address;
command->d.simple.secondaryObject = secondaryObject;
command->parsetree = copyObject(parsetree);
currentEventTriggerState->commandList = lappend(currentEventTriggerState->commandList,
command);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerAlterTableStart
* Prepare to receive data on an ALTER TABLE command about to be executed
*
* Note we don't collect the command immediately; instead we keep it in
* currentCommand, and only when we're done processing the subcommands we will
* add it to the command list.
*/
void
EventTriggerAlterTableStart(Node *parsetree)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc(sizeof(CollectedCommand));
command->type = SCT_AlterTable;
command->in_extension = creating_extension;
command->d.alterTable.classId = RelationRelationId;
command->d.alterTable.objectId = InvalidOid;
command->d.alterTable.subcmds = NIL;
command->parsetree = copyObject(parsetree);
command->parent = currentEventTriggerState->currentCommand;
currentEventTriggerState->currentCommand = command;
MemoryContextSwitchTo(oldcxt);
}
/*
* Remember the OID of the object being affected by an ALTER TABLE.
*
* This is needed because in some cases we don't know the OID until later.
*/
void
EventTriggerAlterTableRelid(Oid objectId)
{
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
currentEventTriggerState->currentCommand->d.alterTable.objectId = objectId;
}
/*
* EventTriggerCollectAlterTableSubcmd
* Save data about a single part of an ALTER TABLE.
*
* Several different commands go through this path, but apart from ALTER TABLE
* itself, they are all concerned with AlterTableCmd nodes that are generated
* internally, so that's all that this code needs to handle at the moment.
*/
void
EventTriggerCollectAlterTableSubcmd(Node *subcmd, ObjectAddress address)
{
MemoryContext oldcxt;
CollectedATSubcmd *newsub;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
Assert(IsA(subcmd, AlterTableCmd));
Assert(currentEventTriggerState->currentCommand != NULL);
Assert(OidIsValid(currentEventTriggerState->currentCommand->d.alterTable.objectId));
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
newsub = palloc(sizeof(CollectedATSubcmd));
newsub->address = address;
newsub->parsetree = copyObject(subcmd);
currentEventTriggerState->currentCommand->d.alterTable.subcmds =
lappend(currentEventTriggerState->currentCommand->d.alterTable.subcmds, newsub);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerAlterTableEnd
* Finish up saving an ALTER TABLE command, and add it to command list.
*
* FIXME this API isn't considering the possibility that an xact/subxact is
* aborted partway through. Probably it's best to add an
* AtEOSubXact_EventTriggers() to fix this.
*/
void
EventTriggerAlterTableEnd(void)
{
CollectedCommand *parent;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
parent = currentEventTriggerState->currentCommand->parent;
/* If no subcommands, don't collect */
if (list_length(currentEventTriggerState->currentCommand->d.alterTable.subcmds) != 0)
{
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList,
currentEventTriggerState->currentCommand);
}
else
pfree(currentEventTriggerState->currentCommand);
currentEventTriggerState->currentCommand = parent;
}
/*
* EventTriggerCollectGrant
* Save data about a GRANT/REVOKE command being executed
*
* This function creates a copy of the InternalGrant, as the original might
* not have the right lifetime.
*/
void
EventTriggerCollectGrant(InternalGrant *istmt)
{
MemoryContext oldcxt;
CollectedCommand *command;
InternalGrant *icopy;
ListCell *cell;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
/*
* This is tedious, but necessary.
*/
icopy = palloc(sizeof(InternalGrant));
memcpy(icopy, istmt, sizeof(InternalGrant));
icopy->objects = list_copy(istmt->objects);
icopy->grantees = list_copy(istmt->grantees);
icopy->col_privs = NIL;
foreach(cell, istmt->col_privs)
icopy->col_privs = lappend(icopy->col_privs, copyObject(lfirst(cell)));
/* Now collect it, using the copied InternalGrant */
command = palloc(sizeof(CollectedCommand));
command->type = SCT_Grant;
command->in_extension = creating_extension;
command->d.grant.istmt = icopy;
command->parsetree = NULL;
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerCollectAlterOpFam
* Save data about an ALTER OPERATOR FAMILY ADD/DROP command being
* executed
*/
void
EventTriggerCollectAlterOpFam(AlterOpFamilyStmt *stmt, Oid opfamoid,
List *operators, List *procedures)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc(sizeof(CollectedCommand));
command->type = SCT_AlterOpFamily;
command->in_extension = creating_extension;
ObjectAddressSet(command->d.opfam.address,
OperatorFamilyRelationId, opfamoid);
command->d.opfam.operators = operators;
command->d.opfam.procedures = procedures;
command->parsetree = (Node *) copyObject(stmt);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerCollectCreateOpClass
* Save data about a CREATE OPERATOR CLASS command being executed
*/
void
EventTriggerCollectCreateOpClass(CreateOpClassStmt *stmt, Oid opcoid,
List *operators, List *procedures)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc0(sizeof(CollectedCommand));
command->type = SCT_CreateOpClass;
command->in_extension = creating_extension;
ObjectAddressSet(command->d.createopc.address,
OperatorClassRelationId, opcoid);
command->d.createopc.operators = operators;
command->d.createopc.procedures = procedures;
command->parsetree = (Node *) copyObject(stmt);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerCollectAlterTSConfig
* Save data about an ALTER TEXT SEARCH CONFIGURATION command being
* executed
*/
void
EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId,
Oid *dictIds, int ndicts)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc0(sizeof(CollectedCommand));
command->type = SCT_AlterTSConfig;
command->in_extension = creating_extension;
ObjectAddressSet(command->d.atscfg.address,
TSConfigRelationId, cfgId);
command->d.atscfg.dictIds = palloc(sizeof(Oid) * ndicts);
memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts);
command->d.atscfg.ndicts = ndicts;
command->parsetree = (Node *) copyObject(stmt);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);
}
/*
* EventTriggerCollectAlterDefPrivs
* Save data about an ALTER DEFAULT PRIVILEGES command being
* executed
*/
void
EventTriggerCollectAlterDefPrivs(AlterDefaultPrivilegesStmt *stmt)
{
MemoryContext oldcxt;
CollectedCommand *command;
/* ignore if event trigger context not set, or collection disabled */
if (!currentEventTriggerState ||
currentEventTriggerState->commandCollectionInhibited)
return;
oldcxt = MemoryContextSwitchTo(currentEventTriggerState->cxt);
command = palloc0(sizeof(CollectedCommand));
command->type = SCT_AlterDefaultPrivileges;
command->d.defprivs.objtype = stmt->action->objtype;
command->in_extension = creating_extension;
command->parsetree = (Node *) copyObject(stmt);
currentEventTriggerState->commandList =
lappend(currentEventTriggerState->commandList, command);
MemoryContextSwitchTo(oldcxt);
}
/*
* In a ddl_command_end event trigger, this function reports the DDL commands
* being run.
*/
Datum
pg_event_trigger_ddl_commands(PG_FUNCTION_ARGS)
{
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
TupleDesc tupdesc;
Tuplestorestate *tupstore;
MemoryContext per_query_ctx;
MemoryContext oldcontext;
ListCell *lc;
/*
* Protect this function from being called out of context
*/
if (!currentEventTriggerState)
ereport(ERROR,
(errcode(ERRCODE_E_R_I_E_EVENT_TRIGGER_PROTOCOL_VIOLATED),
errmsg("%s can only be called in an event trigger function",
"pg_event_trigger_ddl_commands()")));
/* 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);
foreach(lc, currentEventTriggerState->commandList)
{
CollectedCommand *cmd = lfirst(lc);
Datum values[9];
bool nulls[9];
ObjectAddress addr;
int i = 0;
/*
* For IF NOT EXISTS commands that attempt to create an existing
* object, the returned OID is Invalid. Don't return anything.
*
* One might think that a viable alternative would be to look up the
* Oid of the existing object and run the deparse with that. But
* since the parse tree might be different from the one that created
* the object in the first place, we might not end up in a consistent
* state anyway.
*/
if (cmd->type == SCT_Simple &&
!OidIsValid(cmd->d.simple.address.objectId))
continue;
MemSet(nulls, 0, sizeof(nulls));
switch (cmd->type)
{
case SCT_Simple:
case SCT_AlterTable:
case SCT_AlterOpFamily:
case SCT_CreateOpClass:
case SCT_AlterTSConfig:
{
char *identity;
char *type;
char *schema = NULL;
if (cmd->type == SCT_Simple)
addr = cmd->d.simple.address;
else if (cmd->type == SCT_AlterTable)
ObjectAddressSet(addr,
cmd->d.alterTable.classId,
cmd->d.alterTable.objectId);
else if (cmd->type == SCT_AlterOpFamily)
addr = cmd->d.opfam.address;
else if (cmd->type == SCT_CreateOpClass)
addr = cmd->d.createopc.address;
else if (cmd->type == SCT_AlterTSConfig)
addr = cmd->d.atscfg.address;
type = getObjectTypeDescription(&addr);
identity = getObjectIdentity(&addr);
/*
* Obtain schema name, if any ("pg_temp" if a temp
* object). If the object class is not in the supported
* list here, we assume it's a schema-less object type,
* and thus "schema" remains set to NULL.
*/
if (is_objectclass_supported(addr.classId))
{
AttrNumber nspAttnum;
nspAttnum = get_object_attnum_namespace(addr.classId);
if (nspAttnum != InvalidAttrNumber)
{
Relation catalog;
HeapTuple objtup;
Oid schema_oid;
bool isnull;
catalog = table_open(addr.classId, AccessShareLock);
objtup = get_catalog_object_by_oid(catalog,
get_object_attnum_oid(addr.classId),
addr.objectId);
if (!HeapTupleIsValid(objtup))
elog(ERROR, "cache lookup failed for object %u/%u",
addr.classId, addr.objectId);
schema_oid =
heap_getattr(objtup, nspAttnum,
RelationGetDescr(catalog), &isnull);
if (isnull)
elog(ERROR,
"invalid null namespace in object %u/%u/%d",
addr.classId, addr.objectId, addr.objectSubId);
/* XXX not quite get_namespace_name_or_temp */
if (isAnyTempNamespace(schema_oid))
schema = pstrdup("pg_temp");
else
schema = get_namespace_name(schema_oid);
table_close(catalog, AccessShareLock);
}
}
/* classid */
values[i++] = ObjectIdGetDatum(addr.classId);
/* objid */
values[i++] = ObjectIdGetDatum(addr.objectId);
/* objsubid */
values[i++] = Int32GetDatum(addr.objectSubId);
/* command tag */
values[i++] = CStringGetTextDatum(CreateCommandTag(cmd->parsetree));
/* object_type */
values[i++] = CStringGetTextDatum(type);
/* schema */
if (schema == NULL)
nulls[i++] = true;
else
values[i++] = CStringGetTextDatum(schema);
/* identity */
values[i++] = CStringGetTextDatum(identity);
/* in_extension */
values[i++] = BoolGetDatum(cmd->in_extension);
/* command */
values[i++] = PointerGetDatum(cmd);
}
break;
case SCT_AlterDefaultPrivileges:
/* classid */
nulls[i++] = true;
/* objid */
nulls[i++] = true;
/* objsubid */
nulls[i++] = true;
/* command tag */
values[i++] = CStringGetTextDatum(CreateCommandTag(cmd->parsetree));
/* object_type */
values[i++] = CStringGetTextDatum(stringify_adefprivs_objtype(
cmd->d.defprivs.objtype));
/* schema */
nulls[i++] = true;
/* identity */
nulls[i++] = true;
/* in_extension */
values[i++] = BoolGetDatum(cmd->in_extension);
/* command */
values[i++] = PointerGetDatum(cmd);
break;
case SCT_Grant:
/* classid */
nulls[i++] = true;
/* objid */
nulls[i++] = true;
/* objsubid */
nulls[i++] = true;
/* command tag */
values[i++] = CStringGetTextDatum(cmd->d.grant.istmt->is_grant ?
"GRANT" : "REVOKE");
/* object_type */
values[i++] = CStringGetTextDatum(stringify_grant_objtype(
cmd->d.grant.istmt->objtype));
/* schema */
nulls[i++] = true;
/* identity */
nulls[i++] = true;
/* in_extension */
values[i++] = BoolGetDatum(cmd->in_extension);
/* command */
values[i++] = PointerGetDatum(cmd);
break;
}
tuplestore_putvalues(tupstore, tupdesc, values, nulls);
}
/* clean up and return the tuplestore */
tuplestore_donestoring(tupstore);
PG_RETURN_VOID();
}
/*
* Return the ObjectType as a string, as it would appear in GRANT and
* REVOKE commands.
*/
static const char *
stringify_grant_objtype(ObjectType objtype)
{
switch (objtype)
{
case OBJECT_COLUMN:
return "COLUMN";
case OBJECT_TABLE:
return "TABLE";
case OBJECT_SEQUENCE:
return "SEQUENCE";
case OBJECT_DATABASE:
return "DATABASE";
case OBJECT_DOMAIN:
return "DOMAIN";
case OBJECT_FDW:
return "FOREIGN DATA WRAPPER";
case OBJECT_FOREIGN_SERVER:
return "FOREIGN SERVER";
case OBJECT_FUNCTION:
return "FUNCTION";
case OBJECT_LANGUAGE:
return "LANGUAGE";
case OBJECT_LARGEOBJECT:
return "LARGE OBJECT";
case OBJECT_SCHEMA:
return "SCHEMA";
case OBJECT_PROCEDURE:
return "PROCEDURE";
case OBJECT_ROUTINE:
return "ROUTINE";
case OBJECT_TABLESPACE:
return "TABLESPACE";
case OBJECT_TYPE:
return "TYPE";
/* these currently aren't used */
case OBJECT_ACCESS_METHOD:
case OBJECT_AGGREGATE:
case OBJECT_AMOP:
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_EVENT_TRIGGER:
case OBJECT_EXTENSION:
case OBJECT_FOREIGN_TABLE:
case OBJECT_INDEX:
case OBJECT_MATVIEW:
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_STATISTIC_EXT:
case OBJECT_SUBSCRIPTION:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
case OBJECT_TRIGGER:
case OBJECT_TSCONFIGURATION:
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
case OBJECT_USER_MAPPING:
case OBJECT_VIEW:
elog(ERROR, "unsupported object type: %d", (int) objtype);
}
return "???"; /* keep compiler quiet */
}
/*
* Return the ObjectType as a string; as above, but use the spelling
* in ALTER DEFAULT PRIVILEGES commands instead. Generally this is just
* the plural.
*/
static const char *
stringify_adefprivs_objtype(ObjectType objtype)
{
switch (objtype)
{
case OBJECT_COLUMN:
return "COLUMNS";
case OBJECT_TABLE:
return "TABLES";
case OBJECT_SEQUENCE:
return "SEQUENCES";
case OBJECT_DATABASE:
return "DATABASES";
case OBJECT_DOMAIN:
return "DOMAINS";
case OBJECT_FDW:
return "FOREIGN DATA WRAPPERS";
case OBJECT_FOREIGN_SERVER:
return "FOREIGN SERVERS";
case OBJECT_FUNCTION:
return "FUNCTIONS";
case OBJECT_LANGUAGE:
return "LANGUAGES";
case OBJECT_LARGEOBJECT:
return "LARGE OBJECTS";
case OBJECT_SCHEMA:
return "SCHEMAS";
case OBJECT_PROCEDURE:
return "PROCEDURES";
case OBJECT_ROUTINE:
return "ROUTINES";
case OBJECT_TABLESPACE:
return "TABLESPACES";
case OBJECT_TYPE:
return "TYPES";
/* these currently aren't used */
case OBJECT_ACCESS_METHOD:
case OBJECT_AGGREGATE:
case OBJECT_AMOP:
case OBJECT_AMPROC:
case OBJECT_ATTRIBUTE:
case OBJECT_CAST:
case OBJECT_COLLATION:
case OBJECT_CONVERSION:
case OBJECT_DEFAULT:
case OBJECT_DEFACL:
case OBJECT_DOMCONSTRAINT:
case OBJECT_EVENT_TRIGGER:
case OBJECT_EXTENSION:
case OBJECT_FOREIGN_TABLE:
case OBJECT_INDEX:
case OBJECT_MATVIEW:
case OBJECT_OPCLASS:
case OBJECT_OPERATOR:
case OBJECT_OPFAMILY:
case OBJECT_POLICY:
case OBJECT_PUBLICATION:
case OBJECT_PUBLICATION_REL:
case OBJECT_ROLE:
case OBJECT_RULE:
case OBJECT_STATISTIC_EXT:
case OBJECT_SUBSCRIPTION:
case OBJECT_TABCONSTRAINT:
case OBJECT_TRANSFORM:
case OBJECT_TRIGGER:
case OBJECT_TSCONFIGURATION:
case OBJECT_TSDICTIONARY:
case OBJECT_TSPARSER:
case OBJECT_TSTEMPLATE:
case OBJECT_USER_MAPPING:
case OBJECT_VIEW:
elog(ERROR, "unsupported object type: %d", (int) objtype);
}
return "???"; /* keep compiler quiet */
}