Триггеры событий

В дополнение к механизму триггеров, описанному в главе Триггеры, 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_startddl_command_endsql_droptable_rewriteПримечания
ALTER AGGREGATEXX--
ALTER COLLATIONXX--
ALTER CONVERSIONXX--
ALTER DOMAINXX--
ALTER DEFAULT PRIVILEGESXX--
ALTER EXTENSIONXX--
ALTER FOREIGN DATA WRAPPERXX--
ALTER FOREIGN TABLEXXX-
ALTER FUNCTIONXX--
ALTER LANGUAGEXX--
ALTER LARGE OBJECTXX--
ALTER MATERIALIZED VIEWXX--
ALTER OPERATORXX--
ALTER OPERATOR CLASSXX--
ALTER OPERATOR FAMILYXX--
ALTER POLICYXX--
ALTER PROCEDUREXX--
ALTER PUBLICATIONXX--
ALTER ROUTINEXX--
ALTER SCHEMAXX--
ALTER SEQUENCEXX--
ALTER SERVERXX--
ALTER STATISTICSXX--
ALTER SUBSCRIPTIONXX--
ALTER TABLEXXXX
ALTER TEXT SEARCH CONFIGURATIONXX--
ALTER TEXT SEARCH DICTIONARYXX--
ALTER TEXT SEARCH PARSERXX--
ALTER TEXT SEARCH TEMPLATEXX--
ALTER TRIGGERXX--
ALTER TYPEXX-X
ALTER USER MAPPINGXX--
ALTER VIEWXX--
COMMENTXX--Только для локальных объектов
CREATE ACCESS METHODXX--
CREATE AGGREGATEXX--
CREATE CASTXX--
CREATE COLLATIONXX--
CREATE CONVERSIONXX--
CREATE DOMAINXX--
CREATE EXTENSIONXX--
CREATE FOREIGN DATA WRAPPERXX--
CREATE FOREIGN TABLEXX--
CREATE FUNCTIONXX--
CREATE INDEXXX--
CREATE LANGUAGEXX--
CREATE MATERIALIZED VIEWXX--
CREATE OPERATORXX--
CREATE OPERATOR CLASSXX--
CREATE OPERATOR FAMILYXX--
CREATE POLICYXX--
CREATE PROCEDUREXX--
CREATE PUBLICATIONXX--
CREATE RULEXX--
CREATE SCHEMAXX--
CREATE SEQUENCEXX--
CREATE SERVERXX--
CREATE STATISTICSXX--
CREATE SUBSCRIPTIONXX--
CREATE TABLEXX--
CREATE TABLE ASXX--
CREATE TEXT SEARCH CONFIGURATIONXX--
CREATE TEXT SEARCH DICTIONARYXX--
CREATE TEXT SEARCH PARSERXX--
CREATE TEXT SEARCH TEMPLATEXX--
CREATE TRIGGERXX--
CREATE TYPEXX--
CREATE USER MAPPINGXX--
CREATE VIEWXX--
DROP ACCESS METHODXXX-
DROP AGGREGATEXXX-
DROP CASTXXX-
DROP COLLATIONXXX-
DROP CONVERSIONXXX-
DROP DOMAINXXX-
DROP EXTENSIONXXX-
DROP FOREIGN DATA WRAPPERXXX-
DROP FOREIGN TABLEXXX-
DROP FUNCTIONXXX-
DROP INDEXXXX-
DROP LANGUAGEXXX-
DROP MATERIALIZED VIEWXXX-
DROP OPERATORXXX-
DROP OPERATOR CLASSXXX-
DROP OPERATOR FAMILYXXX-
DROP OWNEDXXX-
DROP POLICYXXX-
DROP PROCEDUREXXX-
DROP PUBLICATIONXXX-
DROP ROUTINEXXX-
DROP RULEXXX-
DROP SCHEMAXXX-
DROP SEQUENCEXXX-
DROP SERVERXXX-
DROP STATISTICSXXX-
DROP SUBSCRIPTIONXXX-
DROP TABLEXXX-
DROP TEXT SEARCH CONFIGURATIONXXX-
DROP TEXT SEARCH DICTIONARYXXX-
DROP TEXT SEARCH PARSERXXX-
DROP TEXT SEARCH TEMPLATEXXX-
DROP TRIGGERXXX-
DROP TYPEXXX-
DROP USER MAPPINGXXX-
DROP VIEWXXX-
GRANTXX--Только для локальных объектов
IMPORT FOREIGN SCHEMAXX--
REFRESH MATERIALIZED VIEWXX--
REVOKEXX--Только для локальных объектов
SECURITY LABELXX--Только для локальных объектов
SELECT INTOXX--


Написание триггерных функций событий на 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();