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

от автора

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

Смысл использования языка С при разработке расширений для 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://habr.com/ru/post/497990/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *