PostgeSQL: Разработка расширений (функций) на языке С

в 12:36, , рубрики: C, postgesql, postgresql

Эту статью написал еще пару лет назад, и не знал куда ее можно было бы выложить, а потом и забыл.

Смысл использования языка С при разработке расширений для PostgreSQL по сравнению с интерпретируемыми (скриптовыми) языками можно свести к двум положениям: производительность и функциональность. Ну а по простому, код написанный на С будет работать намного быстрее, например, если функция вызывается миллион раз в запросе на каждую запись. А более конкретно, некоторые возможности PostgreSQL и вовсе нельзя сделать кроме как на С, например, в других языках не поддерживаются типы (особенно если возвращать значение из функции) ANYELEMENT, ANYARRAY и особенно важный VARIADIC.

Простая функция на языке С

Для примера напишем функцию, которая принимает два аргумента и складывает их. Этот пример рассмотрен в документации к PostgreSQL, но мы его чуть улучшим и соберем. Затем загрузим в PostgreSQL и напишем вызов из SQL функции.

#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(add_ab);
Datum add_ab(PG_FUNCTION_ARGS)
{
    int32 arg_a = PG_GETARG_INT32(0);
    int32 arg_b = PG_GETARG_INT32(1);
    PG_RETURN_INT32(arg_a + arg_b);
}

Файл add_func.c можно использовать как шаблон для разработки более сложного функционала. Также, пройдемся по коду

  • #include «postgresql.h»: этот заголовочный файл Вам придется подключать всегда, содержит различные базовые типы и функции.
  • #include «fmgr.h»: заголовочный файл содержит различные PG_* макросы.
  • PG_MODULE_MAGIC: макрос, определяющий что мы разрабатываем модуль для PostgreSQL выше версии 8.2.
  • PG_FUNC_INFO_V1: макрос, определяющий соглашения о вызовах функций внутри PostgreSQL. Если объявить функция без него, то будет соглашение о вызове Версии 0, иначе Версии 1.
  • Datum: тип возвращаемого значения. Вообще, это универсальный тип в PostgreSQL. Что-то наподобие VARIANT у Microsoft. По сути это просто «сырой» указатель на какие-то данные. Как их интерпретировать, решает пользователь данных.
  • add_ab(PG_FUNCTION_ARGS): параметры определяются макросом. Позволяет определить любое кол-во аргументов. Обязателен, даже если функция не принимает никаких аргументов.
  • int32 arg_a = PG_GETARG_INT32(0): макрос чтобы получить значение первого аргумента (начинается с нуля).
  • PG_RETURN_INT32(arg_a + arg_b): макрос позволяет сконструировать значение нужного типа и вернуть его из функции.

Теперь нам нужно правильно скомпилировать и собрать это. На выходе это будет динамически загружаемая разделяемая библиотека (*.so). Для этого удобней будет сделать через Makefile. В документации описаны ключи и пути, которые необходимо прописать, но мы соберем используя PGXS. Это окружение для разработчиков расширений, это означает что в Вашей системе должны быть установлены все необходимые -dev и -devel пакеты для PostgreSQL.

MODULES = add_func
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Создаем SQL функцию

Теперь напишем SQL функцию, которая будет вызываться из нашей раннее созданной библиотеки расширения.

CREATE FUNCTION add(int, int)
  RETURNS int
AS '/usr/lib/postgresql/9.4/lib/add_func', 'add_ab'
LANGUAGE C STRICT;

Вот и всё! Теперь мы можем использовать эту функцию так,

SELECT add(1, 2);

Автоматизируем установку

Теперь немного автоматизируем установку. Это будет весьма полезно, когда Вам заранее неизвестно какая версия PostgreSQL используется, и по какому пути он установлен. Для этого создадим следующий файл,

CREATE FUNCTION add(int, int)
  RETURNS int
AS 'MODULE_PATHNAME', 'add_ab'
LANGUAGE C STRICT;

и добавим в Makefile строку,

MODULES = add_func
DATA_built = add_funcs.sql
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Немного улучшим

Внесем некоторые изменения в наш пример, и сделаем некоторые улучшения.

Проверка аргументов

Помните в определении SQL функции add(int,int) мы использовали ключевое слово STRICT? Оно обозначает, что если хотя бы один из аргументов будет иметь значение NULL, то функция не отработает, и просто вернет NULL. Это похоже на поведение SQL операторов, например, если в операторе "+" хотя бы один аргумент NULL, то результат будет NULL.

Добавим в нашу функцию проверку аргументов, наподобие SQL агрегирующей функции sum(), которая игнорирует значения NULL и продолжает работу. Для этого нам нужно сделать,

  • Добиться того, чтобы функция отработала даже если один из аргументов имеет значение NULL
  • Если оба аргумента имеют значения NULL, то вернуть NULL

PG_FUNCTION_INFO_V1(add_ab_null);
Datum add_ab_null(PG_FUNCTION_ARGS)
{
    int32 not_null = 0;
    int32 sum = 0;
    if (!PG_ARGISNULL(0))
    {
        sum += PG_GETARG_INT32(0);
        not_null = 1;
    }
    if (!PG_ARGISNULL(1))
    {
        sum += PG_GETARG_INT32(1);
        not_null = 1;
    }
    if (not_null)
    {
        PG_RETURN_INT32(sum);
    }
    PG_RETURN_NULL();
}

И теперь проверим это,

CREATE FUNCTION add(int, int)
 RETURNS int
AS '$libdir/add_func', 'add_ab_null'
LANGUAGE C;

SELECT add(NULL, NULL) AS must_be_null, add(NULL, 1) AS must_be_one;
-[ RECORD 1 ]+--
must_be_null |
must_be_one  | 1

А вот как это же самое можно добиться стандартными средствами PostgreSQL,

SELECT (CASE WHEN (a IS null) AND (b IS null)
(THEN null
ELSE coalesce(a, 0) + coalesce(b,0)
END)
FROM (SELECT 1::int AS a, null::int AS b) s;
-[ RECORD 1 ]
 case | 1

Любое кол-во аргументов в функции

Как Вы уже заметили, для получения значения аргументов мы использовали макросы. Поэтому мы можем передать любое кол-во аргументов, а затем просто в цикле прочитать их значения,

if (!PG_ARGISNULL(i))
{
    sum += PG_GETARG_INT32(i);
    not_null = 1;
}

У Вас сразу же может возникнуть вопрос, но ведь можно же использовать массив для передачи значения аргументов. И в действительности так и нужно делать, но к сожалению из-за наличия собственного менеджера памяти в PostgreSQL, это не столь уж тривиальная задача. Но попробуем ее решить. В документации PostgreSQL есть пример где передается массив символов text[], но это не совсем то, что нам нужно. Попробуем адаптировать для наших целей,

#include "utils/array.h"     // подключаем заголовочный файл для работы с массивами
#include "catalog/pg_type.h" // INT4OID
PG_MODULE_MAGIC;
Datum add_int32_array(PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(add_int32_array);
Datum add_int32_array(PG_FUNCTION_ARGS)
{
    // подготовим массив для входных параметров.
    // В данном случае мы используем тип данных для обобщенного массива, который позволяет хранить не только целые int.
    ArrayType *input_array;
    int32 sum = 0;
    bool not_null = false;

    Datum *datums;
    bool *nulls;
    int count;
    int i;
    input_array = PG_GETARG_ARRAYTYPE_P(0); // инициализируем массив значением первого аргумента. Суффикс *_P в конце
                                            // макроса говорит о том что будет возвращен указатель, а не INT32

    // Проверим что мы действительно работаем с массивом целых чисел INT32 (INT4)
    Assert(ARR_ELEMTYPE(input_array) == INT4OID);

    // Удостоверимся что массив целых чисел одномерный
    if (ARR_NDIM(input_array) > 1)
        ereport(ERROR, (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR), errmsg("1-dimensional array needed")));

    deconstruct_array(input_array, // одномерный массив
                      INT4OID,     // целых чисел
                      4,           // размер целого в байтах
                      true,        // int4 передается по значению
                      'i',         // выравнивание 'i'
                      &datums, &nulls, &count); // здесь результат 

    for(i = 0; i < count; i++)
    {
        // проверяем и пропускаем нулевые элементы
        if (nulls[i])
            continue;

        // суммируем, помня что там были не нулевые элементы
        sum += DatumGetInt32(datums[i]);
        not_null = true;
    }
    if (not_null)
        PG_RETURN_INT32(sum);

    PG_RETURN_NULL();
}

Как всегда, пройдемся по коду,

  • Нет специального типа для целочисленных массивов, поэтому мы используем обобщенный тип ArrayType, который сгодится для любого типа массивов
  • Для инициализации массива первым аргументом, мы использовали специальный макрос PG_GETARG_ARRAYTYPE_P
  • Также есть проверка, действительно ли массив одномерный ARR_NDIM
  • Тип OID для int4 (= 23) определен как INT4OID. Чтобы просмотреть определения для других типов, можно воспользоваться SQL,
    select oid, typlen, typbyval, typalign from pg_type
    where typname = 'int4';
    -[ RECORD 1 ]
    oid | 23
    typlen | 4
    typbyval | t
    typalign | i
    

Теперь нам осталось научить PostgreSQL использовать это, объявив функцию, которая принимает аргумент int[],

CREATE OR REPLACE FUNCTION add_arr(int[]) RETURNS int
AS '$libdir/add_func', 'add_int32_array'
LANGUAGE C STRICT;

И проверим,

SELECT add_arr('{1,2,3,4,5,6,7,8,9}');
-[ RECORD 1 ]
add_arr | 45
SELECT add_arr(ARRAY[1,2,NULL]);
-[ RECORD 1 ]
add_arr | 3
SELECT add_arr(ARRAY[NULL::int]);
-[ RECORD 1 ]
add_arr |

Рекомендации

Теперь подведем некоторые итоги, и основные рекомендации для создания расширений.

Работа с памятью

Авторы и создатели СУБД PostgreSQL уделяют особенно пристальное внимание работе с памятью, и предотвращению утечек. Это один из основных правил разработки СУБД и ее расширений. Основано это на собственном решении — контекст памяти.

Используем palloc() и pfree()

Вся работа с памятью в PostgreSQL заключена в использовании нестандартных функций palloc() и pfree().

Инициализация структур

Всегда инициализируйте новые объявленные структуры. Например, вызывайте memset() после palloc().

Подключаемые файлы

Каждое ваше расширение должно подключать не менее двух файлов: postgres.h и fmgr.h.

Полезные ссылки

PostgreSQL: Server Programming Interface.

PostgreSQL: User Defined Functions in C

PostgreSQL Extension Network

Автор: Серебряков Алексей

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js