Расширения PostgreSQL: работа с массивами
9 июля 2025
На данный момент мы уже знакомы с написанием пользовательских функций для PostgreSQL, в том числе и полиморфных функций. Однако в PostgreSQL есть тип, заслуживающий особого внимания — это массивы. Давайте же разберемся, как работать с массивами в расширениях на языке C.
Основные свойства массивов в PostgreSQL следующие:
- Массивы содержат в себе данные одного типа;
- Массивы могут содержать значения NULL;
- По умолчанию нумерация элементов начинается с единицы. Ее можно переопределить для заданного массива;
- Поддерживаются многомерные массивы. Все под-массивы должны быть одинаковой длины;
Вот несколько примеров:
SELECT '{1,2,3}' :: INT[];
-- может содержать NULL'ы
SELECT '{1,NULL,3}' :: INT[];
-- нумерация начиная с нуля
SELECT '[0:2]={1,2,3}' :: INT[];
-- нумерация начиная с -99
SELECT '[-99:-97]={1,2,3}' :: INT[];
-- многомерный массив
SELECT '{{1,2,3},{4,5,6}}' :: INT[];
-- многомерный массив с нумерацией начиная с нуля
SELECT '[0:1][0:2]={{1,2,3},{4,5,6}}' :: INT[];
Код расширения должен корректно обрабатывать все перечисленное.
Не будем оригинальными и напишем функцию, вычисляющую сумму элементов в одномерном целочисленном массиве:
Вот ее код:
experiment_sum(PG_FUNCTION_ARGS)
{
ArrayType *array = PG_GETARG_ARRAYTYPE_P(0);
int ndim = ARR_NDIM(array);
Oid elmtyp = ARR_ELEMTYPE(array);
TypeCacheEntry *typentry;
Datum *elmvalues;
bool *elmnulls;
int i, sum, elmcount;
if (ndim > 1)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("multi-dim arrays are not supported")));
/* Should never happen */
if(elmtyp != INT4OID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("only integer arrays are supported")));
/* Empty array? */
if (ndim < 1)
PG_RETURN_INT32(0);
typentry = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
if (typentry == NULL)
{
typentry = lookup_type_cache(elmtyp, 0);
fcinfo->flinfo->fn_extra = (void *) typentry;
}
deconstruct_array(
array,
elmtyp,
typentry->typlen,
typentry->typbyval,
typentry->typalign,
&elmvalues,
&elmnulls,
&elmcount
);
sum = 0;
for (i = 0; i < elmcount; i++)
{
if (!elmnulls[i])
sum += DatumGetInt32(elmvalues[i]);
}
pfree(elmvalues);
pfree(elmnulls);
PG_RETURN_INT32(sum);
}
Работа с type cache на данном этапе нам уже знакома. В остальном же все довольно просто. С помощью макроса ARR_NDIM
мы определяем размерность массива. Макрос ARR_ELEMTYPE
позволяет узнать Oid типа элементов массива. Массив деконструируется при помощи deconstruct_array
. Затем мы можем запросто посчитать сумму его элементов.
Усложним немного задачу. Напишем функцию, которая находит максимальный элемент в одномерном массиве из элементов произвольного типа:
Реализация функции:
experiment_max(PG_FUNCTION_ARGS)
{
Oid collation = PG_GET_COLLATION();
ArrayType *array = PG_GETARG_ARRAYTYPE_P(0);
int ndim = ARR_NDIM(array);
Oid elmtyp = ARR_ELEMTYPE(array);
TypeCacheEntry *typentry;
Datum *elmvalues;
bool *elmnulls;
int i, elmcount;
int32 cmp_result;
bool resnull = true;
Datum result = 0;
if (ndim > 1)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("multi-dim arrays are not supported")));
/* Empty array? */
if (ndim < 1)
PG_RETURN_NULL();
typentry = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
if (typentry == NULL || typentry->type_id != elmtyp)
{
typentry = lookup_type_cache(elmtyp, TYPECACHE_CMP_PROC_FINFO);
fcinfo->flinfo->fn_extra = (void *) typentry;
}
deconstruct_array(
array,
elmtyp,
typentry->typlen,
typentry->typbyval,
typentry->typalign,
&elmvalues,
&elmnulls,
&elmcount
);
for (i = 0; i < elmcount; i++)
{
if (elmnulls[i])
continue;
if(resnull) {
result = elmvalues[i];
resnull = false;
continue;
}
cmp_result = DatumGetInt32(FunctionCall2Coll(
&typentry->cmp_proc_finfo,
collation, result, elmvalues[i]));
if(cmp_result < 0)
result = elmvalues[i];
}
pfree(elmvalues);
pfree(elmnulls)
if(resnull)
PG_RETURN_NULL();
return result;
}
Качественно эта функция не намного сложнее предыдущей. Основное отличие заключается в том, что здесь мы используем функцию сравнения из type cache, что делает experiment_max
полиморфной. Это мы уже проходили.
С приемом массива в качестве аргумента все относительно ясно. Но что, если требуется не принять, а вернуть массив? Для примера рассмотрим функцию, возвращающую массив, где переданный аргумент повторяется count
раз:
RETURNS ANYARRAY
Код функции:
experiment_repeat(PG_FUNCTION_ARGS)
{
Datum value = PG_GETARG_DATUM(0);
int32 count = PG_GETARG_INT32(1);
Oid elmtyp = get_fn_expr_argtype(fcinfo->flinfo, 0);
TypeCacheEntry *typentry;
ArrayType* result;
Datum *elems;
int dims[1];
int lbs[1];
int i;
if (count < 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("count must not be negative")));
if (count == 0) {
result = construct_empty_array(elmtyp);
PG_RETURN_ARRAYTYPE_P(result);
}
typentry = (TypeCacheEntry *) fcinfo->flinfo->fn_extra;
if (typentry == NULL || typentry->type_id != elmtyp)
{
typentry = lookup_type_cache(elmtyp, TYPECACHE_CMP_PROC_FINFO);
fcinfo->flinfo->fn_extra = (void *) typentry;
}
elems = (Datum *) palloc(sizeof(Datum) * count);
for (i = 0; i < count; i++)
elems[i] = value;
dims[0] = count; /* construct a 1-dimensional array */
lbs[0] = 1; /* array lower bounds */
result = construct_md_array(elems, NULL, 1, dims, lbs, elmtyp,
typentry->typlen, typentry->typbyval, typentry->typalign);
pfree(elems);
PG_RETURN_ARRAYTYPE_P(result);
}
Основными функциями здесь являются construct_empty_array
, если мы хотим вернуть пустой массив, и construct_md_array
, если хотим вернуть не пустой. В остальном же данный пример ничем не сложнее предыдущих.
Если вам нужны более интересные примеры, рекомендую обратить внимание на реализацию array_sort, array_reverse, а также array_sample и array_shuffle из кода ядра PostgreSQL. Все доступные функции и операторы, имеющие дело с массивами, перечислены в документации. Их код находится в array_userfuncs.c. Все функции и макросы для работы с массивами, которые можно вызывать из расширений, вы найдете в arrayfuncs.c и array.h.
Что же до полной версии исходников к посту, то она лежит на GitHub.
Метки: C/C++, PostgreSQL, СУБД.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.