В рамках статьи Учимся писать расширения на языке C для PostgreSQL мы познакомились со структурой расширений для постгреса, узнали, как писать для них тесты, и даже затронули вопрос обновления расширений и использования интерфейса SPI. Но заметка вышла из серии «с места в карьер», без глубокого погружения в детали. А между тем, погружаться есть во что. Хотелось бы заполнить кое-какие пробелы, и начать, пожалуй, следует с Datum и вызова сторонних функций.
Рассмотрим конкретный пример. Допустим, я хочу преобразовать строку в тип Timestamp, или наоборот. Вот как это можно сделать:
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
:
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-х битных системах.
Для закрепления материала рассмотрим еще один пример:
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 , 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: как добавить новую функцию и далее по ссылкам.