Hlavní navigace

Rozšiřování PostgreSQL v C - Funkce

Pavel Stěhule 29. 11. 2002

Tento seriál je určen pro pokročilejší uživatele Linuxu a PostgreSQL. Rozšiřování PostgreSQL pomocí C funkcí není příliš komplikované (je srovnatelné s návrhem aplikací pro jiné toolkity), přesto je složitější než návrh funkce v PL/pgSQL nebo v jiném z podporovaných jazyků.

Prakticky nepřenositelný kód spolu s vyšší pracností je zřejmě důvodem skutečnosti, že se s knihovnami napsanými v C příliš nesetkáte. Na druhou stranu své funkce v C navrhneme mnohem rychlejší a úspornější. Kromě vlastních funkcí máme v PostgreSQL možnost navrhovat vlastní datové typy. Použitím vlastního datového typu se prakticky vzdáme přenositelnosti své aplikace na jiné RDBMS, na oplátku naše aplikace může být opět o něco efektivnější a čitelnější (bohužel pouze pro nás).

Jakákoliv funkce, kterou chceme použít v PostgreSQL, musí vycházet z následujícího schématu.

PG_FUNCTION_INFO_V1 (jméno_funkce);

Datum jméno_funkce (PG_FUNCTION_ARGS)
{
  BpChar *arg1 = PG_GETARG_BPCHAR_P (0);

  PG_RETURN_BOOL (false);
}

Jedná se o tzv. první verzi volající konvence. Jelikož se tzv. nultou variantu nedoporučuje používat, nebudu se o ní zmiňovat více, než že se jedná o klasickou definici funkce v C.

Parametry funkce můžeme předávat odkazem, nebo hodnotou. Makra pro předání hodnoty odkazem končí _P. Číslo označuje pořadí získaného parametru. Hodnotou můžeme předávat pouze typy fixní velikosti, jejichž velikost není větší než čtyři byty (resp. sizeof(Datum)). Datum nemá nic společného s časem, je to tzv. univerzální datový typ.

Pro úplnost uvádím rozvinutý tvar maker z příkladu.

extern Pg_finfo_record * pg_finfo_check_rc (void);
Pg_finfo_record * pg_finfo_check_rc (void)
{
  static Pg_finfo_record my_finfo = { 1 };
  return &my_finfo;
}

Datum jméno_funkce (FunctionCallInfo fcinfo)
{
  BpChar *arg1 = ((BpChar *) pg_detoast_datum ((struct varlena *)
     ((Pointer) ((fcinfo->arg[0]))));

  return ((Datum) (((bool) 0)) ? 1 : 0));
}

Pokud funkci při deklaraci v PostgreSQL neoznačíme jako isStrict, musíme každý parametr otestovat před použitím na hodnotu NULL (a případně vrátit hodnotu NULL).

  IF (PG_ARGISNULL (0)) PG_RETURN_NULL ();

Po přeložení a zkopírování knihovny do lib adresáře PostgreSQL (obvykle /usr/local/pgsql/li­b) musíme ještě funkci vytvořit v PostgreSQL.

CREATE OR REPLACE FUNCTION jméno_funkce (char) RETURNS bool
  AS 'knihovna.so', 'jméno_funkce' LANGUAGE 'C';

Návrh funkcí komplikuje fakt, že PostgreSQL nepoužívá (více-méně) klasický nulou ukončený řetězec, ale řetězec s pevnou velikostí, tj. první čtyři byty řetězce nesou velikost řetězce (včetně hlavičky). Naštěstí máme pro operace s řetězci připraveno několik maker (můžeme je použít pro typy BpChar, VarChar a text* – odpovídají PostgreSQL typům char, varchar, text). Makra jsou postavena nad strukturou varlena (obsahuje dvě pole va_header a va_content).

// získání velikosti a vlastního řetězce

BpChar *arg1 = PG_GETARG_BPCHAR_P (0);
long len = VARSIZE (arg1) - VARHDRSZ;
char *sf = VARDATA (arg1); // pozor nejedná se o sz

// vytvoření řetězce a jeho naplnění jedním znakem

BpChar *result = palloc (VARHDRSZ + 1);
VARATT_SIZEP (result) = VARHDRSZ + 1;
*(VARDATA (result)) = 'M';

K alokování paměti použijeme funkci palloc místo obvyklého malloc. Pamět alokovaná funkcí palloc je automaticky uvolněna po ukončení transakce.

Typ DateADT, který používám níže, se v PostgreSQL používá k uchování datumu. PostgreSQL používá pro uložení datumu celé dlouhé číslo se znaménkem, interpretovatelné jako počet dní od 1. 1. 2000. Jednoduše lze toto číslo (pro konkrétní datum) získat převodem do juliánského kalendáře.

// převod 10.2.1973 do DateADT

DateADT d = date2j (1973, 2, 10) - date2j (2000, 1, 1);

// získání roku, měsíce a dne z DateADT

int rok, mesic, den; DateADT d = -9821;
j2date (d + date2j (2000, 1, 1), &rok, &mesic, &den);

Funkce date2j, j2date naleznete v src/backend/u­tils/adt/date­time.c v zdrojových souborech PostgreSQL. Při převodu se nekontroluje formát datumu, tj bez chyby se převede i 32. 13. 2002.

Následující příklad obsahuje funkce pro kontrolu rodného čísla (test na nulové modulo 11), získání data narození z rodného čísla a získání pohlaví.

Začneme funkcí pro parsování rodného čísla. Je možné zadat rodné číslo buď s, nebo bez znaku lomítka oddělujícího datum narození od indexu. Funkce vrací nulu, nebo číslo chyby. Funkce toleruje za rodným číslem max. 90 mezer (abych mohl funkci použít i pro typ CHAR(n)).

char *chyba [] = {
  "Rodné číslo je příliš dlouhé",
  "Symbol / je na špatné pozici",
  "Rodné číslo obsahuje nepřípustný znak",
  "Rodné číslo má chybný formát",
  "Rodné číslo vytvořené před rokem 1954 může obsahovat pouze
    devět číslic",
  "Modulo 11 rodného čísla není nula",
  "Rodné číslo může obsahovat pouze devět nebo deset číslic"};

#define MAXRCLEN     100

int parsen_rodne_cislo (char *rc, int parts[3], long length)
{
  int i = 0, s2 = 0; char c;
  int64 s = 0;

  while (length--)
    {
      c = *rc++;
      if (c == ' ')
        break;
      if (i > 10)
        return 1;
      if (c == '/')
        if (i == 6)
      continue;
    else
      return 2;
      if (c < '0' || c > '9')
        return 3;
      s  = (s  * 10) + (c - '0');
      s2 = (s2 * 10) + (c - '0');
      i++;

      if (i == 2) {parts[0] = s2; s2 = 0;}
      if (i == 4) {parts[1] = s2; s2 = 0;}
      if (i == 6) {parts[2] = s2; s2 = 0;}
    }

  while (length-- > 0)
    {
      if (i > MAXRCLEN)
        return 1;
      if (*rc++ != ' ')
        return 4;
      i++;
    }
  if (i == 9)
    if (parts [0] > 53)
      return 5;
    else
       parts [0] += 1900;
  else
    if (i == 10)
      {
        if (parts [0] > 53)
          parts [0] += 1900;
        else
          parts [0] += 2000;
        if ((s % 11) != 0)
      return 6;
      }
    else
      return 7;

  parts[3] = s2;
  return 0;
}

U rodného čísla platí následující pravidla:

  • rodná čísla vydaná před rokem 1954 mají pouze třímístný index a nelze je testovat na modulo 11.

  • rodná čísla vydaná po roce 1954 včetně mají čtyřmístnou koncovku (index) a lze je testovat na modulo 11.

  • století, kdy bylo rodné číslo vydáno, se pozná podle počtu číslic v indexu. Pokud mám počáteční číslice např. 19 a trojmístný index, pak jde o rok 1919, pokud bych měl čtyřmístný index, jedná se o rok 2019.

Nyní se dostávám k samotným funkcím, které budou volatelné z PostgreSQL. Musí být proto nadeklarované dle v1 konvence.

PG_FUNCTION_INFO_V1 (check_rc);
PG_FUNCTION_INFO_V1 (birth_date);
PG_FUNCTION_INFO_V1 (sex);

Datum
check_rc (PG_FUNCTION_ARGS)
{
  int parts [] = {0, 0, 0, 0}, result;
  BpChar *rc = PG_GETARG_BPCHAR_P (0);
  result = parsen_rodne_cislo (VARDATA (rc), parts,
   VARSIZE (rc) - VARHDRSZ);

  if (result == 0)
    PG_RETURN_BOOL (true);
  else
    {
      elog (WARNING, chyba [result - 1]);
      PG_RETURN_BOOL (false);
    }
}

Datum
birth_date (PG_FUNCTION_ARGS)
{
  int parts [] = {0, 0, 0, 0}, result; DateADT d;
  BpChar *rc = PG_GETARG_BPCHAR_P (0);
  result = parsen_rodne_cislo (VARDATA (rc), parts,
   VARSIZE (rc) - VARHDRSZ);

  if (result != 0)
    elog (ERROR, chyba [result - 1]);

  if (parts[1] > 50)
    parts [1] -= 50;

  d = date2j (parts[0],parts[1], parts[2]) - date2j (2000, 1, 1);
  PG_RETURN_DATEADT (d);
}

Datum
sex (PG_FUNCTION_ARGS)
{
  int parts [] = {0, 0, 0, 0}, result; char s;
  BpChar *rc = PG_GETARG_BPCHAR_P (0);
  result = parsen_rodne_cislo (VARDATA (rc), parts,
   VARSIZE (rc) - VARHDRSZ);
  BpChar *res;

  if (result != 0)
    elog (ERROR, chyba [result - 1]);

  if (parts[1] > 50) s = 'F'; else s = 'M';

  res = palloc (VARHDRSZ + 1);
  VARATT_SIZEP (res) = VARHDRSZ + 1;
  *(VARDATA (res)) = s;

  PG_RETURN_BPCHAR_P (res);
}

Funkce elog slouží k zobrazení na uživatelskou konzolu. Při úrovni ERROR dojde k přerušení vykonávání funkce. Při úrovni WARNING pouze k vypsání hlášení. Používá se stejně jako v PL/pgSQL konstrukce RAISE.

SQL příkazy, které nám založí funkce do PostgreSQL, jsou následující (provést tyto příkazy může pouze uživatel postgres):

CREATE OR REPLACE FUNCTION check_rc (char) RETURNS bool
  AS 'rc.so', 'check_rc' LANGUAGE 'C' STRICT;

CREATE OR REPLACE FUNCTION birth_date (char) RETURNS date
  AS 'rc.so', 'birth_date' LANGUAGE 'C' STRICT;

CREATE FUNCTION sex (char) RETURNS char
  AS 'rc.so', 'sex' LANGUAGE 'C' STRICT;

Použil jsem jednoduchý Makefile (předpokládám, že zdrojové soubory k PostgreSQL jsou v /usr/src/postgresql-7.3b1/src/include).

PG_INCLUDE = '/usr/src/postgresql-7.3b1/src/include'
PG_LIBDIR = `pg_config --libdir`
PG_PACKAGE = `pg_config --pkglibdir`

all:
  gcc -ggdb -fpic -I$(PG_INCLUDE) rodne_cislo_fce.c -c -o rc.o
  gcc -ggdb -shared -o rc.so rc.o
  cp rc.so $(PG_PACKAGE)/rc.so

Pokud se vše podaří, můžete si vytvořené funkce vyzkoušet.

// rodné číslo je vymyšlené
SELECT check_rc('806115/0262');
SELECT birth_date('806115/0262');
SELECT sex('806115/0262');

Poznámka k ladění

Vývoj funkcí v C má i své příjemnější stránky. Jednou z nich je možnost krokování při ladění funkce (díky za nakopnutí v konferenci). K ladění můžete použít obyčejný gdb debugger nebo některého z jeho komfortnějších následovníků (např. ddd). Je třeba si uvědomit, že se funkce vykonává na serveru, který běží pod jiným účtem než klient. Ladit aplikaci můžeme pouze pod uživatelem, který je vlastníkem laděného procesu. V tomto případě se jedná o uživatele postgres. Celý postup by měl být zřejmý z následujících příkazů.

# spustím si nového klienta psql a nechám provést alespoň jednou
# testovanou funkci, aby se natáhla knihovna

[pavel@localhost]$ su postgres
[pavel@localhost]$ ps -ax | grep postgres
1940 ? S  0:00 postgres: pavel testdb011 [local] idle

[pavel@localhost]$ gdb
(gdb) attach 1940
(gdb) break check_rc
Breakpoint 1 at 0x404040fa: file rc.c line 50.
# v psql spustíme provádění funkce
(gdb) list 50
47
48 Datum
49 check_rc (PG_FUNCTION_ARGS)
50 {
51   int parts [3], result;
52   BpChar *rc = PG_GETARG_BPCHAR (0);
        ...

(gdb) step
51   int parts [3], result;
(gdb) step
52   BpChar *rc = PG_GETARG_BPCHAR (0);
(gdb) ...
(gdb) continue
(gdb) detach
Detaching from program /usr/local/pgsql/bin/postgres, process 1940
(gdb) quit
[postgresql@localhost]$

Použité zdroje

celý zdojový kód + sql kód + Makefile naleznete v archivu. Před překladem je třeba ručně upravit Makefile. Konkrétně hodnotu PG_INCLUDE na adresář, ve kterém je uložen soubor postgres.h.

Příští díl bude věnován návrhu vlastních datových typů.

Našli jste v článku chybu?

2. 12. 2002 11:22

Karel Zak (neregistrovaný)

Pro potreby vyvoje v C doporucuji si prekompilovat postgresql s parametrem --enable-cassert a --enable-debug. Hlavne to prvni zajisti, ze se budou volat podpurne kontrolni makra a funkce jako je Assert() a prubezna kontrola pameti na leaky (pochopitelne nesmite pouzivat malloc, ale jen palloc).

120na80.cz: Stoná vaše dítě často? Upravte mu jídelníček

Stoná vaše dítě často? Upravte mu jídelníček

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Root.cz: Vypadl Google a rozbilo se toho hodně

Vypadl Google a rozbilo se toho hodně

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Podnikatel.cz: Víme první výsledky doby odezvy #EET

Víme první výsledky doby odezvy #EET

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Vitalia.cz: Co pomáhá dítěti při zácpě?

Co pomáhá dítěti při zácpě?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Vitalia.cz: Proč vás každý zubař posílá na dentální hygienu

Proč vás každý zubař posílá na dentální hygienu

Vitalia.cz: Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Vitalia.cz: Mondelez stahuje rizikovou čokoládu Milka

Mondelez stahuje rizikovou čokoládu Milka

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

DigiZone.cz: Rádio Šlágr má licenci pro digi vysílání

Rádio Šlágr má licenci pro digi vysílání

DigiZone.cz: Flix TV má set-top box s HEVC

Flix TV má set-top box s HEVC

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

Vitalia.cz: Tesco: Chudá rodina si koupí levné polské kuře

Tesco: Chudá rodina si koupí levné polské kuře

Lupa.cz: Seznam mění vedení. Pavel Zima v čele končí

Seznam mění vedení. Pavel Zima v čele končí