Расширения 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[];

Код расширения должен корректно обрабатывать все перечисленное.

Не будем оригинальными и напишем функцию, вычисляющую сумму элементов в одномерном целочисленном массиве:

CREATE FUNCTION experiment_sum(arr INT[]) RETURNS INT

Вот ее код:

Datum
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. Затем мы можем запросто посчитать сумму его элементов.

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

CREATE FUNCTION experiment_max(arr ANYARRAY) RETURNS ANYELEMENT

Реализация функции:

Datum
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 раз:

CREATE FUNCTION experiment_repeat(val ANYELEMENT, count INT)
    RETURNS ANYARRAY

Код функции:

Datum
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.

Метки: , , .


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