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" [PDF].
Но если вы хотите чего-то большего, то мануал быстро перестанет вас устраивать. У меня лично с ходу во всем разобраться не очень получилось - так что эта статья посвящена таким, как я :)
Важное предупреждение 1: функции написаны без использования best practices, это всего лишь работающие черновики (даже нет обработки нулевого result set-а, который может вернуть SPI_execute, например). Автор особо не парился над внешним видом своего кода, поэтому нельзя считать его готовым продакшн-вариантом.
Важное предупреждение 2: Статья имеет вид, далекий от финального (см. Важное предупреждение 1), следовательно статья будет меняться. Я только что получил лист замечаний отФедора Сигаева (за что ему большое спасибо!), который знает о сишных функциях для Постгреса всё (в отличие от меня), так что я попытаюсь учесть его комментарии. To be continued, одним словом.
Пишем 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(); /* * переключаем контекст памяти на контекст, сохраняемый между вызовами */ 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 (см. ниже про компиляцию с помощью Makefile), положить Постгресу в нужное место и просто создать эту функцию:
CREATE OR REPLACE FUNCTION iz_test(integer) RETURNS setof int4 AS '/usr/lib/pgsql/c-func_test.so', 'iz_test' LANGUAGE C STRICT;
Потом все будет выглядеть приблизительно вот так:
test_db=# select iz_test(10); iz_test --------- 1 2 3 4 5 6 7 8 9 10 (10 rows) Time: 0.300 ms
Простейшее использование 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);
По правильному замечанию Сергея Копосова, elog является устаревшим способом репорта об ошибках, правильнее использовать ereport, с помощью которого можно написать полностью эквивалентную конструкцию (подробнее читайте в документации):
ereport(NOTICE, (errmsg_internal("My name is %s, I'm %d years old", "Vanya", 21)));
Но если хочется делать все по-взрослому, то нужно использовать стандартный GNU debugger (gdb). Так как отлаживать мы будем процесс postmaster, нужно иметь привилегии пользователя, под которым он запущен. Итак, последовательность действий такова:
- Стартуем какого-либо клиента, например psql
- Из клиента выполняем SELECT pg_backend_pid(), узнавая тем самым pid процесса postmaster (это можно сделать и утилитой ps из командной строки)
- Загружаем из клиента нашу динамическую библиотеку: LOAD '/usr/lib/pgsql/c-func_test.so'
- В другой терминальной сессии под рутом, либо под хозяином postmaster-а (назовем его postgres, как это бывает обычно) стартуем дебаггер: gdb postgres server-process-id
- Ставим breakpoint в нашей функции: (gdb) break my-function
- Идем в назад в клиента и запускаем нашу функцию: SELECT my-function()
- Нажимаем continue в дебаггере: (gdb) c или читаем help, если мы в нем первый раз оказались: (gdb) help
Компиляция кода с помощью Makefile
Для начала вам подойдет простейший Makefile:
# Makefile for building C-functions shared objects for PostgreSQL # Author: IZ # Date: 2005/10/20 SERVER_INCLUDES += -I $(shell pg_config --includedir) SERVER_INCLUDES += -I $(shell pg_config --includedir-server) CFLAGS += -g $(SERVER_INCLUDES) .SUFFIXES: .so .c.so: $(CC) $(CFLAGS) -fpic -c $< $(CC) $(CFLAGS) -shared -o [email protected] $(basename $<).o @echo Built! clean: -rm -f *.o *.so *~ 2>/dev/null @echo Cleaned!
Затем просто запускаем make c-func_test.so и нам делается правильный со-шник в текущей директории. После этой процедуры уже можно делать LOAD в клиенте psql.
Важное примечание: уж не знаю по какой причине, но если я в качестве header-файлов беру те, что содержатся в pg_config --includedir-server, то есть из глобальных путей, при попытке загрузки so-шника в постгрес появляются сообщения вида undefined symbol: elog. Если при этом брать header-файлы из исходников постгреса, то есть указывать глобальные пути к ним, например, указывая в качестве ключа компилятору -I /home/iz/postgresql-8.0.2/src/include/, то определение elog-а найдется и все будет работать. Уж не знаю, почему при установке постгреса .h-файлы так неприятно обрезаются :(
Замечания
1. У вас не получится писать рекурсивные функции. Посмотрите на аргументы и возвращаемые значения, их типы и т.д. и догадайтесь сами, почему не получится.
2. Меня поразило отсутствие нормальной девелоперской документации к этой разработке. Я не считаю себе гуру в сишном программировании, скорее наоборот, но разве нельзя было как-нибудь по-нормальному все задокументировать?
Any feedback is welcome at iz at sai dot msu dot ru
Oct 2005
- Войдите чтобы оставить комментарии