Эту статью написал еще пару лет назад, и не знал куда ее можно было бы выложить, а потом и забыл.
Смысл использования языка С при разработке расширений для PostgreSQL по сравнению с интерпретируемыми (скриптовыми) языками можно свести к двум положениям: производительность и функциональность. Ну а по простому, код написанный на С будет работать намного быстрее, например, если функция вызывается миллион раз в запросе на каждую запись. А более конкретно, некоторые возможности PostgreSQL и вовсе нельзя сделать кроме как на С, например, в других языках не поддерживаются типы (особенно если возвращать значение из функции) ANYELEMENT, ANYARRAY и особенно важный VARIADIC.
Для примера напишем функцию, которая принимает два аргумента и складывает их. Этот пример рассмотрен в документации к PostgreSQL, но мы его чуть улучшим и соберем. Затем загрузим в PostgreSQL и напишем вызов из SQL функции.
Файл add_func.c можно использовать как шаблон для разработки более сложного функционала. Также, пройдемся по коду
Теперь нам нужно правильно скомпилировать и собрать это. На выходе это будет динамически загружаемая разделяемая библиотека (*.so). Для этого удобней будет сделать через Makefile. В документации описаны ключи и пути, которые необходимо прописать, но мы соберем используя PGXS. Это окружение для разработчиков расширений, это означает что в Вашей системе должны быть установлены все необходимые -dev и -devel пакеты для PostgreSQL.
Теперь напишем SQL функцию, которая будет вызываться из нашей раннее созданной библиотеки расширения.
Вот и всё! Теперь мы можем использовать эту функцию так,
Теперь немного автоматизируем установку. Это будет весьма полезно, когда Вам заранее неизвестно какая версия PostgreSQL используется, и по какому пути он установлен. Для этого создадим следующий файл,
и добавим в Makefile строку,
Внесем некоторые изменения в наш пример, и сделаем некоторые улучшения.
Помните в определении SQL функции add(int,int) мы использовали ключевое слово STRICT? Оно обозначает, что если хотя бы один из аргументов будет иметь значение NULL, то функция не отработает, и просто вернет NULL. Это похоже на поведение SQL операторов, например, если в операторе "+" хотя бы один аргумент NULL, то результат будет NULL.
Добавим в нашу функцию проверку аргументов, наподобие SQL агрегирующей функции sum(), которая игнорирует значения NULL и продолжает работу. Для этого нам нужно сделать,
И теперь проверим это,
А вот как это же самое можно добиться стандартными средствами PostgreSQL,
Как Вы уже заметили, для получения значения аргументов мы использовали макросы. Поэтому мы можем передать любое кол-во аргументов, а затем просто в цикле прочитать их значения,
У Вас сразу же может возникнуть вопрос, но ведь можно же использовать массив для передачи значения аргументов. И в действительности так и нужно делать, но к сожалению из-за наличия собственного менеджера памяти в PostgreSQL, это не столь уж тривиальная задача. Но попробуем ее решить. В документации PostgreSQL есть пример где передается массив символов text[], но это не совсем то, что нам нужно. Попробуем адаптировать для наших целей,
Как всегда, пройдемся по коду,
Теперь нам осталось научить PostgreSQL использовать это, объявив функцию, которая принимает аргумент int[],
И проверим,
Теперь подведем некоторые итоги, и основные рекомендации для создания расширений.
Авторы и создатели СУБД PostgreSQL уделяют особенно пристальное внимание работе с памятью, и предотвращению утечек. Это один из основных правил разработки СУБД и ее расширений. Основано это на собственном решении — контекст памяти.
Вся работа с памятью в PostgreSQL заключена в использовании нестандартных функций palloc() и pfree().
Всегда инициализируйте новые объявленные структуры. Например, вызывайте memset() после palloc().
Каждое ваше расширение должно подключать не менее двух файлов: postgres.h и fmgr.h.
PostgreSQL: Server Programming Interface.
PostgreSQL: User Defined Functions in C
PostgreSQL Extension Network
Смысл использования языка С при разработке расширений для 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
