Hlavní navigace

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

Pavel Stěhule

V předchozích dílech jsem navrhovali funkce, jejichž výsledek závisel pouze na jejich parametrech a které z celého systému PostgreSQL používaly zatím jen datové struktury. Rozhraní SPI (Server Programming Interface), které máme k dispozici, nám však dovoluje mnohem více.

Pokud si vzpomínáte na předchozí seriál Úvod do PL/pgSQL, pak si jistě vybavíte příklad agregační funkce html_list, která vracela seznam položek v sloupci formátovaný jako html seznam. Totožnou fonkci bude mít funkce html_list. Jejími parametry bude SQL dotaz, sloupec, jehož položky chceme uložit do seznamu, maximální počet zpracovávaných řádek a velikost bloku alokované paměti.

Pro náš příklad si vystačíme s pěti základními funkcemi: SPI_connect, SPI_finish, SPI_exec, SPI_fnumber a SPI_getvalue. S touto množinou funkcí dokážeme pouze číst výsledek dotazu, nicméně je to v tuto chvíli dostačující. Funkce SPI_connect inicializuje rozhraní a musí proto být volána jako první. Naproti tomu SPI_finish uvolňuje veškerou paměť alokovanou funkcí palloc po dobu běhu funkce a musí být proto volána jako poslední. Pokud naše funkce vrací ukazatel na paměť alokovanou v proceduře (u všech hodnot předávaných pdkazem), nesmí se paměť alokovat příkazem palloc. To z toho důvodu, že ještě než stihneme hodnotu předat volající funkci, SPI_finish stihne pamět s touto hodnotou uvolnit. Tento „neřešitelný“pro­blém řeší alternativní procedury pro práci s pamětí: SPI_palloc a SPI_replloc. Volání SPI_finish nemá vliv na paměť alokovanou těmito funkcemi. Pokud dojde k ukončení funkce voláním elog (ERROR, …), potom je SPI_finish voláno automaticky.

Pokud dojde k chybě, pak SPI funkce vrací obvykle záporné číslo. Po aktivaci rozhraní můžeme funkcí SPI_exec nechat provést SQL příkaz. Druhým parametrem funkce SPI_exec je maximální počet vrácených záznamů. Pokud je nula, pak se použijí všechny záznamy. Po bezchybném provedení dotazu můžeme používat globální proměnnou SPI_processed obsahující počet vrácených záznamů a globální ukazatel SPI_typtable.

Ukazatel SPI_tuptable je ukazatel na strukturu obsahující jednak pole s vrácenými řádkami, jednak odkaz na popis struktury vrácených dat. Pro získání hodnoty ve sloupečku potřebujeme vědět pořadí sloupce v dotazu. Pořadí vrací funkce SPI_fnumber, např.

sloupec = SPI_fnumber(SPI_tuptable->tupdesc, "jmeno");
if (sloupec <= 0)
  elog (ERROR, "SPI_ERROR_NOATTRIBUTE");

Přístup k n-tému sloupečku x-tého řádku zajišťuje funkce SPI_getvalue. Je nutné si uvědomovat, že všechny hodnoty vrácené SPI_getvalue jsou textové. Získání první hodnoty prvního řádku provedeme příkazem:

printf ("%s", SPI_getvalue(
            SPI_tuptable->vals[0],
            SPI_tuptable->tupdesc, 1));

Abych urychlil provádění funkce, nealokuji paměť při každém přidání řetězce, ale po blocích – stránkách, jejichž velikost si sami určíme.

text *sts_strcatex2 (char *str, text *txt,
                     long *zapsano, long *zbyva, long stranka)
{
  long len = strlen (str);
  if (len > stranka)
    elog (ERROR, "Retezec je vetsi nez velikost stranky");
  if (*zbyva < len)
    {
      long nsize = (((*zapsano + len) / stranka) + 1) * stranka;
      txt = (text *) SPI_repalloc (txt, nsize + VARHDRSZ);
      *zbyva = nsize - *zapsano;
    }
  memcpy ((char*)VARDATA (txt) + *zapsano, str, len);
  *zapsano += len; VARATT_SIZEP (txt) = *zapsano + VARHDRSZ;
  *zbyva -= len;
  return (txt);
}

Paměť alokuji voláním funkce SPI_repalloc a nikoliv repalloc z již zmíněného důvodu – pokud bych použil palloc, pak fce SPI_finish by tuto paměť uvolnila. Následné volání PG_RETURN_TEXT_P by vracelo neplatný pointer, což vede (jak jsem se sám přesvědčil zhruba v 80%) k pádu backendu.

Základ funkce html_list je následující:

SPI_connect ();

 if ((ret = SPI_exec (query, max)) < 0)
   elog (ERROR, "SPI_exec vrátilo %d", ret);

 sloupec = SPI_fnumber(SPI_tuptable->tupdesc, jm_sloupce);

 if (sloupec <= 0)
   elog (ERROR, "SPI_ERROR_NOATTRIBUTE");

 zapsano = 0; zbyva = stranka;
 txt = (text*) SPI_palloc (VARHDRSZ + zbyva);

 txt = sts_strcatex2 ("<ul>\n", txt, &zapsano, &zbyva, stranka);

 for (radek = 0; radek < SPI_processed; radek++)
   {
     txt = sts_strcatex2 ("<li>", txt, &zapsano, &zbyva, stranka);
     txt = sts_strcatex2 (SPI_getvalue (SPI_tuptable->vals[radek],
                    SPI_tuptable->tupdesc, sloupec),
              txt, &zapsano, &zbyva, stranka);
     txt = sts_strcatex2 ("</li>\n", txt, &zapsano,
                                &zbyva, stranka);
   }
 txt = sts_strcatex2 ("</ul>", txt, &zapsano, &zbyva, stranka);
 SPI_finish ();
 PG_RETURN_TEXT_P(txt);

K textu není co dodat. V cyklu čtu jednotlivé hodnoty sloupce (SPI_getvalue vrací vždy ukazatel na řetězec), které přidávám do struktury txt typu text.

Po překladu můžeme funkci zaregistrovat v PostgreSQL

CREATE OR REPLACE FUNCTION html_list (cstring, cstring, int, int)
  RETURNS text AS 'html_list.so','html_list' LANGUAGE 'C';

Tabulka, kterou jsem pro test použil, obsahuje 192 záznamů, výsledkem je text o délce mírně přesahující 6KB. Pro změření casu jsem napsal krátký skript v Pythonu. Jelikož jsem chtel použít profiler Pythonu, rozdělil jsem příkazy do samostatných funkcí.

#!/usr/local/bin/python
import psycopg, time

def plain(cur):
    cur.execute ("SELECT jmeno from jmena")

def html_list(cur):
    cur.execute ("SELECT html_list ('select jmeno from jmena',
             'jmeno',0,8000)")
    rows = cur.fetchall ()
    print rows[0][0]

def pyt(cur):
    cur.execute ("SELECT jmeno from jmena")
    rows = cur.fetchall ()
    print "<ul>"
    for r in rows:
        print "<li>%s</li>" % r[0]
    print "</ul>"


def main ():
    for i in range(100):
        con = psycopg.connect ("dbname=testdb011")
        cur = con.cursor ()
        plain(cur)
        html_list(cur)
        pyt(cur)

import profile
profile.run ('main()')

Výsledkem je tato tabulka

ncalls tottime percall cumtime percall filename:lineno(function)
    1   0.000   0.000   4.670   4.670 <string>:1(?)
    1   0.090   0.090   4.760   4.760 profile:0(main())
    0   0.000           0.000         profile:0(profiler)
  100   3.670   0.037   3.670   0.037 test.py:12(pyt)
    1   0.620   0.620   4.670   4.670 test.py:20(main)
  100   0.250   0.003   0.250   0.003 test.py:4(plain)
  100   0.130   0.001   0.130   0.001 test.py:7(html_list)

Z ní je zřejmé, že vytvoření seznamu funkcí html_list je oproti „normálnímu“ sestavení seznamu zhruba 28krát rychlejší. Důvodem jsou nejspíš pomalejší operace Pythonu s řetězci, ale i větší režie spojená s procházením pole v Pythonu.

Celý příklad naleznete v archivu.

Příští díl bude poslední a bude věnován tabulkovým funkcím PostgreSQL.

Zdroje

Našli jste v článku chybu?