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, нужно иметь привилегии пользователя, под которым он запущен. Итак, последовательность действий такова:

  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

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