Расширения PostgreSQL: полиморфизм и type cache
28 мая 2025
Некоторое время назад мы научились писать пользовательские функции (UDF) для PostgreSQL на C — см раз, два, три и далее по ссылкам. Если требуется написать функцию, которая принимает условные TIMESTAMP
и INT
, как-то их обрабатывает, после чего возвращает, к примеру, TEXT
, то такая задача не вызывает проблем. Однако PostgreSQL поддерживает и полиморфные функции. То есть, можно объявить функцию, принимающую аргументы произвольного типа. Как прикажете быть в таком случае? Давайте разбираться.
Объявление полиморфной функции в PostgreSQL может выглядеть так:
В PostgreSQL нет генериков, как в языке Go, или шаблонов, как в C++. СУБД использует уникальный подход, который нигде больше мне не встречался. Он основан на использовании полиморфных типов. К полиморфным типам относятся ANYELEMENT
, ANYARRAY
и другие. Они являются частным случаем псевдо-типов. Псевдо-типы — это такие типы, которые не могут использоваться при объявлении таблиц, но могут служить принимаемыми и возвращаемыми типами при объявлении функций.
Каждый раз, когда мы вызываем полиморфную функцию, СУБД пытается вывести тип ее аргументов. Все аргументы и возвращаемые значения, объявленные как ANYELEMENT
, заменяются на этот тип. В результате из одного объявления функции мы получаем foo(INT,INT)
, foo(TEXT,TEXT)
, и т.д. Если же мы попытаемся изобразить вызов foo(INT,TEXT)
, то PostgreSQL обнаружит неоднозначность в выводе полиморфного типа и бросит ошибку. Помимо этого, допускается накладывать ограничения на типы. Так ANYENUM
требует, чтобы на его месте был перечисляемый тип. Само собой разумеется, полиморфные типы можно использовать вместе с обычными.
Приведенного краткого описания должно быть достаточно для понимания идеи в общих чертах. Если же вас интересуют все подробности работы полиморфных типов, то они описаны в официальной документации.
В качестве примера рассмотрим функцию, которая принимает два аргумента, и возвращает больший из них:
Datum
experiment_max(PG_FUNCTION_ARGS)
{
Datum a = PG_GETARG_DATUM(0);
Datum b = PG_GETARG_DATUM(1);
Oid elmtyp = get_fn_expr_argtype(fcinfo->flinfo, 0);
Oid collation = PG_GET_COLLATION();
TypeCacheEntry *typentry;
int cmp_result;
typentry = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
if (typentry == NULL || typentry->type_id != elmtyp)
{
/* elog(WARNING, "lookup_type_cache() called"); */
typentry = lookup_type_cache(elmtyp, TYPECACHE_CMP_PROC_FINFO);
fcinfo->flinfo->fn_extra = (void *) typentry;
}
cmp_result = DatumGetInt32(FunctionCall2Coll(
&typentry->cmp_proc_finfo, collation, a, b));
return cmp_result <= 0 ? b : a;
}
Каждая пользовательская функция на самом деле принимает аргумент fcinfo
с типом FunctionCallInfo
:
В нем содержится указатель flinfo
на структуру с типом FmgrInfo
. Структура содержит информацию, которую нужно загрузить из системного каталога перед вызовом данной функции. Если функция вызывается много раз, то информация переиспользуется. Помимо прочего, из FmgrInfo
мы можем получить Oid типа аргумента при помощи вызова get_fn_expr_argtype
. Мы знаем, что у нас два аргумента, и что их типы одинаковы, поэтому достаточно одного вызова.
Как сравнивать аргументы мы не знаем, но мы можем получить эту информацию из type cache. Можно догадаться по названию, что это кэш информации о типах. Детали реализации type cache в рамках сего поста мы рассматривать не будем. Заинтересованным читателям предлагается изучить typcache.c и typcache.h самостоятельно, в качестве упражнения.
При помощи lookup_type_cache
мы получаем указатель на TypeCacheEntry
. Последний содержит FmgrInfo
для функции сравнения соответствующих типов, так как мы указали флаг TYPECACHE_CMP_PROC_FINFO
. Наша функция сохраняет TypeCacheEntry
в собственной структуре FmgrInfo
. В ней предусмотрено поле fn_extra
специально на подобный случай.
Если вызывать функцию многократно, то она обратиться к кэшу только один раз:
WARNING: lookup_type_cache() called
experiment_max
----------------
2
2
3
Паттерн с вызовом lookup_type_cache
и сохранением результата в fn_extra
часто встречается как в расширениях, так и в самом PostgreSQL.
После похода в type cache у нас есть все необходимое для вызова функции сравнения. В качестве аргументов она принимает collation, первый аргумент и второй аргумент нашей функции…. Постойте, какой еще collation?
Collation — это такой механизм в PostgreSQL, определяющий правила сравнения текстовых типов. В зависимости от того, на каком языке вы говорите, эти правила могут отличаться. Например, в итальянском алфавите используются те же буквы, что и в английском, но их меньше. В сербском алфавите есть как кириллица, так и латинская буква J, а еще собственные буквы. Буква Ä есть как в немецком, так и в шведском алфавите. Однако в немецком она находится в начале алфавита, тогда как в шведском — в конце.
Пользователь обычно не указывает правила сравнения напрямую. Они либо выводятся из свойств базы данных (см вывод \l+
), либо из свойств колонок, для которых правила указаны явно.
Но также их можно указать для аргументов функции:
Использовать разный collation для аргументов не выйдет. Получите ошибку:
Пример вычисления максимума от двух аргументов заранее неизвестного типа может показаться не очень-то и захватывающим. Однако понимание его работы необходимо для работы с массивами, а также написания агрегатных функций.
Полную версию исходников к посту вы найдете на GitHub.
Метки: C/C++, PostgreSQL, СУБД.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.