C-Language functions for PostgreSQL / C-функции для PostgreSQL
* Пишем Set Returning Function
* Простейшее использование Server Processing Interface
* Отладка (debug) кода
* Компиляция кода с помощью Makefile
* Замечания
Введение
Данная статья содержит материалы, посвященные написанию на C функций
для PostgreSQL - оказывается, делать это довольно легко, ну а бонусов
вы получаете существенно больше, чем если бы вы писали всю ту же
логику на процедурных языках. Скомпиленные в shared object функции
затем можно будет загрузить в PostgreSQL и использовать по своему
усмотрению как SQL команды, например.
Простейшие функции можно посмотреть и в мануале (заходите сюда
только после изучения этого документа). Да, еще обязательно посмотрите
на туториал с OSCON-2004 под названием "Power PostgreSQL:
Extending Database with C".
Но если вы хотите чего-то большего, то мануал быстро перестанет вас
устраивать. У меня лично с ходу во всем разобраться не очень
получилось - так что эта статья посвящена таким, как я :)
Важное предупреждение: функции написаны без использования best
practices, это всего лишь работающие черновики (даже нет обработки
нулевого result set-а, который может вернуть SPI_execute, например).
Автор особо не парился над внешним видом своего кода, поэтому нельзя
считать его готовым продакшн-вариантом.
Пишем Set Returning Function
Итак, попробуем написать простейшую Set Returning Function (функцию,
возвращающую сет значений, result set) - у нас она будет брать на вход
инт с длиной сета и на выходе будет возвращать сет значений от единицы
до заданного числа (вот так просто и тупо). Такая тупость выбрана не
случайно - в функции нет абсолютно никаких наворотов кроме каркаса для
демонстрации multicall-работы SRF. Напомню, что в стандартном случае,
SRF функция работает в режиме value-per-call, то есть она за каждый
вызов возвращает только одно значение и вызывается столько раз,
сколько рядов у нас в result set-е. Между вызовами информация хранится
в специальной структуре - контексте функции.
#include "postgres.h" // main include file (include always)
#include "fmgr.h" // "Function Manager" for V1 style
#include "funcapi.h" // to return set of rows
/* Version 1 Calling Conventions - так нужно писать все функции теперь */
PG_FUNCTION_INFO_V1(iz_test);
Datum
iz_test(PG_FUNCTION_ARGS)
{
/* Тот самый контекст функции */
FuncCallContext *funcctx;
/* Тоже нужно для multicall persistence */
MemoryContext oldcontext;
/* заходим сюда только в первом вызове функции */
if (SRF_IS_FIRSTCALL()) {
/*
* инициализация структуры-контекста фунции для
* хранения информации между вызовами
*/
funcctx = SRF_FIRSTCALL_INIT();
/*
* говорим Постгресу, что у нас тут multicall-функция
* и все серьезно
*/
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/*
* говорим, что функцию нужно дергать столько раз, сколько указано
* в ее первом аргументе
*/
funcctx->max_calls = PG_GETARG_INT32(0);
MemoryContextSwitchTo(oldcontext);
}
/* код, который исполняется при каждом вызове функции */
funcctx = SRF_PERCALL_SETUP(); // контекст функции освежили
if (funcctx->call_cntr < funcctx->max_calls) {
/*
* Это, собственно, возвращение каждого item-а
* Обратите внимание, что SRF_RETURN_NEXT в качестве аргументов
* принимает контекст функции для его обновления (хотя бы даже
* счетчик передвинуть) и собственно то, что нужно вернуть, только
* в виде Datum-а, который мы тут и делаем из инта
*/
SRF_RETURN_NEXT(funcctx, Int32GetDatum(funcctx->call_cntr));
} else {
// так нужно все заканчивать
SRF_RETURN_DONE(funcctx);
}
}
Дальше вы должны сами скомпилить это дело в .so, положить Постгресу в
нужное место и просто создать эту функцию:
CREATE OR REPLACE FUNCTION
iz_test(integer) RETURNS setof int4 AS
'/usr/lib/pgsql/c-func_test.so', 'iz_test'
LANGUAGE C
STRICT;
Простейшее использование Server Processing Interface
Теперь попробуем использовать Server Processing Interface (SPI) для
того, чтобы мы могли выполнять SQL-запросы. Каркас multicall функции
трогать не будем, просто добавим туда работу со SPI_* функциями.
Известные по первому примеру места я уже не комментирую.
#include "postgres.h" // main include file (include always)
#include "fmgr.h" // "Function Manager" for V1 style
#include "executor/spi.h" // Server Processing Interface
#include "funcapi.h" // to return set of rows and cope with tuples
#include <string.h>
#include <stdio.h>
#include <stdlib.h> // мы будем использовать atoi()
PG_FUNCTION_INFO_V1(get_level1_c);
Datum
get_level1_c(PG_FUNCTION_ARGS)
{
int32 pid = PG_GETARG_INT32(0);
int spi_ret;
char sql[100]; // не будем особо париться, пишем чтобы работало
char *tupval;
FuncCallContext *funcctx;
MemoryContext oldcontext;
Datum result;
if (SRF_IS_FIRSTCALL()) {
funcctx = SRF_FIRSTCALL_INIT();
oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
/* Готовимся выполнять запрос */
SPI_connect(); // функция коннекта
// непосредственно сама строка запроса
snprintf(sql, sizeof(sql), "SELECT edge_pid2 FROM edge WHERE edge_pid1 = %d AND edge_pid2 <> %d", pid, pid);
/*
* выполняем запрос. 0 в качестве третьего аргумента означает,
* что нужно обработать все туплы
*/
spi_ret = SPI_execute(sql, true, 0);
/*
* наша функция будет вызвана столько раз, сколько туплов в
* нашем результате
*/
funcctx->max_calls = SPI_processed;
MemoryContextSwitchTo(oldcontext);
}
funcctx = SRF_PERCALL_SETUP();
if (funcctx->call_cntr < funcctx->max_calls) {
/*
* Получаем строковое значение из текущего тупла
* Обратите внимание, хотя мы выбирали в запросе всего одну колонку,
* ее индекс равен 1, а не 0! Я лично потратил пару часов, чтобы это
* понять, не повторяйте моих ошибок
*/
tupval = SPI_getvalue(SPI_tuptable->vals[funcctx->call_cntr], SPI_tuptable->tupdesc, 1);
// дальше тривиально - просто делаем инт из строки и в датум его
result = Int32GetDatum(atoi(tupval));
SRF_RETURN_NEXT(funcctx, result);
} else {
SPI_finish();
SRF_RETURN_DONE(funcctx);
}
}
Отладка (debug) кода
Отлаживать собственный код можно, пользуясь, например, макросом elog и
выводя в виде NOTICE-ов содержание каких-либо переменных. Например:
elog(NOTICE, "My name is %s, I'm %d years old", "Vanya", 21);
Но если хочется делать все по-взрослому, то нужно использовать
стандартный GNU debugger (gdb). Так как отлаживать мы будем процесс
postmaster, нужно иметь привилегии пользователя, под которым он
запущен. Итак, последовательность действий такова:
1. Стартуем какого-либо клиента, например psql
2. Из клиента выполняем SELECT pg_backend_pid(), узнавая тем самым
pid процесса postmaster (это можно сделать и утилитой ps из
командной строки)
3. Загружаем из клиента нашу динамическую библиотеку: LOAD
'/usr/lib/pgsql/c-func_test.so'
4. В другой терминальной сессии под рутом, либо под хозяином
postmaster-а (назовем его postgres, как это бывает обычно)
стартуем дебаггер: gdb postgres server-process-id
5. Ставим breakpoint в нашей функции: (gdb) break my-function
6. Идем в назад в клиента и запускаем нашу функцию: SELECT
my-function()
7. Нажимаем continue в дебаггере: (gdb) c или читаем help, если мы в
нем первый раз оказались: (gdb) help
Компиляция кода с помощью Makefile
Для начала вам подойдет простейший Makefile:
# Makefile for building C-functions shared objects for PostgreSQL
# Author: IZ
# Date: 2005/10/20
Затем просто запускаем make c-func_test.so и нам делается правильный
со-шник в текущей директории. После этой процедуры уже можно делать
LOAD в клиенте psql.
Важное примечание: уж не знаю по какой причине, но если я в качестве
header-файлов беру те, что содержатся в pg_config --includedir-server,
то есть из глобальных путей, при попытке загрузки so-шника в постгрес
появляются сообщения вида undefined symbol: elog. Если при этом брать
header-файлы из исходников постгреса, то есть указывать глобальные
пути к ним, например, указывая в качестве ключа компилятору -I
/home/ns/postgresql-8.0.2/src/include/, то определение elog-а найдется
и все будет работать. Уж не знаю, почему при установке постгреса
.h-файлы так неприятно обрезаются :(
Замечания
1. У вас не получится писать рекурсивные функции. Посмотрите на
аргументы и возвращаемые значения, их типы и т.д. и догадайтесь сами,
почему не получится.
2. Меня поразило отсутствие нормальной девелоперской документации к
этой разработке. Я не считаю себе гуру в сишном программировании,
скорее наоборот, но разве нельзя было как-нибудь по-нормальному все
задокументировать?