Commit 473ab40c authored by Alvaro Herrera's avatar Alvaro Herrera

Add sql_drop event for event triggers

This event takes place just before ddl_command_end, and is fired if and
only if at least one object has been dropped by the command.  (For
instance, DROP TABLE IF EXISTS of a table that does not in fact exist
will not lead to such a trigger firing).  Commands that drop multiple
objects (such as DROP SCHEMA or DROP OWNED BY) will cause a single event
to fire.  Some firings might be surprising, such as
ALTER TABLE DROP COLUMN.

The trigger is fired after the drop has taken place, because that has
been deemed the safest design, to avoid exposing possibly-inconsistent
internal state (system catalogs as well as current transaction) to the
user function code.  This means that careful tracking of object
identification is required during the object removal phase.

Like other currently existing events, there is support for tag
filtering.

To support the new event, add a new pg_event_trigger_dropped_objects()
set-returning function, which returns a set of rows comprising the
objects affected by the command.  This is to be used within the user
function code, and is mostly modelled after the recently introduced
pg_identify_object() function.

Catalog version bumped due to the new function.

Dimitri Fontaine and Álvaro Herrera
Review by Robert Haas, Tom Lane
parent 593c39d1
This diff is collapsed.
......@@ -15980,4 +15980,117 @@ FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger();
<xref linkend="SQL-CREATETRIGGER">.
</para>
</sect1>
<sect1 id="functions-event-triggers">
<title>Event Trigger Functions</title>
<indexterm>
<primary>pg_event_trigger_dropped_objects</primary>
</indexterm>
<para>
Currently <productname>PostgreSQL</> provides one built-in event trigger
helper function, <function>pg_event_trigger_dropped_objects</>.
</para>
<para>
<function>pg_event_trigger_dropped_objects</> returns a list of all object
dropped by the command in whose <literal>sql_drop</> event it is called.
If called in any other context,
<function>pg_event_trigger_dropped_objects</> raises an error.
<function>pg_event_trigger_dropped_objects</> returns the following columns:
<informaltable>
<tgroup cols="3">
<thead>
<row>
<entry>Name</entry>
<entry>Type</entry>
<entry>Description</entry>
</row>
</thead>
<tbody>
<row>
<entry><literal>classid</literal></entry>
<entry><type>Oid</type></entry>
<entry>OID of catalog the object belonged in</entry>
</row>
<row>
<entry><literal>objid</literal></entry>
<entry><type>Oid</type></entry>
<entry>OID the object had within the catalog</entry>
</row>
<row>
<entry><literal>objsubid</literal></entry>
<entry><type>int32</type></entry>
<entry>Object sub-id (e.g. attribute number for columns)</entry>
</row>
<row>
<entry><literal>object_type</literal></entry>
<entry><type>text</type></entry>
<entry>Type of the object</entry>
</row>
<row>
<entry><literal>schema_name</literal></entry>
<entry><type>text</type></entry>
<entry>
Name of the schema the object belonged in, if any; otherwise <literal>NULL</>.
No quoting is applied.
</entry>
</row>
<row>
<entry><literal>object_name</literal></entry>
<entry><type>text</type></entry>
<entry>
Name of the object, if the combination of schema and name can be
used as an unique identifier for the object; otherwise <literal>NULL</>.
No quoting is applied, and name is never schema-qualified.
</entry>
</row>
<row>
<entry><literal>object_identity</literal></entry>
<entry><type>text</type></entry>
<entry>
Text rendering of the object identity, schema-qualified. Each and every
identifier present in the identity is quoted if necessary.
</entry>
</row>
</tbody>
</tgroup>
</informaltable>
</para>
<para>
The <function>pg_event_trigger_dropped_objects</> function can be used
in an event trigger like this:
<programlisting>
CREATE FUNCTION test_event_trigger_for_drops()
RETURNS event_trigger LANGUAGE plpgsql AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
LOOP
RAISE NOTICE '% dropped object: % %.% %',
tg_tag,
obj.object_type,
obj.schema_name,
obj.object_name,
obj.object_identity;
END LOOP;
END
$$;
CREATE EVENT TRIGGER test_event_trigger_for_drops
ON sql_drop
EXECUTE PROCEDURE test_event_trigger_for_drops();
</programlisting>
</para>
<para>
For more information about event triggers,
see <xref linkend="event-triggers">.
</para>
</sect1>
</chapter>
......@@ -190,6 +190,44 @@ static bool stack_address_present_add_flags(const ObjectAddress *object,
ObjectAddressStack *stack);
/*
* Go through the objects given running the final actions on them, and execute
* the actual deletion.
*/
static void
deleteObjectsInList(ObjectAddresses *targetObjects, Relation *depRel,
int flags)
{
int i;
/*
* Keep track of objects for event triggers, if necessary.
*/
if (trackDroppedObjectsNeeded())
{
for (i = 0; i < targetObjects->numrefs; i++)
{
ObjectAddress *thisobj = targetObjects->refs + i;
if ((!(flags & PERFORM_DELETION_INTERNAL)) &&
EventTriggerSupportsObjectType(getObjectClass(thisobj)))
{
EventTriggerSQLDropAddObject(thisobj);
}
}
}
/*
* Delete all the objects in the proper order.
*/
for (i = 0; i < targetObjects->numrefs; i++)
{
ObjectAddress *thisobj = targetObjects->refs + i;
deleteOneObject(thisobj, depRel, flags);
}
}
/*
* performDeletion: attempt to drop the specified object. If CASCADE
* behavior is specified, also drop any dependent objects (recursively).
......@@ -215,7 +253,6 @@ performDeletion(const ObjectAddress *object,
{
Relation depRel;
ObjectAddresses *targetObjects;
int i;
/*
* We save some cycles by opening pg_depend just once and passing the
......@@ -250,15 +287,8 @@ performDeletion(const ObjectAddress *object,
NOTICE,
object);
/*
* Delete all the objects in the proper order.
*/
for (i = 0; i < targetObjects->numrefs; i++)
{
ObjectAddress *thisobj = targetObjects->refs + i;
deleteOneObject(thisobj, &depRel, flags);
}
/* do the deed */
deleteObjectsInList(targetObjects, &depRel, flags);
/* And clean up */
free_object_addresses(targetObjects);
......@@ -332,15 +362,8 @@ performMultipleDeletions(const ObjectAddresses *objects,
NOTICE,
(objects->numrefs == 1 ? objects->refs : NULL));
/*
* Delete all the objects in the proper order.
*/
for (i = 0; i < targetObjects->numrefs; i++)
{
ObjectAddress *thisobj = targetObjects->refs + i;
deleteOneObject(thisobj, &depRel, flags);
}
/* do the deed */
deleteObjectsInList(targetObjects, &depRel, flags);
/* And clean up */
free_object_addresses(targetObjects);
......@@ -356,6 +379,10 @@ performMultipleDeletions(const ObjectAddresses *objects,
* This is currently used only to clean out the contents of a schema
* (namespace): the passed object is a namespace. We normally want this
* to be done silently, so there's an option to suppress NOTICE messages.
*
* Note we don't fire object drop event triggers here; it would be wrong to do
* so for the current only use of this function, but if more callers are added
* this might need to be reconsidered.
*/
void
deleteWhatDependsOn(const ObjectAddress *object,
......
This diff is collapsed.
......@@ -4478,7 +4478,6 @@ DropTrigStmt:
*
* QUERIES :
* CREATE EVENT TRIGGER ...
* DROP EVENT TRIGGER ...
* ALTER EVENT TRIGGER ...
*
*****************************************************************************/
......
......@@ -351,6 +351,7 @@ ProcessUtility(Node *parsetree,
fncall; \
if (isCompleteQuery) \
{ \
EventTriggerSQLDrop(parsetree); \
EventTriggerDDLCommandEnd(parsetree); \
} \
} while (0)
......@@ -366,10 +367,48 @@ ProcessUtility(Node *parsetree,
fncall; \
if (_supported) \
{ \
EventTriggerSQLDrop(parsetree); \
EventTriggerDDLCommandEnd(parsetree); \
} \
} while (0)
/*
* UTILITY_BEGIN_QUERY and UTILITY_END_QUERY are a pair of macros to enclose
* execution of a single DDL command, to ensure the event trigger environment
* is appropriately set up before starting, and tore down after completion or
* error.
*/
#define UTILITY_BEGIN_QUERY(isComplete) \
do { \
bool _needCleanup = false; \
\
if (isComplete) \
{ \
_needCleanup = EventTriggerBeginCompleteQuery(); \
} \
\
PG_TRY(); \
{ \
/* avoid empty statement when followed by a semicolon */ \
(void) 0
#define UTILITY_END_QUERY() \
} \
PG_CATCH(); \
{ \
if (_needCleanup) \
{ \
EventTriggerEndCompleteQuery(); \
} \
PG_RE_THROW(); \
} \
PG_END_TRY(); \
if (_needCleanup) \
{ \
EventTriggerEndCompleteQuery(); \
} \
} while (0)
void
standard_ProcessUtility(Node *parsetree,
const char *queryString,
......@@ -386,6 +425,8 @@ standard_ProcessUtility(Node *parsetree,
if (completionTag)
completionTag[0] = '\0';
UTILITY_BEGIN_QUERY(isCompleteQuery);
switch (nodeTag(parsetree))
{
/*
......@@ -615,8 +656,11 @@ standard_ProcessUtility(Node *parsetree,
}
if (isCompleteQuery)
{
EventTriggerSQLDrop(parsetree);
EventTriggerDDLCommandEnd(parsetree);
}
}
break;
case T_CreateTableSpaceStmt:
......@@ -726,7 +770,10 @@ standard_ProcessUtility(Node *parsetree,
if (isCompleteQuery
&& EventTriggerSupportsObjectType(stmt->removeType))
{
EventTriggerSQLDrop(parsetree);
EventTriggerDDLCommandEnd(parsetree);
}
break;
}
......@@ -856,6 +903,12 @@ standard_ProcessUtility(Node *parsetree,
ereport(NOTICE,
(errmsg("relation \"%s\" does not exist, skipping",
atstmt->relation->relname)));
if (isCompleteQuery)
{
EventTriggerSQLDrop(parsetree);
EventTriggerDDLCommandEnd(parsetree);
}
}
break;
......@@ -1248,8 +1301,9 @@ standard_ProcessUtility(Node *parsetree,
break;
case T_DropOwnedStmt:
/* no event triggers for global objects */
DropOwnedObjects((DropOwnedStmt *) parsetree);
InvokeDDLCommandEventTriggers(
parsetree,
DropOwnedObjects((DropOwnedStmt *) parsetree));
break;
case T_ReassignOwnedStmt:
......@@ -1372,6 +1426,8 @@ standard_ProcessUtility(Node *parsetree,
(int) nodeTag(parsetree));
break;
}
UTILITY_END_QUERY();
}
/*
......
......@@ -345,7 +345,7 @@ format_procedure_internal(Oid procedure_oid, bool force_qualify)
/*
* Would this proc be found (given the right args) by regprocedurein?
* If not, we need to qualify it.
* If not, or if caller requests it, we need to qualify it.
*/
if (!force_qualify && FunctionIsVisible(procedure_oid))
nspname = NULL;
......
......@@ -169,6 +169,8 @@ BuildEventTriggerCache(void)
event = EVT_DDLCommandStart;
else if (strcmp(evtevent, "ddl_command_end") == 0)
event = EVT_DDLCommandEnd;
else if (strcmp(evtevent, "sql_drop") == 0)
event = EVT_SQLDrop;
else
continue;
......
......@@ -53,6 +53,6 @@
*/
/* yyyymmddN */
#define CATALOG_VERSION_NO 201303201
#define CATALOG_VERSION_NO 201303271
#endif
......@@ -4693,6 +4693,9 @@ DATA(insert OID = 3473 ( spg_range_quad_leaf_consistent PGNSP PGUID 12 1 0 0 0
DESCR("SP-GiST support for quad tree over range");
/* event triggers */
DATA(insert OID = 3566 ( pg_event_trigger_dropped_objects PGNSP PGUID 12 10 100 0 0 f f f f t t s 0 0 2249 "" "{26,26,23,25,25,25,25}" "{o,o,o,o,o,o,o}" "{classid, objid, objsubid, object_type, schema_name, object_name, object_identity}" _null_ pg_event_trigger_dropped_objects _null_ _null_ _null_ ));
DESCR("list objects dropped by the current command");
/*
* Symbolic values for provolatile column: these indicate whether the result
* of a function is dependent *only* on the values of its explicit arguments,
......
......@@ -13,13 +13,14 @@
#ifndef EVENT_TRIGGER_H
#define EVENT_TRIGGER_H
#include "catalog/objectaddress.h"
#include "catalog/pg_event_trigger.h"
#include "nodes/parsenodes.h"
typedef struct EventTriggerData
{
NodeTag type;
char *event; /* event name */
const char *event; /* event name */
Node *parsetree; /* parse tree */
const char *tag; /* command tag */
} EventTriggerData;
......@@ -42,5 +43,11 @@ extern void AlterEventTriggerOwner_oid(Oid, Oid newOwnerId);
extern bool EventTriggerSupportsObjectType(ObjectType obtype);
extern void EventTriggerDDLCommandStart(Node *parsetree);
extern void EventTriggerDDLCommandEnd(Node *parsetree);
extern void EventTriggerSQLDrop(Node *parsetree);
extern bool EventTriggerBeginCompleteQuery(void);
extern void EventTriggerEndCompleteQuery(void);
extern bool trackDroppedObjectsNeeded(void);
extern void EventTriggerSQLDropAddObject(ObjectAddress *object);
#endif /* EVENT_TRIGGER_H */
......@@ -1151,6 +1151,9 @@ extern Datum pg_identify_object(PG_FUNCTION_ARGS);
/* commands/constraint.c */
extern Datum unique_key_recheck(PG_FUNCTION_ARGS);
/* commands/event_trigger.c */
extern Datum pg_event_trigger_dropped_objects(PG_FUNCTION_ARGS);
/* commands/extension.c */
extern Datum pg_available_extensions(PG_FUNCTION_ARGS);
extern Datum pg_available_extension_versions(PG_FUNCTION_ARGS);
......
......@@ -19,7 +19,8 @@
typedef enum
{
EVT_DDLCommandStart,
EVT_DDLCommandEnd
EVT_DDLCommandEnd,
EVT_SQLDrop
} EventTriggerEvent;
typedef struct
......
This diff is collapsed.
......@@ -97,10 +97,112 @@ drop event trigger regress_event_trigger;
-- should fail, regression_bob owns regress_event_trigger2/3
drop role regression_bob;
-- cleanup before next test
-- these are all OK; the second one should emit a NOTICE
drop event trigger if exists regress_event_trigger2;
drop event trigger if exists regress_event_trigger2;
drop event trigger regress_event_trigger3;
drop event trigger regress_event_trigger_end;
drop function test_event_trigger();
drop role regression_bob;
-- test support for dropped objects
CREATE SCHEMA schema_one authorization regression_bob;
CREATE SCHEMA schema_two authorization regression_bob;
CREATE SCHEMA audit_tbls authorization regression_bob;
SET SESSION AUTHORIZATION regression_bob;
CREATE TABLE schema_one.table_one(a int);
CREATE TABLE schema_one."table two"(a int);
CREATE TABLE schema_one.table_three(a int);
CREATE TABLE audit_tbls.schema_one_table_two(the_value text);
CREATE TABLE schema_two.table_two(a int);
CREATE TABLE schema_two.table_three(a int, b text);
CREATE TABLE audit_tbls.schema_two_table_three(the_value text);
CREATE OR REPLACE FUNCTION schema_two.add(int, int) RETURNS int LANGUAGE plpgsql
CALLED ON NULL INPUT
AS $$ BEGIN RETURN coalesce($1,0) + coalesce($2,0); END; $$;
CREATE AGGREGATE schema_two.newton
(BASETYPE = int, SFUNC = schema_two.add, STYPE = int);
RESET SESSION AUTHORIZATION;
CREATE TABLE undroppable_objs (
object_type text,
object_identity text
);
INSERT INTO undroppable_objs VALUES
('table', 'schema_one.table_three'),
('table', 'audit_tbls.schema_two_table_three');
CREATE TABLE dropped_objects (
type text,
schema text,
object text
);
-- This tests errors raised within event triggers; the one in audit_tbls
-- uses 2nd-level recursive invocation via test_evtrig_dropped_objects().
CREATE OR REPLACE FUNCTION undroppable() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
obj record;
BEGIN
PERFORM 1 FROM pg_tables WHERE tablename = 'undroppable_objs';
IF NOT FOUND THEN
RAISE NOTICE 'table undroppable_objs not found, skipping';
RETURN;
END IF;
FOR obj IN
SELECT * FROM pg_event_trigger_dropped_objects() JOIN
undroppable_objs USING (object_type, object_identity)
LOOP
RAISE EXCEPTION 'object % of type % cannot be dropped',
obj.object_identity, obj.object_type;
END LOOP;
END;
$$;
CREATE EVENT TRIGGER undroppable ON sql_drop
EXECUTE PROCEDURE undroppable();
CREATE OR REPLACE FUNCTION test_evtrig_dropped_objects() RETURNS event_trigger
LANGUAGE plpgsql AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
LOOP
IF obj.object_type = 'table' THEN
EXECUTE format('DROP TABLE IF EXISTS audit_tbls.%I',
format('%s_%s', obj.schema_name, obj.object_name));
END IF;
INSERT INTO dropped_objects
(type, schema, object) VALUES
(obj.object_type, obj.schema_name, obj.object_identity);
END LOOP;
END
$$;
CREATE EVENT TRIGGER regress_event_trigger_drop_objects ON sql_drop
WHEN TAG IN ('drop table', 'drop function', 'drop view',
'drop owned', 'drop schema', 'alter table')
EXECUTE PROCEDURE test_evtrig_dropped_objects();
ALTER TABLE schema_one.table_one DROP COLUMN a;
DROP SCHEMA schema_one, schema_two CASCADE;
DELETE FROM undroppable_objs WHERE object_identity = 'audit_tbls.schema_two_table_three';
DROP SCHEMA schema_one, schema_two CASCADE;
DELETE FROM undroppable_objs WHERE object_identity = 'schema_one.table_three';
DROP SCHEMA schema_one, schema_two CASCADE;
SELECT * FROM dropped_objects WHERE schema IS NULL OR schema <> 'pg_toast';
DROP OWNED BY regression_bob;
SELECT * FROM dropped_objects WHERE type = 'schema';
DROP ROLE regression_bob;
DROP EVENT TRIGGER regress_event_trigger_drop_objects;
DROP EVENT TRIGGER undroppable;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment