Расширения PostgreSQL: Datum и вызов функций

7 марта 2022

В рамках статьи Учимся писать расширения на языке C для PostgreSQL мы познакомились со структурой расширений для постгреса, узнали, как писать для них тесты, и даже затронули вопрос обновления расширений и использования интерфейса SPI. Но заметка вышла из серии «с места в карьер», без глубокого погружения в детали. А между тем, погружаться есть во что. Хотелось бы заполнить кое-какие пробелы, и начать, пожалуй, следует с Datum и вызова сторонних функций.

Рассмотрим конкретный пример. Допустим, я хочу преобразовать строку в тип Timestamp, или наоборот. Вот как это можно сделать:

Datum
timestamp_in_out_test(PG_FUNCTION_ARGS)
{
    const char* tstamp_str = "Jan 01 00:00:00 2010";
    Timestamp tstamp;

    tstamp = DatumGetTimestamp(DirectFunctionCall3(timestamp_in,
        CStringGetDatum(tstamp_str),
        ObjectIdGetDatum(InvalidOid),
        Int32GetDatum(-1)));

    tstamp_str = DatumGetCString(DirectFunctionCall1(timestamp_out,
        TimestampGetDatum(tstamp)));

    PG_RETURN_TEXT_P(CStringGetTextDatum(tstamp_str));
}

Здесь строка tstamp_str преобразуется в Timestamp. Затем Timestamp преобразуется обратно в char*. Эта строка возвращается в виде результата с SQL-типом TEXT.

Тип Datum и макросы DirectFunctionCall* к этому моменту нам уже знакомы, поэтому не будем на них задерживаться. Куда интереснее другое. Как я узнал, что для сериализации Timestamp в строку есть функция timestamp_out, а для десериализации — timestamp_in? И откуда я знаю, сколько у них аргументов?

В постгресе для любого типа есть текстовое представление. Это нужно для того, чтобы пользователь мог вводить значения разных типов в каком-нибудь psql, а также для отображения значений, прочитанных из таблиц. Функции, преобразующие строку к заданному типу и обратно называются <type>_in и <type>_out. Также есть функции для преобразования одного типа в другой. Они называются <type1>_<type2>. Например, для преобразования Date в Timestamp есть функция date_timestamp.

Узнать, что принимает функция и что она возвращает, можно в psql:

=# \df+ timestamp_out
List of functions
-[ RECORD 1 ]-------+----------------------------
Schema              | pg_catalog
Name                | timestamp_out
Result data type    | cstring
Argument data types | timestamp without time zone
Type                | func
Volatility          | stable
Parallel            | safe
Owner               | eax
Security            | invoker
Access privileges   |
Language            | internal
Source code         | timestamp_out
Description         | I/O

Если вы ищите функцию, но не знаете точно, как она называется, можно открыть файл src/include/catalog/pg_proc.dat в исходном коде PostgreSQL.

Важно! Обращайте внимание на volatility функций! Здесь функция является STABLE, то есть, ее поведение зависит от параметров сессии. Это может быть или не быть проблемой, смотря что за задачу вы решаете. Если ваша функция использует STABLE-функции, она тоже должна быть объявлена, как STABLE.

Функция timestamp_in принимает три аргумента. Хотя, казалось бы, мы просто хотим преобразовать строку в Timestamp. Зачем нужно два «лишних» аргумента? Оказывается, что второй аргумент (typelem) на самом деле игнорируется. Третий аргумент (typmod) позволяет производить не очень понятные мне манипуляции. Если вам интересно в этом разобраться, можете почитать полную реализацию в src/backend/utils/adt/timestamp.c. В приведенном примере указывается значение typemod = -1, что означает не производить никаких манипуляций.

Разные функции <type>_in принимают разное количество аргументов, так что будьте внимательны. Например, date_in принимает только cstring. Другой важный момент заключается в том, что cstring — это не то же самое, что text! Первый представляет собой буквально char*, тогда как второй — это объект, представляющий SQL-тип TEXT. Вы не можете передавать text в функцию, ожидающую cstring, или возвращать cstring там, где следует возвращать text. Также ни в коем случае нельзя путать макрос DatumGetTextP с DatumGetCString, и так далее. Если функция вернула cstring, а нам нужен text, можно воспользоваться макросом CStringGetTextDatum. Этот макрос и используется в приведенном примере. Для преобразования в обратную сторону есть TextDatumGetCString.

Важно! В зависимости от платформы, на которой собран постгрес, Datum может хранить 64-х битные числа либо по значению, либо по ссылке. Это означает, что сравнивать два Datum’а напрямую нельзя, даже если вы знаете, что в нем лежит Timestamp! Такой код будет прекрасно работать на x64, но развалится на 32-х битных системах.

Для закрепления материала рассмотрим еще один пример:

Datum
timestamptz_zone_test(PG_FUNCTION_ARGS)
{
    Timestamp result;
    TimestampTz timestamptz = TimestampTzGetDatum(PG_GETARG_DATUM(0));
    const char* tzname = "Europe/Moscow";

    // Timestamp and TimestampTz internally are the same.
    // The TZ-version of TIMESTAMP_NOT_FINITE() is not even defined!
    if(TIMESTAMP_NOT_FINITE(timestamptz)) {
        // for + or -infinity return + or -infinity
        result = (Timestamp)timestamptz;
        PG_RETURN_TIMESTAMP(result);
    }

    if(PG_NARGS() > 1) {
        tzname = TextDatumGetCString(PG_GETARG_DATUM(1));
    }

    // The code is equal to 'timestamptz AT TIME ZONE tzname'
    result = DatumGetTimestamp(DirectFunctionCall2(timestamptz_zone,
            CStringGetTextDatum(tzname),
            TimestampTzGetDatum(timestamptz)));

    PG_RETURN_TIMESTAMP(result);
}

Здесь мы имеем дело с перегруженной функцией:

timestamptz_zone_test(tstamp TIMESTAMPTZ) TIMESTAMP
timestamptz_zone_test(tstamp TIMESTAMPTZ, timezone TEXT) TIMESTAMP

Тонкости работы с часовыми поясами в PostgreSQL ранее были рассмотрены в отдельной статье. Главное, что нужно знать — тип TimestampTz представляет собой время в UTC, точно так же, как и Timestamp. Разница только в том, как эти типы показываются пользователю. TimestampTz отображается в часовом поясе сессии, который можно поменять командой SET TIME ZONE.

Приведенная функция приводит переданное время во время в заданном часовом поясе. Время передается первым аргументом, а часовой пояс вторым. Если часовой пояс не указан, используется значение по умолчанию Europe/Moscow.

Пример интересен тем, что фактически он делает timestamptz AT TIME ZONE tzname. Но как я узнал, что это эквивалентно timestamptz_zone(tzname, timestamptz)? Если поискать строку AT TIME ZONE по исходному коду PostgreSQL, можно быстро наткнуться на src/backend/parser/gram.y:

...
| a_expr AT TIME ZONE a_expr            %prec AT
    {
        $$ = (Node *) makeFuncCall(SystemFuncName("timezone"),
                                   list_make2($5, $1),
                                   COERCE_SQL_SYNTAX,
                                   @2);
    }
...

Не нужно быть экспертом во Flex и Bison, чтобы понять — здесь вызывается функция timezone. Это перегруженная функция и в зависимости от аргументов будет вызвана та или иная функция на языке C. Смотрим на \df+ и видим:

...
-[ RECORD 5 ]-------+---------------------------------------
Schema              | pg_catalog
Name                | timezone
Result data type    | timestamp without time zone
Argument data types | text, timestamp with time zone
Type                | func
Volatility          | immutable
Parallel            | safe
Owner               | eax
Security            | invoker
Access privileges   |
Language            | internal
Source code         | timestamptz_zone
Description         | adjust timestamp to new time zone
...

В аргументах text и timestamptz, возвращаемое значение — timestamp. То, что нужно. Этой версии соответствует сишная функция timestamptz_zone. Вот и ответ.

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

Дополнение: В продолжение темы см Расширения PostgreSQL: логирование и исключения, Расширения PostgreSQL: управление памятью, Внутренности PostgreSQL: как добавить новую функцию и далее по ссылкам.

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.