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

В дополнение к механизму триггера, описанному в главе 7, QHB. также предоставляет триггеры событий. В отличие от обычных триггеров, которые присоединяются к одной таблице и захватывают только события DML, триггеры событий являются глобальными для конкретной базы данных и способны захватывать события DDL.

Как и обычные триггеры, триггеры событий могут быть написаны на любом процедурном языке, который включает поддержку триггеров событий, на C или Rust, но не на простом SQL.

Обзор поведения триггера событий

Триггер события срабатывает всякий раз, когда событие, с которым оно связано, происходит в базе данных. В настоящее время поддерживаются только события ddl_command_start, ddl_command_end, table_rewrite и sql_drop. Поддержка дополнительных событий может быть добавлена в будущих выпусках.

Событие ddl_command_start наступает непосредственно перед выполнением 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 триггера события ddl_command_end (см. Раздел 9.28). Обратите внимание, что триггер срабатывает после выполнения действий (но до фиксации транзакции), и, таким образом, системные каталоги могут быть прочитаны как уже измененные.

Событие 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_start не будут предпринимать попыток выполнить саму команду. Точно так же, если триггер ddl_command_end завершается с ошибкой, эффекты оператора DDL будут откатываться, как и в любом другом случае, когда содержащая транзакция прерывается.

Полный список команд, поддерживаемых механизмом запуска событий, см. в главе Денежные Типы.

Триггеры событий создаются с помощью команды CREATE EVENT TRIGGER. Для того чтобы создать триггер события, вы должны сначала создать функцию со специальным типом возврата event_trigger. Эта функция не должна (и не может) возвращать значение; возвращаемый тип служит просто сигналом, что функция должна быть вызвана как триггер события.

Если для определенного события определено более одного триггера, они будут срабатывать в алфавитном порядке по имени триггера.

Определение триггера также может указывать условие WHEN так что, например, триггер ddl_command_start может быть запущен только для определенных команд, которые пользователь хочет перехватить. Обычно такие триггеры используются для ограничения диапазона операций DDL, которые могут выполнять пользователи.

Матрица запуска событий

В таблице 8.1 перечислены все команды, для которых поддерживаются триггеры событий.

Таблица 8.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 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 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

В этом разделе описываются подробности низкоуровневого интерфейса для функции запуска события. Эта информация необходима только при написании функций триггера событий на C. Если вы используете язык более высокого уровня, то эти детали обрабатываются автоматически. В большинстве случаев вам следует подумать об использовании процедурного языка, прежде чем писать свои триггеры событий на C. В документации каждого процедурного языка объясняется, как написать триггер событий на этом языке.

Функции триггера событий должны использовать интерфейс диспетчера «version 1».

Когда функция вызывается менеджером триггера события, ей не передаются никакие обычные аргументы кроме указателя «context», указывающий на структуру EventTriggerData. Функции на C могут проверить, были ли они вызваны из менеджера триггеров событий или нет, выполнив макрос:

CALLED_AS_EVENT_TRIGGER(fcinfo)

который распахивается в

((fcinfo)->context != NULL && IsA((fcinfo)->context, EventTriggerData))

Если это возвращает true, тогда безопасно привести fcinfo->context к типу EventTriggerData * и использовать структуру EventTriggerData. Функция не должна изменять структуру EventTriggerData или любые данные, на которые она указывает.

Структура EventTriggerData определена в commands/event_trigger.h:

typedef struct EventTriggerData
{
    NodeTag     type;
    const char *event;      /* event name */
    Node       *parsetree;  /* parse tree */
    const char *tag;        /* command tag */
} EventTriggerData;

где члены определены следующим образом:

  • type - всегда T_EventTriggerData.

  • event описывает событие, для которого вызывается функция, одно из ddl_command_start, ddl_command_end, sql_drop, table_rewrite (см. Раздел 8.1 о значении этих событий).

  • parsetree - указатель на дерево разбора команды. Проверьте исходный код QHB для уточнения деталей. Структура дерева разбора может быть изменена без предварительного уведомления.

  • tag - тег команды, связанный с событием, для которого запускается триггер события, например "CREATE FUNCTION".

Внимание! Функция триггера события должна возвращать указатель NULL (но НЕ нулевое значение SQL, т.е. не устанавливать isNull как true).

Полный пример запуска события

Вот очень простой пример функции триггера события, написанной на C. (Примеры триггеров, написанных на процедурных языках, можно найти в документации по процедурным языкам.)

Функция noddl вызывает ошибку каждый раз, когда она вызывается. Определение триггера события связало функцию с событием ddl_command_start. В результате все команды DDL (с исключениями, упомянутыми в разделе 8.1) не могут быть запущены.

Это исходный код функции триггера:

#include "postgres.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))  /* internal error */
        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();
}

После того, как вы скомпилировали исходный код (см. Раздел 37.10.5), объявите функцию и триггеры:

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

В этой ситуации, чтобы иметь возможность запускать некоторые команды 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
$$
---
--- Implement local Table Rewriting policy:
---   public.foo is not allowed rewriting, ever
---   other tables are only allowed rewriting between 1am and 6am
---   unless they have more than 100 blocks
---
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();