Триггеры событий
В дополнение к механизму триггеров, описанному в главе Триггеры, QHB также предоставляет триггеры событий. В отличие от обычных триггеров, которые прикрепляются к одной таблице и захватывают только события DML, триггеры событий являются глобальными для конкретной базы данных и способны захватывать события DDL.
Как и обычные триггеры, триггеры событий можно написать на любом процедурном языке, который включает поддержку триггеров событий на C/RUST, но не на простом SQL.
Обзор поведения триггеров событий
Триггер события срабатывает всякий раз, когда событие, с которым он связан, происходит в базе данных, в которой он определен. В настоящее время поддерживаются только события ddl_command_start, ddl_command_end, table_rewrite и sql_drop. Поддержка дополнительных событий может быть добавлена в будущих выпусках.
Событие ddl_command_start наступает непосредственно перед выполнением
команды CREATE
, ALTER
, DROP
, SECURITY LABEL
, COMMENT
, GRANT
или
REVOKE
. Перед срабатыванием триггера события не проверяется, существует ли
затронутый объект или нет. Однако, в виде исключения, это событие не наступает
для команд DDL, направленных на разделяемые объекты — базы данных, роли и табличные
пространства, — или для команд, направленных на сами триггеры событий. Механизм
триггеров событий не поддерживает эти типы объектов. Кроме того, событие
ddl_command_start наступает непосредственно перед выполнением команды SELECT INTO
, поскольку это равнозначно CREATE TABLE AS
.
Событие ddl_command_end наступает сразу после выполнения этого же набора команд. Для получения более подробной информации о выполненных операциях DDL воспользуйтесь функцией pg_event_trigger_ddl_commands(), возвращающей множества, из кода триггера события ddl_command_end (см. раздел Функции триггеров событий). Обратите внимание, что триггер срабатывает после выполнения действий (но до фиксации транзакции), поэтому системные каталоги могут быть прочитаны как уже измененные.
Событие sql_drop наступает непосредственно перед триггером ddl_command_end для любой операции, которая удаляет объекты базы данных. Для просмотра списка удаленных объектов воспользуйтесь функцией pg_event_trigger_dropped_objects(), возвращающей множества, из кода триггера события sql_drop (см. раздел Функции триггеров событий). Обратите внимание, что этот триггер выполняется после того, как объекты были удалены из системных каталогов, поэтому их больше невозможно увидеть.
Событие table_rewrite наступает непосредственно перед перезаписью таблицы
определенными действиями команд ALTER TABLE
и ALTER TYPE
. Хотя другие
управляющие операторы, например CLUSTER
и VACUUM
, тоже способны перезаписать
таблицу, событие table_rewrite для них не запускается.
Триггеры событий (как и другие функции) нельзя выполнить в прерванной транзакции. Таким образом, если команда DDL завершается с ошибкой, любые связанные триггеры ddl_command_end выполнены не будут. И наоборот, если триггер ddl_command_start завершается с ошибкой, дальнейшие триггеры событий не сработают, и попыток выполнить саму команду тоже предпринято не будет. Аналогичным образом если триггер ddl_command_end завершается с ошибкой, действия оператора DDL откатятся, как и в любом другом случае прерывания содержащей их транзакции.
Полный список команд, поддерживаемых механизмом запуска событий, см. в разделе Матрица срабатывания триггеров событий.
Триггеры событий создаются с помощью команды CREATE EVENT TRIGGER
. Чтобы создать
триггер события, сначала следует создать функцию со специальным возвращаемым типом
event_trigger. Эта функция не должна возвращать значение (и может этого не
делать); возвращаемый тип служит просто сигналом, что функция будет вызываться
как триггер события.
Если для определенного события определено более одного триггера события, они будут срабатывать в алфавитном порядке по имени триггера.
В определении триггера также можно указать условие WHEN, чтобы, например, триггер ddl_command_start мог срабатывать только для определенных команд, которые пользователь желает перехватить. Обычно такие триггеры используются для ограничения диапазона операций DDL, которые могут осуществлять пользователи.
Матрица срабатывания триггеров событий
В Таблице 1 перечислены все команды, для которых поддерживаются триггеры событий.
Таблица 1. Поддержка триггеров событий по тегам команд
Командный тег | ddl_command_start | ddl_command_end | sql_drop | table_rewrite | Примечания |
---|---|---|---|---|---|
ALTER AGGREGATE | X | X | - | - | |
ALTER COLLATION | X | X | - | - | |
ALTER CONVERSION | X | X | - | - | |
ALTER DOMAIN | X | X | - | - | |
ALTER DEFAULT PRIVILEGES | X | X | - | - | |
ALTER EXTENSION | X | X | - | - | |
ALTER FOREIGN DATA WRAPPER | X | X | - | - | |
ALTER FOREIGN TABLE | X | X | X | - | |
ALTER FUNCTION | X | X | - | - | |
ALTER LANGUAGE | X | X | - | - | |
ALTER LARGE OBJECT | X | X | - | - | |
ALTER MATERIALIZED VIEW | X | X | - | - | |
ALTER OPERATOR | X | X | - | - | |
ALTER OPERATOR CLASS | X | X | - | - | |
ALTER OPERATOR FAMILY | X | X | - | - | |
ALTER POLICY | X | X | - | - | |
ALTER PROCEDURE | X | X | - | - | |
ALTER PUBLICATION | X | X | - | - | |
ALTER ROUTINE | X | X | - | - | |
ALTER SCHEMA | X | X | - | - | |
ALTER SEQUENCE | X | X | - | - | |
ALTER SERVER | X | X | - | - | |
ALTER STATISTICS | X | X | - | - | |
ALTER SUBSCRIPTION | X | X | - | - | |
ALTER TABLE | X | X | X | X | |
ALTER TEXT SEARCH CONFIGURATION | X | X | - | - | |
ALTER TEXT SEARCH DICTIONARY | X | X | - | - | |
ALTER TEXT SEARCH PARSER | X | X | - | - | |
ALTER TEXT SEARCH TEMPLATE | X | X | - | - | |
ALTER TRIGGER | X | X | - | - | |
ALTER TYPE | X | X | - | X | |
ALTER USER MAPPING | X | X | - | - | |
ALTER VIEW | X | X | - | - | |
COMMENT | X | X | - | - | Только для локальных объектов |
CREATE ACCESS METHOD | X | X | - | - | |
CREATE AGGREGATE | X | X | - | - | |
CREATE CAST | X | X | - | - | |
CREATE COLLATION | X | X | - | - | |
CREATE CONVERSION | X | X | - | - | |
CREATE DOMAIN | X | X | - | - | |
CREATE EXTENSION | X | X | - | - | |
CREATE FOREIGN DATA WRAPPER | X | X | - | - | |
CREATE FOREIGN TABLE | X | X | - | - | |
CREATE FUNCTION | X | X | - | - | |
CREATE INDEX | X | X | - | - | |
CREATE LANGUAGE | X | X | - | - | |
CREATE MATERIALIZED VIEW | X | X | - | - | |
CREATE OPERATOR | X | X | - | - | |
CREATE OPERATOR CLASS | X | X | - | - | |
CREATE OPERATOR FAMILY | X | X | - | - | |
CREATE POLICY | X | X | - | - | |
CREATE PROCEDURE | X | X | - | - | |
CREATE PUBLICATION | X | X | - | - | |
CREATE RULE | X | X | - | - | |
CREATE SCHEMA | X | X | - | - | |
CREATE SEQUENCE | X | X | - | - | |
CREATE SERVER | X | X | - | - | |
CREATE STATISTICS | X | X | - | - | |
CREATE SUBSCRIPTION | X | X | - | - | |
CREATE TABLE | X | X | - | - | |
CREATE TABLE AS | X | X | - | - | |
CREATE TEXT SEARCH CONFIGURATION | X | X | - | - | |
CREATE TEXT SEARCH DICTIONARY | X | X | - | - | |
CREATE TEXT SEARCH PARSER | X | X | - | - | |
CREATE TEXT SEARCH TEMPLATE | X | X | - | - | |
CREATE TRIGGER | X | X | - | - | |
CREATE TYPE | X | X | - | - | |
CREATE USER MAPPING | X | X | - | - | |
CREATE VIEW | X | X | - | - | |
DROP ACCESS METHOD | X | X | X | - | |
DROP AGGREGATE | X | X | X | - | |
DROP CAST | X | X | X | - | |
DROP COLLATION | X | X | X | - | |
DROP CONVERSION | X | X | X | - | |
DROP DOMAIN | X | X | X | - | |
DROP EXTENSION | X | X | X | - | |
DROP FOREIGN DATA WRAPPER | X | X | X | - | |
DROP FOREIGN TABLE | X | X | X | - | |
DROP FUNCTION | X | X | X | - | |
DROP INDEX | X | X | X | - | |
DROP LANGUAGE | X | X | X | - | |
DROP MATERIALIZED VIEW | X | X | X | - | |
DROP OPERATOR | X | X | X | - | |
DROP OPERATOR CLASS | X | X | X | - | |
DROP OPERATOR FAMILY | X | X | X | - | |
DROP OWNED | X | X | X | - | |
DROP POLICY | X | X | X | - | |
DROP PROCEDURE | X | X | X | - | |
DROP PUBLICATION | X | X | X | - | |
DROP ROUTINE | X | X | X | - | |
DROP RULE | X | X | X | - | |
DROP SCHEMA | X | X | X | - | |
DROP SEQUENCE | X | X | X | - | |
DROP SERVER | X | X | X | - | |
DROP STATISTICS | X | X | X | - | |
DROP SUBSCRIPTION | X | X | X | - | |
DROP TABLE | X | X | X | - | |
DROP TEXT SEARCH CONFIGURATION | X | X | X | - | |
DROP TEXT SEARCH DICTIONARY | X | X | X | - | |
DROP TEXT SEARCH PARSER | X | X | X | - | |
DROP TEXT SEARCH TEMPLATE | X | X | X | - | |
DROP TRIGGER | X | X | X | - | |
DROP TYPE | X | X | X | - | |
DROP USER MAPPING | X | X | X | - | |
DROP VIEW | X | X | X | - | |
GRANT | X | X | - | - | Только для локальных объектов |
IMPORT FOREIGN SCHEMA | X | X | - | - | |
REFRESH MATERIALIZED VIEW | X | X | - | - | |
REVOKE | X | X | - | - | Только для локальных объектов |
SECURITY LABEL | X | X | - | - | Только для локальных объектов |
SELECT INTO | X | X | - | - |
Написание триггерных функций событий на C/RUST
В этом разделе описываются низкоуровневые детали интерфейса для триггерной функции события. Эта информация необходима только при написании функций триггера событий на C/RUST. Если вы используете язык более высокого уровня, эти детали обрабатываются незаметно для вас. В большинстве случаев вам следует рассмотреть возможность использовании процедурного языка, прежде чем писать свои триггеры событий на C/RUST. В документации каждого процедурного языка объясняется, как написать на нем триггер события.
Триггерные функции событий должны использовать интерфейс менеджера функций «version 1».
Когда функция вызывается менеджером триггеров событий, ей не передаются никакие обычные аргументы, но передается указатель «context», указывающий на структуру EventTriggerData. Функции на C/RUST могут проверить, были ли они вызваны из менеджера триггеров событий или нет, выполнив следующий макрос:
CALLED_AS_EVENT_TRIGGER(fcinfo)
который разворачивается в:
((fcinfo)->context != NULL && IsA((fcinfo)->context, EventTriggerData))
Если эта запись возвращает true, то можно безопасно привести fcinfo->context к типу EventTriggerData * и использовать структуру EventTriggerData. Функция не должна изменять структуру EventTriggerData или любые данные, на которые она указывает.
struct EventTriggerData определена в commands/event_trigger.h:
typedef struct EventTriggerData
{
NodeTag type;
const char *event; /* имя события */
Node *parsetree; /* дерево синтаксического анализа */
const char *tag; /* тег команды */
} EventTriggerData;
где члены определены следующим образом:
type
Всегда T_EventTriggerData.
event
Описывает событие, для которого вызывается функция: "ddl_command_start",
"ddl_command_end", "sql_drop" или "table_rewrite" (значения этих событий см.
в разделе Обзор поведения триггеров событий).
parsetree
Указатель на дерево синтаксического анализа команды. Детали можно уточнить,
просмотрев исходный код QHB. Структура дерева анализа может быть
изменена без предварительного уведомления.
tag
Тег команды, связанный с событием, для которого запускается триггер события,
например "CREATE FUNCTION".
Триггерная функция события должна возвращать указатель NULL (но не SQL-значение NULL, т. е. не устанавливать isNull равным true).
Полный пример триггера события
Вот очень простой пример триггерной функции события, написанной на C/RUST. (Примеры триггеров, написанных на процедурных языках, можно найти в документации по процедурным языкам.)
Функция noddl выдает ошибку при каждом вызове. Определение триггера события связало эту функцию с событием ddl_command_start. В результате все команды DDL (за исключением упомянутых в разделе Обзор поведения триггеров событий) невозможно запустить.
Это исходный код триггерной функции:
#include "qhb.h"
#include "commands/event_trigger.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(noddl);
Datum
noddl(PG_FUNCTION_ARGS)
{
EventTriggerData *trigdata;
if (!CALLED_AS_EVENT_TRIGGER(fcinfo)) /* внутренняя ошибка */
elog(ERROR, "not fired by event trigger manager");
/* ОШИБКА, "не запускается менеджером триггеров событий" */
trigdata = (EventTriggerData *) fcinfo->context;
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("command \"%s\" denied", trigdata->tag)));
PG_RETURN_NULL();
}
После компиляции исходного кода (см. подраздел Компиляция и связывание динамически загружаемых функций), объявляем функцию и триггеры:
CREATE FUNCTION noddl() RETURNS event_trigger
AS 'noddl' LANGUAGE C;
CREATE EVENT TRIGGER noddl ON ddl_command_start
EXECUTE FUNCTION noddl();
Теперь можно проверить работу триггера:
=# \dy
List of event triggers
Name | Event | Owner | Enabled | Function | Tags
-------+-------------------+-------+---------+----------+------
noddl | ddl_command_start | dim | enabled | noddl |
(1 row)
=# CREATE TABLE foo(id serial);
ERROR: command "CREATE TABLE" denied
/* ОШИБКА: команда "CREATE TABLE" отклонена */
В этой ситуации, чтобы иметь возможность при необходимости запускать некоторые команды DDL, следует либо удалить триггер события, либо выключить его. Также может быть удобно выключить триггер только на время транзакции:
BEGIN;
ALTER EVENT TRIGGER noddl DISABLE;
CREATE TABLE foo (id serial);
ALTER EVENT TRIGGER noddl ENABLE;
COMMIT;
(Напомним, что триггеры событий не действуют на команды DDL самих триггеров событий.)
Пример триггера события перезаписи таблицы
Благодаря событию table_rewrite можно реализовать политику перезаписи таблицы, разрешив перезапись только в определенные моменты обслуживания.
Вот пример реализации такой политики.
CREATE OR REPLACE FUNCTION no_rewrite()
RETURNS event_trigger
LANGUAGE plpgsql AS
$$
---
--- Реализация локальной политики перезаписи таблицы:
--- перезапись public.foo не допускается ни при каких обстоятельствах
--- другие таблицы могут перезаписываться только между 1 часом ночи и 6 часами утра,
--- если только их размер не превышает 100 блоков
---
DECLARE
table_oid oid := pg_event_trigger_table_rewrite_oid();
current_hour integer := extract('hour' from current_time);
pages integer;
max_pages integer := 100;
BEGIN
IF pg_event_trigger_table_rewrite_oid() = 'public.foo'::regclass
THEN
RAISE EXCEPTION 'you''re not allowed to rewrite the table %',
table_oid::regclass;
END IF;
SELECT INTO pages relpages FROM pg_class WHERE oid = table_oid;
IF pages > max_pages
THEN
RAISE EXCEPTION 'rewrites only allowed for table with less than % pages',
max_pages;
END IF;
IF current_hour NOT BETWEEN 1 AND 6
THEN
RAISE EXCEPTION 'rewrites only allowed between 1am and 6am';
END IF;
END;
$$;
CREATE EVENT TRIGGER no_rewrite_allowed
ON table_rewrite
EXECUTE FUNCTION no_rewrite();