Смысл использования языка С при разработке расширений для 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
gudvinr
А как работать с типами, которые не перечислены в Table 37.1? Например, CIDR, UUID, JSONB, numeric и т.д.? Ещё есть домены, композитные типы.
select_artur
Смотря, что вы хотите сделать. Например, для работы с jsonb есть функции для парсинга, для вывода и др. в файле jsonb.h.