Написание обработчика процедурного языка

Все вызовы функций, написанных на языке, отличном от вызываемого через текущий интерфейс «версии 1» для компилируемых языков (сюда входят функции, написанные на пользовательских процедурных языках, и функции, написанные на SQL), выполняются через функцию-обработчик вызова для указанного языка. В обязанности обработчика входит выполнение функции надлежащим образом, например, интерпретируя переданный ему исходный текст этой функции. В этой главе в общих чертах рассказывается, как можно написать обработчик вызова для нового процедурного языка.

Обработчик вызова для нового процедурного языка — это «обычная» функция, которая должна быть написана на компилируемом языке, например, на C, вызывается через интерфейс версии 1 и регистрируется в QHB как не принимающая аргументы и возвращающая тип language_handler. Этот специальный псевдотип определяет функцию как обработчик вызова и препятствует ее вызову напрямую в командах SQL. Более подробную информацию о соглашении о вызовах и динамической загрузке кода на языке C см. в разделе Функции на нативном языке.

Обработчик вызова вызывается так же, как и любая другая функция: он получает указатель на структуру FunctionCallInfoBaseData, содержащую значения аргументов и информацию о вызываемой функции, и должен вернуть результат типа Datum (и, возможно, установить поле isnull в структуре FunctionCallInfoBaseData, если желает вернуть результат SQL NULL). Разница между обработчиком вызова и обычной вызываемой функцией состоит в том, что поле flinfo->fn_oid структуры FunctionCallInfoBaseData будет содержать OID фактически вызываемой функции, а не самого обработчика вызова. Обработчик вызова должен использовать это поле для определения того, какую функцию выполнить. Кроме того, список передаваемых аргументов формируется в соответствии с объявлением целевой функции, а не обработчика вызова.

Обработчик вызова должен сам выбрать запись функции системного каталога pg_proc и проанализировать типы аргументов и результаты вызываемой функции. Содержимое предложения AS команды CREATE FUNCTION для этой функции можно будет найти в столбце prosrc ее строки в pg_proc. Как правило, это исходный текст на процедурном языке, но теоретически это может быть и что-то другое, например, путь к файлу или что-то еще, указывающее обработчику вызова, что конкретно ему делать.

Зачастую одна и та же функция многократно вызывается в одном операторе SQL. Обработчик вызова может избежать повторных поисков информации о вызываемой функции, воспользовавшись полем flinfo->fn_extra. Изначально оно содержит NULL, но обработчик вызова может установить в нем указатель на нужную информацию. При последующих вызовах, если flinfo->fn_extra уже отлично от NULL, им можно воспользоваться и пропустить этап поиска информации. Обработчик вызова должен позаботиться о том, чтобы flinfo->fn_extra указывал на место в памяти, которое не будет освобождено как минимум до конца текущего запроса, поскольку именно столько может существовать структура данных FmgrInfo. Один из способов этого добиться — разместить дополнительные данные в контексте памяти, указанном в flinfo->fn_mcxt; срок жизни таких данных обычно совпадает со сроком жизни самой структуры FmgrInfo. С другой стороны, обработчик может выбрать более долгоживущий контекст памяти, чтобы иметь возможность кэшировать информацию из определения функции между запросами.

Когда функция на процедурном языке вызывается как триггер, ей не передаются аргументы обычном способом; вместо этого поле context в структуре FunctionCallInfoBaseData указывает на структуру TriggerData, а не содержит NULL, как при обычном вызове функции. А обработчик языка должен предоставить механизмы, чтобы функции на процедурном языке получили эту информацию о запуске.

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

Хотя обработчика вызова достаточно для создания простейшего процедурного языка, существуют еще две функции, которые можно предоставить дополнительно, чтобы пользоваться языком было удобнее. Это валидатор и обработчик встраиваемого кода. Валидатор позволяет проводить проверку синтаксиса конкретного языка во время CREATE FUNCTION. Обработчик же встраиваемого кода позволяет языку поддерживать выполнение анонимных блоков кода посредством команды DO.

Если для процедурного языка предоставляется валидатор, он должен быть объявлен как функция, принимающая один параметр типа oid. Результат валидатора игнорируется, поэтому обычно он объявляется как возвращающий тип void. Валидатор будет вызываться в конце выполнения команды CREATE FUNCTION, создающей или изменяющей функцию на процедурном языке. Переданный ему OID принадлежит строке в pg_proc для этой функции. Валидатор должен выбрать эту строку обычным способом и произвести все необходимые проверки. Сначала нужно вызвать CheckFunctionValidatorAccess(), чтобы валидатор отличал явные вызовы этой функции, которые пользователь не мог произвести посредством команды CREATE FUNCTION. После этого обычные проверки включают подтверждение того, что типы аргументов и результата функции поддерживаются языком и что тело функции синтаксически правильно для данного языка. Если валидатор удостоверяется, что с функцией все в порядке, он просто завершается. Если же он обнаруживает ошибку, то должен сообщить о ней через обычный механизм регистрации ошибок ereport(). Выданная ошибка приведет к откату транзакции и тем самым предотвратит фиксацию определения некорректной функции.

Функции-валидаторы обычно должны учитывать параметр check_function_bodies: если он выключен, то все затратные или контекстно-зависимые проверки следует пропустить. Если язык предусматривает выполнение кода во время компиляции, валидатор должен отменять проверки, которые могут вызвать такое выполнение. В частности, этот параметр выключает утилита qhb_dump, чтобы иметь возможность загружать функции на процедурных языках, не беспокоясь о побочных эффектах или зависимостях тел функций от других объектов базы данных. (Из-за этого требования обработчик вызова не должен предполагать, что валидатор полностью проверил функцию. Смысл наличия валидатора не в том, чтобы позволять обработчику опускать проверки, а в том, чтобы немедленно уведомить пользователя, если в команде CREATE FUNCTION имеются очевидные ошибки.) Хотя выбор, что именно проверять, по большей части остается на усмотрение функции-валидатора, обратите внимание, что основной код CREATE FUNCTION выполняет предложения SET, связанные с функцией, только когда check_function_bodies включен. Таким образом, проверки, на результаты которых могут повлиять параметры GUC, определенно должны пропускаться, когда check_function_bodies выключен, во избежание ложных ошибок при восстановлении базы из дампа.

Если для процедурного языка предоставляется обработчик встраиваемого кода, он должен объявляться как функция, принимающая один параметр типа internal. Результат обработчика встраиваемого кода игнорируется, поэтому обычно он объявляется как возвращающий тип void. Обработчик встраиваемого кода будет вызываться при выполнении оператора DO с указанным процедурным языком. Параметр, передающийся на самом деле, является указателем на структуру InlineCodeBlock, содержащую информацию о параметрах оператора DO, в частности, текст выполняемого анонимного блока кода. Обработчик встраиваемого кода должен выполнить этот код и завершиться.

Все подобные объявления функций, а также саму команду CREATE LANGUAGE рекомендуется упаковывать в расширение так, чтобы для установки языка достаточно было простой команды CREATE EXTENSION. Информацию о написании расширений см. в разделе Упаковка связанных объектов в расширение.

Процедурные языки, включенные в стандартный пакет, могут послужить хорошим примером при написании собственного обработчика языка. Некоторые полезные детали также можно узнать на справочной странице команды CREATE LANGUAGE.



Пример написания обработчика

/*
 * Обработчик процедурного языка PL/Sample
 */

#include "qhb.h"

#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/event_trigger.h"
#include "commands/trigger.h"
#include "executor/spi.h"
#include "funcapi.h"
#include "utils/builtins.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(plsample_call_handler);

static Datum plsample_func_handler(PG_FUNCTION_ARGS);
static HeapTuple plsample_trigger_handler(PG_FUNCTION_ARGS);

/*
 * Вызовы функции-обработчика, процедуры и триггера.
 */
Datum
plsample_call_handler(PG_FUNCTION_ARGS)
{
   Datum       retval = (Datum) 0;

   /*
    * Для многих языков потребуется очистка, которая происходит даже в случае
    * ошибки.  Это может произойти в блоке PG_FINALLY.  Если очистка не нужна,
    * эту конструкцию PG_TRY можно опустить.
    */
   PG_TRY();
   {
       /*
        * Определяем, будет ли обработчик вызываться как функция или как триггер,
        * и вызываем подходящий вложенный обработчик.
        */
       if (CALLED_AS_TRIGGER(fcinfo))
       {
           /*
            * Эта функция была вызвана как триггер, у которого в строку
            * (TriggerData *) fcinfo->context включена информация о контексте.
            */
           retval = PointerGetDatum(plsample_trigger_handler(fcinfo));
       }
       else if (CALLED_AS_EVENT_TRIGGER(fcinfo))
       {
           /*
            * Эта функция была вызвана как триггер события, у которого в строку
            * (EventTriggerData *) fcinfo->context включена информация о контексте.
            *
            * TODO: provide an example handler.
            */
       }
       else
       {
           /* Обычный обработчик функции */
           retval = plsample_func_handler(fcinfo);
       }
   }
   PG_FINALLY();
   {
   }
   PG_END_TRY();

   return retval;
}

/*
 * plsample_func_handler
 *
 * Функция, вызванная обработчиком вызовов для выполнения функции.
 */
static Datum
plsample_func_handler(PG_FUNCTION_ARGS)
{
   HeapTuple   pl_tuple;
   Datum       ret;
   char       *source;
   bool        isnull;
   FmgrInfo   *arg_out_func;
   Form_pg_type type_struct;
   HeapTuple   type_tuple;
   Form_pg_proc pl_struct;
   volatile MemoryContext proc_cxt = NULL;
   Oid        *argtypes;
   char      **argnames;
   char       *argmodes;
   char       *proname;
   Form_pg_type pg_type_entry;
   Oid         result_typioparam;
   Oid         prorettype;
   FmgrInfo    result_in_func;
   int         numargs;

   /* Выбираем запись функции в каталоге pg_proc. */
   pl_tuple = SearchSysCache1(PROCOID,
                              ObjectIdGetDatum(fcinfo->flinfo->fn_oid));
   if (!HeapTupleIsValid(pl_tuple))
       elog(ERROR, "cache lookup failed for function %u",
            fcinfo->flinfo->fn_oid);

   /*
    * Извлекаем и выводим исходный текст функции.  Это можно использовать как
    * основу для валидации и выполнения функции.
    */
   pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
   proname = pstrdup(NameStr(pl_struct->proname));
   ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
   if (isnull)
       elog(ERROR, "could not find source text of function \"%s\"",
            proname);
   source = DatumGetCString(DirectFunctionCall1(textout, ret));
   ereport(NOTICE,
           (errmsg("source text of function \"%s\": %s",
                   proname, source)));

   /*
    * Выделяем в памяти контекст, где будут содержаться все данные QHB для
    * процедуры.
    */
   proc_cxt = AllocSetContextCreate(TopMemoryContext,
                                    "PL/Sample function",
                                    ALLOCSET_SMALL_SIZES);

   arg_out_func = (FmgrInfo *) palloc0(fcinfo->nargs * sizeof(FmgrInfo));
   numargs = get_func_arg_info(pl_tuple, &argtypes, &argnames, &argmodes);

   /*
    * Выполняем итерацию всех аргументов функции, выводя все входные значения.
    */
   for (int i = 0; i < numargs; i++)
   {
       Oid         argtype = pl_struct->proargtypes.values[i];
       char       *value;

       type_tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(argtype));
       if (!HeapTupleIsValid(type_tuple))
           elog(ERROR, "cache lookup failed for type %u", argtype);

       type_struct = (Form_pg_type) GETSTRUCT(type_tuple);
       fmgr_info_cxt(type_struct->typoutput, &(arg_out_func[i]), proc_cxt);
       ReleaseSysCache(type_tuple);

       value = OutputFunctionCall(&arg_out_func[i], fcinfo->args[i].value);
       ereport(NOTICE,
               (errmsg("argument: %d; name: %s; value: %s",
                       i, argnames[i], value)));
   }

   /* Тип результата */
   prorettype = pl_struct->prorettype;
   ReleaseSysCache(pl_tuple);

   /*
    * Получение информацию, требуемой для входного преобразования возвращаемого
    * значения.
    *
    * Если результатом функции является VOID, лучше, чтобы возвращался NULL.
    * В любом случае, будем откровенны.  Это просто шаблон, так что мы мало что
    * можем тут поделать.  Функция не возвращает NULL, только если тип результата
    * текстовый, когда результатом является исходный текст функции.
    */
   if (prorettype != TEXTOID)
       PG_RETURN_NULL();

   type_tuple = SearchSysCache1(TYPEOID,
                                ObjectIdGetDatum(prorettype));
   if (!HeapTupleIsValid(type_tuple))
       elog(ERROR, "cache lookup failed for type %u", prorettype);
   pg_type_entry = (Form_pg_type) GETSTRUCT(type_tuple);
   result_typioparam = getTypeIOParam(type_tuple);

   fmgr_info_cxt(pg_type_entry->typinput, &result_in_func, proc_cxt);
   ReleaseSysCache(type_tuple);

   ret = InputFunctionCall(&result_in_func, source, result_typioparam, -1);
   PG_RETURN_DATUM(ret);
}

/*
 * plsample_trigger_handler
 *
 * Функция, вызываемая обработчиком вызовов для выполнения триггера.
 */
static HeapTuple
plsample_trigger_handler(PG_FUNCTION_ARGS)
{
   TriggerData *trigdata = (TriggerData *) fcinfo->context;
   char       *string;
   volatile HeapTuple rettup;
   HeapTuple   pl_tuple;
   Datum       ret;
   char       *source;
   bool        isnull;
   Form_pg_proc pl_struct;
   char       *proname;
   int         rc PG_USED_FOR_ASSERTS_ONLY;

   /* Убедимся, что это было вызвано из триггера. */
   if (!CALLED_AS_TRIGGER(fcinfo))
       elog(ERROR, "not called by trigger manager");

   /* Подключимся к менеджеру SPI */
   if (SPI_connect() != SPI_OK_CONNECT)
       elog(ERROR, "could not connect to SPI manager");

   rc = SPI_register_trigger_data(trigdata);
   Assert(rc >= 0);

   /* Выбираем запись функции в каталоге pg_proc. */
   pl_tuple = SearchSysCache1(PROCOID,
                              ObjectIdGetDatum(fcinfo->flinfo->fn_oid));
   if (!HeapTupleIsValid(pl_tuple))
       elog(ERROR, "cache lookup failed for function %u",
            fcinfo->flinfo->fn_oid);

   /*
    * Получение кода
    *
    * Извлекаем и выводим исходный текст функции.  Это можно использовать как
    * основу для валидации и выполнения функции.
    */
   pl_struct = (Form_pg_proc) GETSTRUCT(pl_tuple);
   proname = pstrdup(NameStr(pl_struct->proname));
   ret = SysCacheGetAttr(PROCOID, pl_tuple, Anum_pg_proc_prosrc, &isnull);
   if (isnull)
       elog(ERROR, "could not find source text of function \"%s\"",
            proname);
   source = DatumGetCString(DirectFunctionCall1(textout, ret));
   ereport(NOTICE,
           (errmsg("source text of function \"%s\": %s",
                   proname, source)));

   /*
    * Мы закончили с кортежем pg_proc, так что освобождаем его.  (Обратите внимание,
    * что теперь строки "proname" и "source" являются отдельными копиями.)
    */
   ReleaseSysCache(pl_tuple);

   /*
    * Расширение кода
    *
    * Здесь можно расширить исходный текст, к примеру, обернуть его как
    * тело функции на целевом языке, добавив перед списком параметров такие
    * имена, как TD_name, TD_relid, TD_table_name, TD_table_schema,
    * TD_event, TD_when, TD_level, TD_NEW, TD_OLD, и аргументы, используя
    * подходящие типы из целевого языка. Расширенный текст можно кэшировать
    * в долгоживущем контексте памяти или, если в целевом языке имеется
    * этап компиляции, это можно сделать сейчас, кэшируя результат компиляции.
    */

   /*
    * Выполнение кода
    *
    * Здесь функция (исходный текст (возможно, расширенный) или результат
    * компиляции, если в целевом языке имеется этот этап) должна быть
    * выполнена, после привязывания значений из структуры TriggerData
    * к соответствующим параметрам.
    *
    * В данном примере мы просто выводим большой объем информации посредством ereport.
    */

   PG_TRY();
   {
       ereport(NOTICE,
               (errmsg("trigger name: %s", trigdata->tg_trigger->tgname)));
       string = SPI_getrelname(trigdata->tg_relation);
       ereport(NOTICE, (errmsg("trigger relation: %s", string)));

       string = SPI_getnspname(trigdata->tg_relation);
       ereport(NOTICE, (errmsg("trigger relation schema: %s", string)));

       /* Пример обработки различных аспектов триггера. */

       if (TRIGGER_FIRED_BY_INSERT(trigdata->tg_event))
       {
           ereport(NOTICE, (errmsg("triggered by INSERT")));
           rettup = trigdata->tg_trigtuple;
       }
       else if (TRIGGER_FIRED_BY_DELETE(trigdata->tg_event))
       {
           ereport(NOTICE, (errmsg("triggered by DELETE")));
           rettup = trigdata->tg_trigtuple;
       }
       else if (TRIGGER_FIRED_BY_UPDATE(trigdata->tg_event))
       {
           ereport(NOTICE, (errmsg("triggered by UPDATE")));
           rettup = trigdata->tg_trigtuple;
       }
       else if (TRIGGER_FIRED_BY_TRUNCATE(trigdata->tg_event))
       {
           ereport(NOTICE, (errmsg("triggered by TRUNCATE")));
           rettup = trigdata->tg_trigtuple;
       }
       else
           elog(ERROR, "unrecognized event: %u", trigdata->tg_event);

       if (TRIGGER_FIRED_BEFORE(trigdata->tg_event))
           ereport(NOTICE, (errmsg("triggered BEFORE")));
       else if (TRIGGER_FIRED_AFTER(trigdata->tg_event))
           ereport(NOTICE, (errmsg("triggered AFTER")));
       else if (TRIGGER_FIRED_INSTEAD(trigdata->tg_event))
           ereport(NOTICE, (errmsg("triggered INSTEAD OF")));
       else
           elog(ERROR, "unrecognized when: %u", trigdata->tg_event);

       if (TRIGGER_FIRED_FOR_ROW(trigdata->tg_event))
           ereport(NOTICE, (errmsg("triggered per row")));
       else if (TRIGGER_FIRED_FOR_STATEMENT(trigdata->tg_event))
           ereport(NOTICE, (errmsg("triggered per statement")));
       else
           elog(ERROR, "unrecognized level: %u", trigdata->tg_event);

       /*
        * Выполняем итерацию всех аргументов триггера, выводя все входные
        * значения.
        */
       for (int i = 0; i < trigdata->tg_trigger->tgnargs; i++)
           ereport(NOTICE,
                   (errmsg("trigger arg[%i]: %s", i,
                           trigdata->tg_trigger->tgargs[i])));
   }
   PG_CATCH();
   {
       /* Здесь мог бы появиться код ошибки при очистке */
       PG_RE_THROW();
   }
   PG_END_TRY();

   if (SPI_finish() != SPI_OK_FINISH)
       elog(ERROR, "SPI_finish() failed");

   return rettup;
}