C a PostgreSQL - interní mechanismy
Autor: Pavel Stěhule, 2011
Před devíti lety jsem pro Root napsal seriál o psaní doplňků pro PostgreSQL. Tehdy jsem zhruba věděl, jak co funguje, a dokázal jsem napsat několik jednodušších funkcí. Moje znalosti byly, když to tak vezmu, docela povrchní. Teď mám o něco více zkušeností, takže bych rád předchozí seriál doplnil o popis několika základních a zásadních technik, které se používají jak při vývoji samotného PostgreSQL, tak při vývoji doplňků pro PostgreSQL.
Jazyk C a PostgreSQL
Zdrojové kódy PostgreSQL jsou z 99% v Cčku. Proč jazyk C? Odpověď je poměrně jednoduchá - v době, která byla pro PostgreSQL přelomová nebyl k dispozici vhodný překladač C++ a stejně tak chyběli programátoři, kteří by perfektně ovládali C++. Z důvodu přenositelnosti se vývojáři drží letitého standardu ANSI C. Nyní po 16-letém vývoji by byl přepis do jiného programovacího jazyka kontraproduktivní. Zdravé jádro vývojářů má Cčko perfektně zažité, poměrně bezbolestně udržuje multiplatformní kód a vývoj funguje bez větších excesů - takže není důvod ke změně. Je pravděpodobné, že časem dojde k razantní změně - a ke kompletní revizi kódu, ale ve výsledku to už nebude starý PostgreSQL (viz Firebird - přepis do C++ z Interbase nebo Drizzle - přepis z C++ do C++ z MySQL). Osobně si nemyslím, že by se tak stalo v příštích několika letech. PostgreSQL se nemůže dostat do stejné situace jako MySQL nebo Interbase, která si vynutila fork (díky BSD licenci).
V PostgreSQL 9.0 došlo k několika úpravám zdrojového kódu, tak aby bylo možné vyvíjet externí moduly i v C++ (modifikovaly se hlavičkové soubory, upravovaly se identifikátory, které kolidovaly s klíčovými slovy C++). Jazyk C nenabízí příliš komfortu. To je fakt, s kterým se musíme smířit. Je ale podporován všude, a přeložený kód je ďábelsky rychlý. Nedostatek komfortu je do jisté míry vyvážen poměrně bohatým repertoárem maker a knihovních funkcí, které si vývojáři pro sebe během těch 16 let vytvořili.
Ze začátku to může být docela nezvyk, jelikož se v PostgreSQL používá vlastní typový systém a i vlastní způsob předávání parametrů. Můžete mi ovšem věřit, že to má svojí logiku. Vychází to z požadavku na rozšiřitelnost PostgreSQL - v PostgreSQL můžete mít vlastní funkce, vlastní operátory, vlastní datové typy, vlastní implementaci indexů. Rozhodně nebudu zde tvrdit, že psaní zákaznických funkcí v Cčku je běžné a banální. To není. Ale jen díky rozšiřitelné architektuře PostgreSQL vznikají moduly jako je PostGIS nebo třeba Orafce.
Zdroje na internetu
Typový systém
Pro všechny datové typy dostupné z SQL se v PostgreSQL používá interní datový typ "Datum". To je 4/8 bajtový generický typ, který obsahuje buďto ukazatel nebo nějakou hodnotu, pokud se tato hodnota vejde do 4/8 bajtů (zaleží na platformně 32/64bit). Generický typ "Datum" neobsahuje žádná data, z kterých by bylo možné rozpoznat obsah - není zde prostor pro identifikátor typu. Programátor musí vždy vědět s jakým typem pracuje (Výjimka potvrzuje pravdlo - i uvnitř funkce lze dohledat údaje o typu každého parametru pomocí deskriptoru funkce. To ale na věci nic moc nemění. S dynamickými datovými typy (v PostgreSQL tzv. polymorfní typy) je spojena větší režie, a u obvykle i delší a složitější kód, tudíž je lépe se jim vyhnout (pokud je to možné - v C (v PLpgSQL je to zas trochu o něčem jiném))) - také proto je v PostgreSQL přísně staticky typový systém. Pokud zaregistruji funkci příkazem:
CREATE FUNCTION foo(a text) RETURNS text AS ...
tak mám zajištěno, že první parametr bude typu "text" a je mým úkolem vracet hodnotu typu "text". Pokud vrátím jiný typ, tak pravděpodobně dojde k pádu serveru. Vzhledem k tomu, že je typový systém pro zákaznická data v PostgreSQL nezávislý na typovém systému jazyka C, tak mi s typovou kontrolou céčkový překladač moc nepomůže. To proto, že popis SQL funkcí je uložen mimo dosah překladače (v systémové tabulce pg_proc). Z pohledu překladače jsou všechny SQL funkce téměř stejné - liší se pouze názvem.
Pokud hodnota typu Datum obsahuje ukazatel, tak v 99% obsahuje ukazatel na strukturu, kterou označujeme jako typ varlena nebo obsahuje ukazatel na Cčkový string. V několika málo případech může obsahovat ukazatel na cokoliv, ale to je natolik výjmečná situace, že s ní teď nebudu počítat. Typ varlena je předek všech SQL typů s variabilní délkou nebo typů, které jsou delší než je velikost typu datum. Typ varlena vypadá následovně - prvních n bajtů obsahuje informaci o délce (1/2/4 byte) a ve zbývajících je zakódován vlastní obsah. Pro pamětníky - je to hodně podobné implementaci stringu v Borland Pascalu. Pár volných bitů v hlavičce struktury varlena ještě obsahuje informace o tom, zda-li je vlastní obsah načtený v paměti nebo je jej nutné načíst z tabulky _toast - případně, zda-li je nutné obsah dekomprimpovat. Pravidla pro přístup k jednotlivým položkém této struktury jsou docela složitá, takže pro přístup k atributům struktury varlena se používají makra (zrovna tak pro nastavení atributů).
Pozn. Systém pro ukládání velkách dat TOAST. V PostgreSQL se používá poměrně unikátní systém pro ukládaní hodnot nad 8KB, který se označujeme zkratkou TOAST. PostgreSQL jako většina databází docela obtížně řeší situaci, kdy velikost záznamu přesáhne velikost datové stránky, což je 8KB. TOAST je trik, kterým se tato situace řeší. Pokud je záznam delší než 2KB, tak se vybere nejdelší položka záznamu. Zkomprimuje se, a rozseká na bloky s maximální velikostí 2KB. Tyto bloky se pak uloží do tzv. TOAST tabulky (pokud tabulka může obsahovat velká data, tak pro ní systém automaticky vytvoří pomocnou tabulku (TOAST tabulku), do které se tato velká data uloží). Adresa dat v TOAST tabulce nahradí původní data v záznamu - tím se záznam, který chceme uložit radikálně zkrátí. Popsaný proces je opakován tak dlouho, dokud nemá záznam požadovanou délku (pozn. spouštěcí mechanismus a pravidla pro TOAST se liší verze od verze - v novějších verzích se spouští vždy, jelikož je snaha o komprimaci ukládaných dat). Při čtení záznamu se přečtou všechny jeho položky. Některé položky ovšem nemusí obsahovat přímo data. Místo toho obsahují adresu (ukazatel) do TOAST tabulky. Pokud není potřeba zpracovávat data v TOAST tabulce, tak se TOAST tabulka nečte. K načtení dat v TOAST tabulkce dojde teprve ve chvíli, kdy jsou skutečně potřeba. Podobný mechanismus se v ORM označuje jako "Lazy Loading". Přístup k datům uloženým v TOAST tabulkách je natolik komplikovaný, že se téměř vždy používají makra nebo funkce, které proces načítání a dekomprimace obalují a skrývají před programátorem.
Předávání parametrů - volající konvence
Můžete psát zákaznické funkce v Cčku a používat Cčkové předávání parametrů. To je ovšem způsob, který se nedoporučuje. Vývojáři si napsali vlastní systém předávání parametrů a výsledku, který lépe odpovídá požadavkům SQL. V čem je problém? Problém je v NULL. V Cčku neumíme rozlišit mezi 0 a NULL u parametrů funkce předávaných hodnotou. Způsob předávání parametrů (označuje se jako V1) spočívá v tom, že si parametry předáváme pomocí komplexnější struktury místo přes zásobník. Ta struktura se jmenuje 'FunctionCallInfoData':
typedef struct { FmgrInfo *flinfo; // deskriptor funkce fmNodePtr context; // kontext volání funkce fmNodePtr resultinfo; // deskriptor výsledku (pokud je výsledkem složená hodnota) bool isnull; // výsledek je NULL? short nargs; // počet parametrů Datum arg[FUNC_MAX_ARGS]; // vlastní parametry funkce bool argnull[FUNC_MAX_ARGS]; // který parametr je nebo není NULL } FunctionCallInfoData.
jednoduchá funkce, která sečte dvě čísla by mohla vypadat následovně:
Datum sum_int(FunctionCallInfoData *fcinfo) { int a1, a2; /* pokud je některý z parametrů NULL, pak výsledek je NULL */ if (fcinfo->argnull[0] || fcinfo->argnull[1]) { fcinfo->isnull = true; return (Datum) 0; } a1 = DatumGetInt32(fcinfo->arg[0]); a2 = DatumGetInt32(fcinfo->arg[1]); fcinfo->isnull = false; return Int32GetDatum(a1 + a2); }
Funkci musíme zaregistrovat:
CREATE FUNCTION sum_int(int, int) RETURNS int AS 'libdir/nazev_knihovny' LANGUAGE C;
Tuto funkci můžeme zjednodušit s pomocí atributu STRICT:
CREATE FUNCTION sum_int(int, int) RETURNS int AS 'libdir/nazev_knihovny' LANGUAGE C STRICT;
Pokud je funkce označena jako STRICT, pak v případě, že je jeden z parametrů NULL, tak je jasné, že výsledkem bude NULL, a funkce se nezavolá. Díky tomu nemusíme uvnitř funkce zjišťovat, zda-li je nebo není některý z parametrů NULL (prakticky všechny vestavěné SQL funkce mají atribut STRICT). Již bylo řečeno, že se při vývoji zákaznických funkcí intenzivně používají makra. Při jejich použití (pro varlena typy je to nutnost) by zdrojový kód výše uvedené funkce vypadal asi takto:
Datum sum_int(PG_FUNCTION_ARGS) { int a1 = PG_GETARG_INT32(0); // vrať první parametr funkce převedený do 4byte int int a2 = PG_GETARG_INT32(1); // vrať druhý parametr funkce převedený do 4byte int PG_RETURN_INT32(a1 + a2); // jako výsledek funkce vrať součet hodnot }
Pro začátečníka to může být trochu kryptografické - ale hodně to snižuje riziko chyb a také to zavádí určitý jednotný styl, určitou štábní kulturu. Pro přístup k parametrům se používají makra PG_GETARG_typ(n) (případně makra PG_GETARG_typ_P(n) pokud je typ ukazatelem). Naopak výsledek funkce se předává pomocí makra PG_RETURN_typ(hodnota) (případně makrem PG_RETURN_POINTER(ukazatel), pokud je typ ukazatelem). V případě, že funkce vrací NULL se použije makro PG_RETURN_NULL(). Tato makra se postarají o všechny potřebné vstupně/výstupní konverze. Aby funkce vracela správné hodnoty, musí typ vrácené hodnoty souhlasit s návratovým typem registrované funkce. To je poměrně častá chyba začátečníků.
#define PG_FUNCTION_ARGS FuncationCallInfo fcinfo #define PG_NARGS() (fcinfo->nargs) #define PG_ARGISNULL(n) (fcinfo->argnull[n]) #define PG_GETARG_DATUM(n) (fcinfo->arg[n]) #define PG_GETARG_INT32(n) DatumGetInt32(PG_GETARG_DATUM(n)) #define PG_RETURN_INT32(x) return Int32GetDatum(x) #define PG_RETURN_NULL() do { fcinfo->isnull = true; return (Datum) 0; } while (0)
Pozor na přetěžování funkcí. V PostrgeSQL můžete mít registrováno několik funkcí pod stejným názvem s různými parametry a různými typy vrácené hodnoty. Také se mi stalo, že jsem upravoval funkci, přičemž moje úpravy se vůbec v chování funkce neprojevily. Až po chvíli mi došlo, že jsem upravoval jinou funkci než tu, kterou jsem volal.
V1 funkci nelze volat klasicky - protože V1 funkce očekává V1 předávání parametrů místo Cčkového způsobu předávání parametrů. Protože ale předávání parametrů V1 volající konvencí je fakticky postavené na Cčkovém předávání parametrů lze snadno realizovat volání V1 funkce přímo v C. Musíme jen připravit a naplnit strukturu FunctionCallInfo. K inicializaci této struktury můžeme použít makro InitFunctionCallInfo.
#define InitFunctionCallInfoData(Fcinfo, Flinfo, Nargs, Context, Resultinfo) \ do { \ (Fcinfo).flinfo = (Flinfo); \ (Fcinfo).context = (Context); \ (Fcinfo).resultinfo = (Resultinfo); \ (Fcinfo).isnull = false; \ (Fcinfo).nargs = (Nargs); \ } while (0)
Pokud známe adresu funkce (ukazatel na funkci), tak lze použít přípravenou sadu funkcí, DirectFunctionCallN, kterým předáváme ukazatel na funkci a parametry. Uvnitř této funkce se inicializuje struktura FunctionCallInfo a dojde k zavolání funkce:
Datum DirectFunctionCall1(PGFunction func, Datum arg1) { FunctionCallInfoData fcinfo; Datum result; InitFunctionCallInfoData(fcinfo, NULL, 1, NULL, NULL); fcinfo.arg[0] = arg1; fcinfo.argnull[0] = false; result = (*func) (&fcinfo); /* Check for null result, since caller is clearly not expecting one */ if (fcinfo.isnull) elog(ERROR, "function %p returned NULL", (void *) func); return result; } /* ukázka - převod řetězce na velká písmena */ result = DatumGetTextP(DirectFunctionCall1(upper, TextPGetDatum(str)));
Pokud adresu funkce neznáme - což jsou např funkce z dynamických knihoven, tak můžeme identifikovat volanou funkci jejím oid (object identifier). Místo DirectFunctionCall používáme funkci OidFunctionCall:
Datum OidFunctionCall1(Oid functionId, Datum arg1) { FmgrInfo flinfo; FunctionCallInfoData fcinfo; Datum result; fmgr_info(functionId, &flinfo); InitFunctionCallInfoData(fcinfo, &flinfo, 1, NULL, NULL); fcinfo.arg[0] = arg1; fcinfo.argnull[0] = false; result = FunctionCallInvoke(&fcinfo); /* Check for null result, since caller is clearly not expecting one */ if (fcinfo.isnull) elog(ERROR, "function %u returned NULL", flinfo.fn_oid); return result; }
Nyní stojíme před problémem, jak získat Oid funkce. Pro hledání vstupně/výstupních funkcí k požadovanému typu existuje funkce getTypeInputInfo. Id běžných funkcí můžeme dohledat dotazem:
postgres=# select 'sin(double precision)'::regprocedure::int; int4 ------ 1604 (1 row)
také lze použít fragment z implementace vstupní funkce typu regprocedure:
/* * Else it's a name and arguments. Parse the name and arguments, look up * potential matches in the current namespace search list, and scan to see * which one exactly matches the given argument types.<>(There will not be * more than one match.) * * XXX at present, this code will not work in bootstrap mode, hence this * datatype cannot be used for any system column that needs to receive * data during bootstrap. */ parseNameAndArgTypes(pro_name, false, &names, &nargs, argtypes); clist = FuncnameGetCandidates(names, nargs, NIL, false, false); for (; clist; clist = clist->next) { if (memcmp(clist->args, argtypes, nargs * sizeof(Oid)) == 0) break; } if (clist == NULL) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function \"%s\" does not exist", pro_name))); result = clist->oid;
Pokud víme, že funkce není přetížena, lze výše uvedený fragment zjednodušit:
names = stringToQualifiedNameList(pro_name); clist = FuncnameGetCandidates(names, -1, NIL, false, false); if (clist == NULL) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function \"%s\" does not exist", pro_name_or_oid))); else if (clist->next != NULL) ereport(ERROR, (errcode(ERRCODE_AMBIGUOUS_FUNCTION), errmsg("more than one function named \"%s\"", pro_name))); result = clist->oid;
Konečně - pokud funkci identifikovanou Oid voláme opakovaně, tak je vhodné si uložit její descriptor FmgrInfo a ten pak používat pro identifikaci funkce:
/* * This macro invokes a function given a filled-in FunctionCallInfoData * struct. The macro result is the returned Datum --- but note that * caller must still check fcinfo->isnull! Also, if function is strict, * it is caller's responsibility to verify that no null arguments are present * before calling. */ #define FunctionCallInvoke(fcinfo) ((* (fcinfo)->flinfo->fn_addr) (fcinfo)) Datum FunctionCall1(FmgrInfo *flinfo, Datum arg1) { FunctionCallInfoData fcinfo; Datum result; InitFunctionCallInfoData(fcinfo, flinfo, 1, NULL, NULL); fcinfo.arg[0] = arg1; fcinfo.argnull[0] = false; result = FunctionCallInvoke(&fcinfo); /* Check for null result, since caller is clearly not expecting one */ if (fcinfo.isnull) elog(ERROR, "function %u returned NULL", fcinfo.flinfo->fn_oid); return result; }
K čemu slouží signatura funkce?
Při zavádění V1 volající konvence vývojáři PostgreSQL hledali způsob, jak zajistit kompatibilitu stávajících externích modulů. Řešení je poměrně vtipné - každá V1 funkce musí mít svou signaturu - a na základě její existence nebo neexistence se executor rozhoduje, jestli použije C volající konvenci nebo V1 volající konvenci.
Toto řešení funguje dobře - drobným kazem na kráse jsou občas "nevysvětlitelné pády" serveru při ladění V1 funkcí, u kterých vývojář zapomněl signaturu - obvykle přístup k V1 struktuře, která ovšem nebyla inicializována, jelikož funkce byla volána s C volající konvencí vede k segmantation-faultu. Je proto praktické napsat nejprve signaturu V1 funkce a teprve poté vlastní V1 funkci (pozn. pokud dojde k nečekaným pádům v makrech PG_GETARG_* nebo PG_ARGISNULL(), pak jako první krok doporučuji zkontrolovat shodu identifikátoru funkce a identifikátoru signatury).
Signaturou funkce se míní použití makra PG_FUNCTION_INFO_V1:
PG_FUNCTION_INFO_V1(pst_left); Datum pst_left(PG_FUNCTION_ARGS) { ... ... PG_RETURN_TEXT_P(result); }
Toto makro vytvoří funkci pg_finfo_názevfce:
const Pg_finfo_record * pg_finfo_pst_left (void) { static const pg_finfo_record myfinfo = {1}; return &myfinfo; }
Přičemž před použitím funkce se zjišťuje, zda-li taková funkce existuje. Pokud existuje, tak se zavolá a z výsledku se zjistí, jakou volající konvenci funkce vyžaduje (podporuje). Zatím existuje pouze V1 volající konvence, nicméně v budoucnu je možné vytvořit i další rozšíření této konvence.
const Pg_finfo_record * fetch_finfo_record(void *filehandle, char *funcname) { char *infofuncname; PGFInfoFunction infofunc; static Pg_finfo_record default_inforec = {0}; infofuncname = (char *) palloc(strlen(funcname) + 10); strcpy(infofuncname, “pg_finfo_”); strcat(infofuncname, funcname); infofunc = lookup_external_function(filehandle, Infofuncname); if (infofunc == NULL) { /* Not found --- assume version 0 */ pfree(infofuncname); return &default_inforec; } /* Found, so call it */ inforec = (*infofunc) (); pfree(infofuncname); return inforec; }
Na základě signatury funkce je rozhodnuto o způsobu volání funkce - viz kód funkce fmgr_info_C_lang.
K čemu slouží signatura modulu?
Vývoj externích modulů v PostgreSQL se potýká s jedním základním problémem - a tím je neexistence univerzálního stabilního API napříč major verzemi. Samozřejmě, že je snahou API neměnit - případně, pokud to lze, tak měnit bez narušení zpětné kompatibility. Jinak by byli vývojáři PostgreSQL sami proti sobě. Navíc z důvodu rozdílné velikosti základních typů nejsou kompatibilní moduly pro 32 a 64bit. Obtížně detekovatelnou chybou je použití nekompatibilních externích modulů. Aby se této chybě zabránilo, tak každý modul má v sobě zakompilovanou informaci o tom, vůči které verzi PostgreSQL byl zkompilován. Při inicializaci externího modulu se tato informace porovná se skutečnou verzí PostgreSQL a v případě, že modul nebyl zkompilován pro použitou verzi PostgreSQL, je hlašena chyba a modul není použit. Vložení informací o verzi pro pozdější porovnání zajistí makro PG_MODULE_MAGIC, tím že vytvoří funkci Pg_magic_func:
#ifdef PG_MODULE_MAGIC PG_MODULE_MAGIC; #endif
Aktuálně se vloží následující údaje:
/* Definition of the magic block structure */ typedef struct { int len; /* sizeof(this struct) */ int version; /* PostgreSQL major version */ int funcmaxargs; /* FUNC_MAX_ARGS */ int indexmaxkeys; /* INDEX_MAX_KEYS */ int namedatalen; /* NAMEDATALEN */ int float4byval; /* FLOAT4PASSBYVAL */ int float8byval; /* FLOAT8PASSBYVAL */ } Pg_magic_struct; /* The actual data block contents */ #define PG_MODULE_MAGIC_DATA \ { \ sizeof(Pg_magic_struct), \ PG_VERSION_NUM / 100, \ FUNC_MAX_ARGS, \ INDEX_MAX_KEYS, \ NAMEDATALEN, \ FLOAT4PASSBYVAL, \ FLOAT8PASSBYVAL \ } #define PG_MODULE_MAGIC \ const Pg_magic_struct * \ Pg_magic_func(void) \ { \ static const Pg_magic_struct Pg_magic_data = { \ sizeof(Pg_magic_struct), \ PG_VERSION_NUM / 100, \ FUNC_MAX_ARGS, \ INDEX_MAX_KEYS, \ NAMEDATALEN, \ FLOAT4PASSBYVAL, \ FLOAT8PASSBYVAL \ }; \ return &Pg_magic_data; \ } \
Funkce Pg_magic_func je volána při natažení modulu - vrací strukturu Pg_magic_struct, která je pak binárně porovnána s aktuální Pg_magic_struct strukturou serveru.
/* Check the magic function to determine compatibility */ magic_func = (PGModuleMagicFunction) pg_dlsym(file_scanner->handle, PG_MAGIC_FUNCTION_NAME_STRING); if (magic_func) { const Pg_magic_struct *magic_data_ptr = (*magic_func) (); if (magic_data_ptr->len != magic_data.len || memcmp(magic_data_ptr, &magic_data, magic_data.len) != 0) { /* copy data block before unlinking library */ Pg_magic_struct module_magic_data = *magic_data_ptr;
Konverze z generického typu "Datum" do C typu a zpět
Typ Datum je pro PostgreSQL zásadní - představuje jediný interní abstraktní typ s kterým se pracuje v kódu PostgreSQL. Datum je celočíselný typ stejné velikosti jako ukazatel - může obsahovat číslo, znak nebo ukazatel na větší strukturu dat. Trochu zvláštností je konverze na C typy, která je realizována pomocí sady maker/funkcí. Tyto makra jsou relativně důležitá, zakrývají implementační záležitosti a transparentně zajišťují deTOAST a dekomprimaci typů s variabilní délkou. U kratších typů než je ukazatel tato makra zajišťují vymaskování balastu. Kromě jiného realizují i konkrétní přetypování:
#define Int32GetDatum(X) ((Datum) SET_4_BYTES(X)) #define DatumGetInt32(X) ((int32) GET_4_BYTES(X)) #define PointerGetDatum(X) ((Datum) X)
DeTOAST struktury varlena zajišťuje zajišťuje funkce pg_detoast_datum, která se v následující ukázce objeví:
#define PG_DETOAST_DATUM(X) pg_detoast_datum((struct varlena *) DatumGetPointer(X)) #define DatumGetTextP(X) ((text *) PG_DETOAST_DATUM(X)) #define PG_GETARG_TEXT_P(X) DatumGetTextP(PG_GETARG_DATUM(X)) #define PG_RETURN_TEXT_P(X) PG_RETURN_POINTER(X)
Názvy konverzních maker dodržují následující metodiku: ZdrojovýTypGetCílovýTyp.
String Builder
Spojování, formátování řetězců je v Cčku docela "dřina". Musí se správně alokovat paměť, musí se dávat pozor na přetečení bufferů. K zpříjemnění života si vývojáři připravili knihovnu "stringinfo", která všechny výše uvedené operace zvládá:
#include "lib/stringinfo.h" ... StringInfoData ds; initStringInfo(&ds); appenStringInfo(&ds, "Hello, %s", "World"); ... pfree(ds.data);
Použití C řetězců
Cčkovému char* odpovídá typ CString. Používá se pro vstupně/výstupní operace. Každý datový typ v PostgreSQL má zaregistrovánu jednu "in" funkci a jednu "out" funkci. Vstupem "in" funkce je hodnota typu CString, výstupem pak adekvátní binární hodnota. "out" funkce je reverzní k "in" funkci. "in" funkce se používá pro parsování vstupu. "out" funkce se používá pro formátování výstupu. Je jasné, že se s typem CString bude v Cčku poměrně pohodlně pracovat - přesto se nedoporučuje psát funkce (mimo "in"/"out" funkcí), které by měly parametry tohoto typu nebo vracely CString. Tento typ totiž nepodporuje TOAST a tudíž nelze uložit delší řetězce než je 8KB (samozřejmě, že lze pracovat s delšími CString řetězci, jen je nelze uložit do db). Proto si také můžete všimnout, že se CString neobjevuje ani ve vestavěných funkcí. Z pohledu SQL generický typ pro řetězce je "text".
Pro konverzi mezi CStringem a typem "text", která je častá (naopak funkce z glibc neumí pracovat s typy PostgreSQL), lze používat makra:
text_to_cstring(text *txt); cstring_to_text(char *str); cstring_to_text_with_len(char *str, size_t len);
Variace na funkci "Hello" by v PostgreSQL s použitím string builderu mohla vypadat asi následovně:
Datum hello(PG_FUNCTION_ARGS) { StringInfoData ds; text *txt; txt = PG_GETARG_TEXT_P(0); initStringInfo(&ds); appendStringInfo(&ds, "Hello, %s", text_to_cstring(txt)); PG_RETURN_TEXT_P(cstring_to_text_with_len(ds.data, ds.len)); } -- registrace funkce CREATE FUNCTION hello(text) RETURNS text AS 'libdir/hellolib' LANGUAGE C STRICT;
Úpravy kódu pro více bajtová kódování (UTF8)
V okamžiku, kdy uvnitř funkce potřebujeme znát počet znaků, musíme si uvědomit, že existují tzv více bajtová kódování. Jedno z nich - UTF8 - se intenzivně používá i v středoevropském prostoru (cca v devadesátých letech bylo použití více bajtových kódování omezeno téměř výlučně na asijskou oblast). Rozdíly v implementaci více bajtových kódování mizí při použití PostgreSQL knihovní funkce pg_mblen. Tato funkce vrací velikost aktuálního vícebajtového znaku.
Funkce, která vrací počet znaků v řetězci - nikoliv počet bajtů může vypadat následovně:
Datum string_length(PG_FUNCTION_ARGS) { text *str = PG_GETARG_TEXT_P(0); int bytes, processed, mblen = 0; int result = 0; char *ptr; ptr = VARDATA(str); bytes = VARSIZE(str) - VARHDRSZ; while (processed < bytes) { result++; mblen = pg_mblen(ptr); ptr += mblen; processed += mblen; } PG_RETURN_INT32(result); }
Základní makra pro typ "varlena"
Abstraktní typ varlena se v PostgreSQL používá pro všechny typy s variabilní velikostí nebo pro typy větší než je velikost typu Datum. Tato struktura je sama o sobě poměrně variabilní - zvládá jak typy, které podporují TOAST, tak i typy, které TOAST nepodporují. Navíc v novějších může být délka (viz níže) 8, 16 nebo 32 bitů. V zásadě je varlena blok dat, který v prvních n bajtů má uloženou délku bloku (plus v několika málo bitech ještě procesní metadata). U krátkých typů (např. NUMERIC) je zásadní, že délka může být zakódována v jednom bajtu. S hodnotou typu varlena můžeme provádět základní operace - kopírování, serializaci, deserializaci, aniž bychom museli rozumět obsahu, který je v hodnotě zakódován.
typedef struct varlena { char v1_len[4]; char v1_dat[1]; }
Vzhledem ke složitosti operací, které tato struktura zajišťuje (a jejich významu) se nedoporučuje pracovat s daty ve formátu (struktuře) varlena přímo. Používají se připravená makra dle zažitých vzorů:
/* získání čisté délky */ VARSIZE(ptr) - VARHDRSZ; /* získání ukazatele na vlastní data */ VARDATA(ptr); /* určení délky */ SET_VARSIZE(ptr, velikost + VARHDRSZ);
Dobrou a jednoduchou ukázkou používání varlena typu může být jednoduchá funkce concat, která slouží k spojení dvou řetězců:
Datum concat(PG_FUNCTION_ARGS) { text *a = PG_GETARG_TEXT_P(0); text *b = PG_GETARG_TEXT_P(1); text *result; int l_a, l_b, l_result; char *ptr; l_a = VARSIZE(a) - VARHDRSZ; l_b = VARSIZE(b) - VARHDRSZ; l_result = l_a + l_b + VARHDRSZ; result = palloc(l_result); ptr = VARDATA(result); memcpy(ptr, VARDATA(a), l_a); memcpy(ptr + l_a, VARDATA(b), l_b); SET_VARSIZE(result, l_result); PG_RETURN_TEXT_P(result); }
Správa paměti
V PostgreSQL je dynamické paměť alokována z aktuálního nebo jinak určeného kontextu. Zjednodušeně lze kontext popsat jako jakousi abstraktní evidenci jednotlivých alokací paměti. Alokovanou paměť lze uvolnit voláním pfree (čímž dojde k uvolnění bufferů a výmazu z evidence) nebo zrušením kontextu. Odstranění kontextu je podstatně rychlejší operace než jednotlivá volání pfree - navíc použití kontextů snižuje fragmentaci paměti. Řada důležitých procesů v PostgreSQL má přidělen jeden nebo více paměťových kontextů. Uvnitř procesu se alokuje paměť vždy z těchto přidělených kontextů a po dokončení procesu se přidělené paměťové kontexty odstraňují. Tím se minimalizuje riziko úniků paměti (memory leaks) a do jisté míry se i zjednodušuje kód. Paměťové kontexty mohou být hierarchické. Odstranění kontextu vede k odstranění všech podřízených paměťových kontextů.
Nevýhodou paměťových kontextů je jejich režie, která se projeví při alokaci palloc velkého počtu extrémně malých bloků. Implementace paměťových kontextů předřazuje ke každému alokovanému bloku ukazatel na deskriptor kontextu. Pokud by mělo docházet k intenzivní alokaci paměti a pak její realokaci realloc, tak je výhodnější si alokovat více paměti a snížit počet volání realloc. Dobrou ukázkou je kód v knihovně stringinfo. poměrně často se v PostgreSQL můžeme setkat se situací, kdy je paměť pouze alokována a nikde není explicitně uvolněna (např. ve funkcích, které jsou dostupné z SQL). Před zavoláním každé funkce executorem je vytvořen dočasný aktuální kontext, který se prohlásí za aktuální. Uvnitř funkce se pak paměť alokuje (pokud se neuvede explicitně jinak) z tohoto kontextu. Poté, co funkce skončí (a to ať úspěšně nebo neúspěšně), executor odstraní tento dočasný kontext (poté co zkopíroval výsledek do nadřazeného kontextu - pokud se jedná o varlena typ). Tímto mechanismem je zajištěno, že nedojde k únikům paměti - nebo je toto riziko zásadně minimalizováno. To je pro PostgreSQL poměrně zásadní, neboť se předpokládá, že zákaznické funkce v C si pokročilí uživatelé PostgreSQL budou psát sami a je poměrně velká šance, že svůj kód dostatečně nezabezpečí proti úniku paměti. Pokud nedochází k velkým alokacím paměti, pak (při dnešních kapacitách paměti), lze neřešit explicitní uvolnění paměti (které může být pomalejší než zruešení kontextu), protože víme, že po ukončení funkce bude vždy celý paměťový kontext, a s ním i veškerá v něm alokovaná paměť, vždy odstraněn.
Určitou obtíž, kterou paměťové kontexty mají je nutnost jejich zvládnutí a pochopení pro začínající programátory. Typicky se očekává, že bude výsledek typu varlena alokován v aktuálním kontextu (v aktuálním kontextu při startu funkce). Některá API - např. SPI při své inicializaci mění aktuální kontext - za svůj vlastní, který je při uvolnění SPI odstraněn. Pokud by návratová hodnota byla alokována v tomto kontextu, tak je přepsána (nebo může být náhodně přepsána (viz parametr konfigurace --enable-cassert)). Reference na takový blok paměti může vést k získání výsledku, který odpovídá náhodné posloupnosti bajtů v paměti nebo k pádu serveru - typicky segmentation fault. Pokud je PostgreSQL konfigurován s parametrem --enable-cassert, pak před uvolněním kontextu dojde k explicitnímu přepsání alokovaných dat (takže viditelně poznáme problém) a v některých kontrolních bodech ověřuje platnost kontextu. Pro SPI je nutné paměť pro výsledek alokovat funkcí SPI_calloc, která vrací paměť z kontextu, který byl aktuální před inicializací SPI.
Pokud funkce vrací typ tuplestore je nutné použít nikoliv aktuální kontext při inicializaci funkce, nýbrž vyhrazený kontext, který je přednastavený ve struktuře fcinfo. Tím, že tento kontext prohlásíme aktuální, budou všechny alokace nasměrované do tohoto kontextu (s přenastavením aktuálního kontextu na konkrétní aktuální kontext se v PostgreSQL setkáme často viz MemoryContextSwitchTo):
Datum generate_series(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc tupdesc; Tuplestorestate *tupstore; MemoryContext per_query_ctx; MemoryContext oldcontext; int i, start, end; /* check to see if caller supports us returning a tuplestore */ if (!(rsinfo->allowedModes & SFRM_Materialize)) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("materialize mode required, but it is not " \ "allowed in this context"))); start = PG_GETARG_INT32(0); end = PG_GETARG_INT32(1); /* need to build tuplestore in query context */ per_query_ctx = rsinfo->econtext->ecxt_per_query_memory; oldcontext = MemoryContextSwitchTo(per_query_ctx); tupdesc = rsinfo->expectedDesc; tupstore = tuplestore_begin_heap(true, false, work_mem); for (i = start; i <= end; i++) { Datum value; bool null = false; HeapTuple tuple; /* generate junk in short-term context */ MemoryContextSwitchTo(oldcontext); value = Int32GetDatum(i); tuple = heap_form_tuple(tupdesc, &value, &null); MemoryContextSwitchTo(per_query_ctx); tuplestore_puttuple(tupstore, tuple); } /* clean up and return the tuplestore */ tuplestore_donestoring(tupstore); MemoryContextSwitchTo(oldcontext); rsinfo->returnMode = SFRM_Materialize; rsinfo->setResult = tupstore; rsinfo->setDesc = tupdesc; return (Datum) 0; }
Privátní kontext se obvykle vytváří pod paměťovým kontextem vytvořeným executorem. Tím, je zajištěno jeho odstranění v případě chyby. Vlastník kontextu (procedura, která vytvořila kontext) je zodpovědný za jeho zrušení. Pokud nemůžeme využít tento mechanismus - např. chceme aby paměťový kontext "přežil" volání funkce, musíme odstranit kontext v callback funkci transakce nebo subtransakce viz funkce AtEOSubXact_SPI. Callback funkce transakce se registrují funkcemi RegisterXactCallback nebo RegisterSubXactCallback:
RegisterXactCallback(plpgsql_xact_cb, NULL); RegisterSubXactCallback(plpgsql_subxact_cb, NULL);
API pro zákaznické funkce (Server Programming Interface)
Rozhraní SPI je jediné rozhraní, které je, jako takové, určeno pro použití v uživatelských funkcích. Umožňuje provést SQL příkaz, umožňuje získat a zpracovat výsledek SQL příkazu. Podporuje kurzory a nechybí ani podpora prepared statements a parametrized queries. Práce s tímto rozhraním není komplikovaná. Pravděpodobně jediná záludnost je v tom, že vytváří vlastní paměťový kontext, v kterém jsou uložené výsledky SQL příkazů, a který po uzavření API odstraňuje. Pokud nedojde k zkopírování výsledků do vnějšího paměťového kontextu, tak zrušením kontextu dojde ke ztrátě výsledků. Pro alokaci paměti z vnějšího paměťového kontextu lze použít funkci SPI_palloc().
API lze používat po volání funkce SPI_connect(). Funkce SPI_finish() uvolňuje zdroje - zejména alokovanou paměť.
Datum eval_query(PG_FUNCTION_ARGS) { char *query = text_to_cstring(PG_GETARG_TEXT_P(1)); char *result = NULL; text *result_text = NULL; MemoryContext callCtx = CurrentMemoryContext; SPI_connect(); ret = SPI_exec(query, 0); if (ret > 0 && SPI_tuptable != NULL) { TupleDesc tupdesc = SPI_tuptable->tupdesc; SPITupleTable *tuptable = SPI_tuptable; HeapTuple tuple = tuptable->vals[0]; if (tupdesc->natts > 1) elog(ERROR, “Query returned more columns”); if (SPI_processed > 1) elog(ERROR, “Query returned more rows”); else if (SPI_processed == 1) result = SPI_getvalue(tuple, tupdesc, 1); } /* * Aktuálně result je NULL nebo se odkazuje na c string v * v prostoru spravovaném SPI, proto je nutne pripadny * obsah vytvorit v kontextu, ktery byl aktualni v dobe * volani funkce. */ if (result != NULL) { MemoryContext spiCtx; spiCtx = MemoryContextSwitchTo(callCtx); result_text = cstring_to_text(result); MemoryContextSwitchTo(spiCtx); } /* * Nyni je vysledek dotazu uspesne zkopirovan, a tudiz lze * odstranit pametovy kontext spravovany SPI */ SPI_logout(); if (result_text != NULL) PG_RETURN_TEXT_P(result_text); else PG_RETURN_NULL(); }
Ve výše zmíněné ukázce dochází k transformaci výsledku dotazu z formátu cstring do formátu text. Jelikož před touto transformací došlo ke změně paměťového kontextu, tak zároveň dochází k exportu dat z kontextu SPI do kontextu funkce. Transformovaný řetězec - ve formátu text bude vytvořen v paměťovém kontextu, který je aktuální při spuštění funkce (a který je aktuální během transformace).
Parametrický dotaz se provádí prostřednictvím funkce SPI_execute_with_args. Pokud se používají skalární datové typy, tak i tato funkce se používá jednoduše. Poněkud neobvyklé je předávání informace o tom, zda-li je některý parametr NULL. Tyto informace se nepředávají v poli typu boolean, jak bychom čekali, nýbrž v poli znaků, kde mezera znamená NOT NULL a znak 'n' znamená NULL. pokud bychom naší funkci eval rozšířili o podporu parametrizovaných dotazů, pak by tato funkce - resp. část, kde se aktivuje zpracování SQL příkazu, mohla vypadat následovně (Funkce musí být deklarována s variadickým parametrem typu "any"):
Datum args[FUNC_MAX_ARGS]; char nulls[FUNC_MAX_ARGS]; Oid types[FUNC_MAX_ARGS] int i; for(i = 0; i < PG_NARGS() - 1; i++) { args[i] = PG_GETARG_DATUM(i + 1); nulls[i] = PG_ARGISNULL(i + 1) ? 'n' : ' '; types[i] = get_fn_expr_argtype(fcinfo->flinfo, i + 1); } res = SPI_execute_with_args(query, PG_NARGS() - 1, types, args, nulls, true, 2); ...
Funkce get_fn_expr_argtype vrací Oid n-tého parametru.
Sebereflexe uvnitř PostgreSQL - typ Node
Typ Node a typy z něj vycházející umožňují vytvářet "chytré" kompozitní typy, které podporují serializaci, deserializaci, porovnání, kopírování a zobrazení. Typ Node je jednoduchá struktura, která jako první položku obsahuje číslo, které určuje obsah struktury. Typ Node můžeme chápat jako o pokus implementace abstraktní třídy v klasickém neobjektovém jazyku C. V C není k dispozici dědičnost, tudíž není možné dost dobře mluvit o potomcích typu Node. Můžeme ovšem hovořit o množině typů respektujících interface typu Node.
typedef struct Node { NodeTag type; } Node; /* * nodes/{outfuncs.c,print.c} */ extern char *nodeToString(void *obj); /* * nodes/{readfuncs.c,read.c} */ extern void *stringToNode(char *str); /* * nodes/copyfuncs.c */ extern void *copyObject(void *obj); /* * nodes/equalfuncs.c */ extern bool equal(void *a, void *b); typedef struct Value { NodeTag type; /* tag appropriately (eg. T_String) */ union ValUnion { long ival; /* machine integer */ char *str; /* string */ } val; } Value; typedef struct Value { NodeTag type; /* tag appropriately (eg. T_String) */ union ValUnion { long ival; /* machine integer */ char *str; /* string */ } val; } Value;
Typu respektující interface Node se v PostgreSQL vyskytují ve všech modulech - v parseru, optimalizéru, v executoru, v interních knihovnách. Všude, kde je nutné řešit serializaci, deserializaci. Všude, kde je potřeba řešit hluboké kopírování. Generický typ List respektuje Node interface. V některých případech z meta dat funkce můžeme zjistit kontext volání funkce. Opět tato metadata typicky respektují interface Node. Množinu typů respektujících interface Node nelze rozšiřovat bez změny zdrojových kódů PostgreSQL - primárně rozšíření výčtu NodeTag.
Pokud datový typ respektuje interface Node, tak pak můžeme ověřit, zda-li je nebo není nějaká struktura určitého typu. Používá se makro IsA:
#define nodeTag(nodeptr) (((Node*)(nodeptr))->type) #define IsA(nodeptr,_type_) (nodeTag(nodeptr) == T_##_type_)
V kódu PostgreSQL se často setkáváme hierarchií struktur respektujících interface Node:
struct ExprState { NodeTag> type; Expr *expr; /* associated Expr node */ ExprStateEvalFunc evalfunc; /* routine to run to execute node */ }; /* ---------------- * GenericExprState node * * This is used for Expr node types that need no local run-time state, * but have one child Expr node. * ---------------- */ typedef struct GenericExprState { ExprState xprstate; ExprState *arg; /* state of my child node */ } GenericExprState; /* ---------------- * AggrefExprState node * ---------------- */ typedef struct AggrefExprState { ExprState xprstate; List *args; /* states of argument expressions */ int aggno; /* ID number for agg within its plan node */ } AggrefExprState; /* ---------------- * WindowFuncExprState node * ---------------- */ typedef struct WindowFuncExprState { ExprState xprstate; List *args; /* states of argument expressions */ int wfuncno; /* ID number for wfunc within its plan node */ } WindowFuncExprState;
V následující ukázce se zjišťuje kontext volání funkce a podle tagu se určuje, zda-li byla funkce volána jako agregační nebo jako analytická (window) funkce:
int AggCheckCallContext(FunctionCallInfo fcinfo, MemoryContext *aggcontext) { if (fcinfo->context && IsA(fcinfo->context, AggState)) { if (aggcontext) *aggcontext = ((AggState *) fcinfo->context)->aggcontext; return AGG_CONTEXT_AGGREGATE; } if (fcinfo->context && IsA(fcinfo->context, WindowAggState)) { if (aggcontext) *aggcontext = ((WindowAggState *) fcinfo->context)->aggcontext; return AGG_CONTEXT_WINDOW; } /* this is just to prevent "uninitialized variable" warnings */ if (aggcontext) *aggcontext = NULL; return 0; }
K vytvoření nové struktury typu Node slouží makro makeNode. Toto makro je ukázkou toho, co lze realizovat preprocesorem v C, pokud si vybudujeme šikovnou infrastrukturu datových typů a identifikátorů:
#define newNode(size, tag) \ ( \ AssertMacro((size) >= sizeof(Node)), /* need the tag, at least */ \ newNodeMacroHolder = (Node *) palloc0fast(size), \ newNodeMacroHolder->type = (tag), \ newNodeMacroHolder \ ) #endif /* __GNUC__ */ #define makeNode(_type_) ((_type_ *) newNode(sizeof(_type_),T_##_type_)) #define NodeSetTag(nodeptr,t) (((Node*)(nodeptr))->type = (t))
Operace nad zanořenými (rekurzivními) strukturami podporující interface Node
V zdrojovém kódu PostgreSQL se nejčastěji setkáme s identifikací struktury typu Node a s provedením určitých operací odpovídajících tomu či onomu subtypu:
Node * transformExpr(ParseState *pstate, Node *expr) { Node *result = NULL; if (expr == NULL) return NULL; /* Guard against stack overflow due to overly complex expressions */ check_stack_depth(); switch (nodeTag(expr)) { case T_ColumnRef: result = transformColumnRef(pstate, (ColumnRef *) expr); break; case T_ParamRef: result = transformParamRef(pstate, (ParamRef *) expr); break; case T_A_Const: { A_Const *con = (A_Const *) expr; Value *val = &con->val; result = (Node *) make_const(pstate, val, con->location); break; } ... case T_ArrayCoerceExpr: case T_ConvertRowtypeExpr: case T_CollateExpr: case T_CaseTestExpr: case T_ArrayExpr: case T_CoerceToDomain: case T_CoerceToDomainValue: case T_SetToDefault: { result = (Node *) expr; break; } default: /* should not reach here */ elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); break; } return result; }
již bylo zmíněno, že data uložená ve strukturách typu Node lze porovnávat, kopírovat (fce copyObject()), porovnávat (fce equal()), serializovat (fce nodeToString() a načítat (fce stringToNode()).
Pro porovnávání a kopírování nelze použít funkce, které přistupují přímo k paměti. Při kopírování se vytváří nezávislá kopie včetně vnořených položek. Při porovnávání se zase ignoruje položka location.
Implementace opět intenzivně používá makra.
Kopírování:
/* Copy a simple scalar field (int, float, bool, enum, etc) */ #define COPY_SCALAR_FIELD(fldname) \ (newnode->fldname = from->fldname) /* Copy a field that is a pointer to some kind of Node or Node tree */ #define COPY_NODE_FIELD(fldname) \ (newnode->fldname = copyObject(from->fldname)) /* Copy a field that is a pointer to a Bitmapset */ #define COPY_BITMAPSET_FIELD(fldname) \ (newnode->fldname = bms_copy(from->fldname)) /* Copy a field that is a pointer to a C string, or perhaps NULL */ #define COPY_STRING_FIELD(fldname) \ (newnode->fldname = from->fldname ? pstrdup(from->fldname) : (char *) NULL) /* * _copyNamedArgExpr * */ static NamedArgExpr * _copyNamedArgExpr(NamedArgExpr *from) { NamedArgExpr *newnode = makeNode(NamedArgExpr); COPY_NODE_FIELD(arg); COPY_STRING_FIELD(name); COPY_SCALAR_FIELD(argnumber); COPY_LOCATION_FIELD(location); return newnode; }
Test na ekvivalenci:
/* Compare a simple scalar field (int, float, bool, enum, etc) */ #define COMPARE_SCALAR_FIELD(fldname) \ do { \ if (a->fldname != b->fldname) \ return false; \ } while (0) /* Compare a field that is a pointer to some kind of Node or Node tree */ #define COMPARE_NODE_FIELD(fldname) \ do { \ if (!equal(a->fldname, b->fldname)) \ return false; \ } while (0) /* Compare a field that is a pointer to a Bitmapset */ #define COMPARE_BITMAPSET_FIELD(fldname) \ do { \ if (!bms_equal(a->fldname, b->fldname)) \ return false; \ } while (0) /* Compare a field that is a pointer to a C string, or perhaps NULL */ #define COMPARE_STRING_FIELD(fldname) \ do { \ if (!equalstr(a->fldname, b->fldname)) \ return false; \ } while (0) static bool _equalNamedArgExpr(NamedArgExpr *a, NamedArgExpr *b) { COMPARE_NODE_FIELD(arg); COMPARE_STRING_FIELD(name); COMPARE_SCALAR_FIELD(argnumber); COMPARE_LOCATION_FIELD(location); return true; }
Serializace:
#define WRITE_NODE_TYPE(nodelabel) \ appendStringInfoString(str, nodelabel) /* Write an integer field (anything written as ":fldname %d") */ #define WRITE_INT_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %d", node->fldname) /* Write an unsigned integer field (anything written as ":fldname %u") */ #define WRITE_UINT_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %u", node->fldname) /* Write an OID field (don't hard-wire assumption that OID is same as uint) */ #define WRITE_OID_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %u", node->fldname) /* Write a long-integer field */ #define WRITE_LONG_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %ld", node->fldname) /* Write a char field (ie, one ascii character) */ #define WRITE_CHAR_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %c", node->fldname) /* Write an enumerated-type field as an integer code */ #define WRITE_ENUM_FIELD(fldname, enumtype) \ appendStringInfo(str, " :" CppAsString(fldname) " %d", \ (int) node->fldname) /* Write a float field --- caller must give format to define precision */ #define WRITE_FLOAT_FIELD(fldname,format) \ appendStringInfo(str, " :" CppAsString(fldname) " " format, node->fldname) /* Write a boolean field */ #define WRITE_BOOL_FIELD(fldname) \ appendStringInfo(str, " :" CppAsString(fldname) " %s", \ booltostr(node->fldname)) /* Write a character-string (possibly NULL) field */ #define WRITE_STRING_FIELD(fldname) \ (appendStringInfo(str, " :" CppAsString(fldname) " "), \ _outToken(str, node->fldname)) static void _outNamedArgExpr(StringInfo str, NamedArgExpr *node) { WRITE_NODE_TYPE("NAMEDARGEXPR"); WRITE_NODE_FIELD(arg); WRITE_STRING_FIELD(name); WRITE_INT_FIELD(argnumber); WRITE_LOCATION_FIELD(location); }
Použití iterátorů a mutátorů
Vzájemně se referencující struktury typu Node vytvářejí dynamickou super strukturu dat a vzájemných vztahů se kterou je nutné transformovat, zjednodušovat, rozpoznávat. Tato superstruktura není nesena abstraktní datovou strukturou, tudíž nelze jednoduše iterovat napříč touto strukturou. Navíc jazyk C nezpřístupňuje (minimálně ne jednoduše) popis jednotlivých struktur - tudíž nelze použít postupy založené na sebereflexi. V PostgreSQL se tento problém řeší pomocí tzv. iterátorů nebo mutátorů - což jsou funkce, uvnitř kterých je natvrdo zakódována struktura struktur typu Node, a které umožňují pro každou identifikovanou hodnotu zavolat callback funkci.
Iteraci napříč strukturami lze přerušit vrácením true z callback funkce.
bool expression_tree_walker(Node *node, bool (*walker) (), void *context) { ListCell *temp; /* * The walker has already visited the current node, and so we need only * recurse into any sub-nodes it has. * * We assume that the walker is not interested in List nodes per se, so * when we expect a List we just recurse directly to self without * bothering to call the walker. */ if (node == NULL) return false; /* Guard against stack overflow due to overly complex expressions */ check_stack_depth(); switch (nodeTag(node)) { case T_Var: case T_Const: case T_Param: case T_CoerceToDomainValue: case T_CaseTestExpr: case T_SetToDefault: case T_CurrentOfExpr: case T_RangeTblRef: case T_SortGroupClause: /* primitive node types with no expression subnodes */ break; case T_Aggref: { Aggref *expr = (Aggref *) node; /* recurse directly on List */ if (expression_tree_walker((Node *) expr->args, walker, context)) return true; if (expression_tree_walker((Node *) expr->aggorder, walker, context)) return true; if (expression_tree_walker((Node *) expr->aggdistinct, walker, context)) return true; } break; ... case T_List: foreach(temp, (List *) node) { if (walker((Node *) lfirst(temp), context)) return true; } break; ... case T_PlaceHolderInfo: return walker(((PlaceHolderInfo *) node)->ph_var, context); default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; } return false; }
Úkolem iterátoru je zavolat callback funkci pro každou referenci na strukturu typu Node ve vybraných strukturách typu Node. Iterátory jsou vždy zaměřené na hodnoty vyskytující se v určitém procesu zpracování SQL příkazů: surový dotaz, transformovaný dotaz, plán (ukázka obsahuje iterátor pro struktury typu Node použité pro implementaci výrazů).
Callback funkce používané v kombinaci s iterátory obsahují rozskok podle podtypu a případnou volání iterátoru:
static Bitmapset * find_unaggregated_cols(AggState *aggstate) { Agg *node = (Agg *) aggstate->ss.ps.plan; Bitmapset *colnos; colnos = NULL; (void) find_unaggregated_cols_walker((Node *) node->plan.targetlist, &colnos); (void) find_unaggregated_cols_walker((Node *) node->plan.qual, &colnos); return colnos; } static bool find_unaggregated_cols_walker(Node *node, Bitmapset **colnos) { if (node == NULL) return false; if (IsA(node, Var)) { Var *var = (Var *) node; /* setrefs.c should have set the varno to OUTER */ Assert(var->varno == OUTER); Assert(var->varlevelsup == 0); *colnos = bms_add_member(*colnos, var->varattno); return false; } if (IsA(node, Aggref)) /* do not descend into aggregate exprs */ return false; return expression_tree_walker(node, find_unaggregated_cols_walker, (void *) colnos); }
Mutátor slouží k modifikování stávající super struktury. Mutátor vytváří novou upravenou kopii dat. Stávající data zůstávají nezměněna.
Node * expression_tree_mutator(Node *node, Node *(*mutator) (), void *context) { /* * The mutator has already decided not to modify the current node, but we * must call the mutator for any sub-nodes. */ #define FLATCOPY(newnode, node, nodetype) \ ( (newnode) = (nodetype *) palloc(sizeof(nodetype)), \ memcpy((newnode), (node), sizeof(nodetype)) ) #define CHECKFLATCOPY(newnode, node, nodetype) \ ( AssertMacro(IsA((node), nodetype)), \ (newnode) = (nodetype *) palloc(sizeof(nodetype)), \ memcpy((newnode), (node), sizeof(nodetype)) ) #define MUTATE(newfield, oldfield, fieldtype) \ ( (newfield) = (fieldtype) mutator((Node *) (oldfield), context) ) if (node == NULL) return NULL; /* Guard against stack overflow due to overly complex expressions */ check_stack_depth(); switch (nodeTag(node)) { /* * Primitive node types with no expression subnodes. Var and * Const are frequent enough to deserve special cases, the others * we just use copyObject for. */ case T_Var: { Var *var = (Var *) node; Var *newnode; FLATCOPY(newnode, var, Var); return (Node *) newnode; } break; case T_Const: { Const *oldnode = (Const *) node; Const *newnode; FLATCOPY(newnode, oldnode, Const); /* XXX we don't bother with datumCopy; should we? */ return (Node *) newnode; } break; case T_Param: case T_CoerceToDomainValue: case T_CaseTestExpr: case T_SetToDefault: case T_CurrentOfExpr: case T_RangeTblRef: case T_SortGroupClause: return (Node *) copyObject(node); .... case T_List: { /* * We assume the mutator isn't interested in the list nodes * per se, so just invoke it on each list element. NOTE: this * would fail badly on a list with integer elements! */ List *resultlist; ListCell *temp; resultlist = NIL; foreach(temp, (List *) node) { resultlist = lappend(resultlist, mutator((Node *) lfirst(temp), context)); } return (Node *) resultlist; } break; .... case T_PlaceHolderInfo: { PlaceHolderInfo *phinfo = (PlaceHolderInfo *) node; PlaceHolderInfo *newnode; FLATCOPY(newnode, phinfo, PlaceHolderInfo); MUTATE(newnode->ph_var, phinfo->ph_var, PlaceHolderVar *); /* Assume we need not copy the relids bitmapsets */ return (Node *) newnode; } break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); break; } /* can't get here, but keep compiler happy */ return NULL; }
expression_tree_mutator se používá např. při substituci a vyhodnocení konstant v SQL příkazu:
static Node * eval_const_expressions_mutator(Node *node, eval_const_expressions_context *context) { if (node == NULL) return NULL; ... if (IsA(node, FuncExpr)) { FuncExpr *expr = (FuncExpr *) node; List *args; bool has_named_args; Expr *simple; FuncExpr *newexpr; ListCell *lc; /* * Reduce constants in the FuncExpr's arguments, and check to see if * there are any named args. */ args = NIL; has_named_args = false; foreach(lc, expr->args) { Node *arg = (Node *) lfirst(lc); arg = eval_const_expressions_mutator(arg, context); if (IsA(arg, NamedArgExpr)) has_named_args = true; args = lappend(args, arg); } /* * Code for op/func reduction is pretty bulky, so split it out as a * separate function. Note: exprTypmod normally returns -1 for a * FuncExpr, but not when the node is recognizably a length coercion; * we want to preserve the typmod in the eventual Const if so. */ simple = simplify_function(expr->funcid, expr->funcresulttype, exprTypmod(node), expr->funccollid, expr->inputcollid, &args, has_named_args, true, context); if (simple) /* successfully simplified it */ return (Node *) simple; /* * The expression cannot be simplified any further, so build and * return a replacement FuncExpr node using the possibly-simplified * arguments. Note that we have also converted the argument list to * positional notation. */ newexpr = makeNode(FuncExpr); newexpr->funcid = expr->funcid; newexpr->funcresulttype = expr->funcresulttype; newexpr->funcretset = expr->funcretset; newexpr->funcformat = expr->funcformat; newexpr->funccollid = expr->funccollid; newexpr->inputcollid = expr->inputcollid; newexpr->args = args; newexpr->location = expr->location; return (Node *) newexpr; } ... if (IsA(node, CoalesceExpr)) { CoalesceExpr *coalesceexpr = (CoalesceExpr *) node; CoalesceExpr *newcoalesce; List *newargs; ListCell *arg; newargs = NIL; foreach(arg, coalesceexpr->args) { Node *e; e = eval_const_expressions_mutator((Node *) lfirst(arg), context); /* * We can remove null constants from the list. For a non-null * constant, if it has not been preceded by any other * non-null-constant expressions then it is the result. * Otherwise, it's the next argument, but we can drop following * arguments since they will never be reached. */ if (IsA(e, Const)) { if (((Const *) e)->constisnull) continue; /* drop null constant */ if (newargs == NIL) return e; /* first expr */ newargs = lappend(newargs, e); break; } newargs = lappend(newargs, e); } /* If all the arguments were constant null, the result is just null */ if (newargs == NIL) return (Node *) makeNullConst(coalesceexpr->coalescetype, -1, coalesceexpr->coalescecollid); newcoalesce = makeNode(CoalesceExpr); newcoalesce->coalescetype = coalesceexpr->coalescetype; newcoalesce->coalescecollid = coalesceexpr->coalescecollid; newcoalesce->args = newargs; newcoalesce->location = coalesceexpr->location; return (Node *) newcoalesce; } ... if (IsA(node, PlaceHolderVar) &&context->estimate) { /* * In estimation mode, just strip the PlaceHolderVar node altogether; * this amounts to estimating that the contained value won't be forced * to null by an outer join. In regular mode we just use the default * behavior (ie, simplify the expression but leave the PlaceHolderVar * node intact). */ PlaceHolderVar *phv = (PlaceHolderVar *) node; return eval_const_expressions_mutator((Node *) phv->phexpr, context); } /* * For any node type not handled above, we recurse using * expression_tree_mutator, which will copy the node unchanged but try to * simplify its arguments (if any) using this routine. For example: we * cannot eliminate an ArrayRef node, but we might be able to simplify * constant expressions in its subscripts. */ return expression_tree_mutator(node, eval_const_expressions_mutator, (void *) context); }
Použití iterátorů a mutátorů se snížil počet chyb v PostgreSQL, které byly způsobeny zavedením nové struktury typu Node. Na druhou stranu, použití mutátorů zvyšuje paměťovou náročnost - dochází k intenzivnímu kopírování dat, přičemž k uvolňování paměti dochází až v okamžiku ukončení procesu - např. po vytvoření prováděcího plánu, po dokončení zpracování SQL příkazu.
Seznamy a základní operace se seznamy v PostgreSQL
Implementace generického jednosměrného seznamu List umožňuje efektivní přidávání prvku na konec nebo začátek seznamu a iteraci přes prvky uložené v seznamu. Jeho použití je nezvykle časté - viz parser (traduje se, že se jedná o pozůstatek z dob, kdy byl planner Postgres implementován v Lispu). Typ List podporuje interface Node a je jedním ze základních prvků komplexních datových struktur. Funkcí list_makeN inicializujeme seznam s N prvky. Funkce lappend slouží k přidání prvku na konec seznamu. V parseru, který je založen na gramatice zapsané v Bizonu (Yaccu) je typickou konstrukcí parsování seznamu hodnot oddělených čárkou:
alter_generic_option_list: alter_generic_option_elem { $$ = list_make1($1); } | alter_generic_option_list ',' alter_generic_option_elem { $$ = lappend($1, $3); } ;
K iteraci nad seznamem se používá makro foreach a makro lfirst.V následující ukázce dochází k transformaci seznamu. Pokud dohází k transformaci interních struktur, tak až výjimky nedochází k modifikaci originálních struktur. Originální data zůstávají - nedochází ani k jejich uvolňování. Alokovaná paměť se uvolňuje až ve chvíli, kdy je zrušen aktuální paměťový kontext. Např. při generování prováděcího plánu je vytvořen dočasný paměťový kontext, který se stane aktuálním kontextem. Poté dojde k vytvoření prováděcího plánu. Následně se prováděcí plán zkopíruje (deep copy) do nadřazeného kontextu a aktuální kontext ze uvolní. Tím automaticky dojde k uvolnění všech dočasných dat. Tento přístup k paměti je poměrně náročný - nicméně potřeba paměti je zanedbatelná vzhledem k spotřebě paměti při realizaci datových operací - sort, hashjoin, hashagg. Výhodou je čitelnější a jednodušší kód s menším pravděpodobností kritických chyb. Pozn: při optimalizaci extrémně velkých dotazů může dojít k použití virtuální paměti.
exprList = NIL; foreach(lc, selectQuery->targetList) { TargetEntry *tle = (TargetEntry *) lfirst(lc); Expr *expr; if (tle->resjunk) continue; if (tle->expr && (IsA(tle->expr, Const) ||IsA(tle->expr, Param)) && exprType((Node *) tle->expr) == UNKNOWNOID) expr = tle->expr; else { Var *var = makeVarFromTargetEntry(rtr->rtindex, tle); var->location = exprLocation((Node *) tle->expr); expr = (Expr *) var; } exprList = lappend(exprList, expr); }
Definice typů List a ListCell je následující:
typedef struct ListCell ListCell; typedef struct List { NodeTag type; /* T_List, T_IntList, or T_OidList */ int length; ListCell *head; ListCell *tail; } List; struct ListCell { union { void *ptr_value; int int_value; Oid oid_value; } data; ListCell *next; }; /* * The *only* valid representation of an empty list is NIL; in other * words, a non-NIL list is guaranteed to have length >= 1 and * head/tail != NULL */ #define NIL ((List *) NULL) static inline ListCell * list_head(List *l) { return l ? l->head : NULL; }
Důležité makro lfirst vrací obsah prvního prvku seznamu - parametrem je ukazatel na ListCell:
#define lfirst(lc) ((lc)->data.ptr_value)
Makro foreach zapouzdřuje deklaraci příkazu for:
/* * foreach - * a convenience macro which loops through the list */ #define foreach(cell, l) \ for ((cell) = list_head(l); (cell) != NULL; (cell) = lnext(cell))
Dále existují makra pro přístup k prvnímu prvku seznamu linitial, k druhému prvku seznamu lsecond atd (k poslednímu prvku seznamu llast).
Při rozvinutí maker může jednoduchá iterace nad prvky seznamu vypadat následovně:
List *l ; ListCell *lc; l = ..; for ((lc) = list_head(l); (lc) != NULL; (lc) = lnext(lc)) { Datum d = (Datum) ((lc)->data.ptr_value); ... }
K dispozici jsou všechny operace, které očekáváme od seznamu - vrácení n-tého prvku, oříznutí seznamu, sloučení dvou seznamů, rozdíl dvou seznamů, test zda-li je hodnota prvkem seznamu či nikoliv, funkce list_copy představuje tzv shallow copy (mělké kopírování - vytváří se pouze struktur obsahujících seznamu - List a ListCell, nedochází ke kopírování dat uložených v seznamu. Hluboké kopírování (deep copy) je možné pouze tehdy, když seznam obsahuje pouze Oid případně Int hodnoty - nebo ukazatele na varlena hodnoty.
Vyvolání výjimky
PostgreSQL interně používá vlastní systém výjimek založený na Longjmp a SetJmp. Starší funkce, kterou je možné použít k vyvolání výjimky je funkce elog. K dispozici je několik úrovní výjimek - varování (WARNING) a oznámení (NOTICE) nelze zachytit (zrovna tak nelze zachytit úrovně FATAL a PANIC:
if (!HeapTupleIsValid(tuple)) elog(ERROR, "cache lookup failed for language %u", langId);
Podstatně bohatší možnosti nabízí funkce ereport, která umožňuje do ve výjimce přenést SQLSTATE kód, detailní popis chyby, zjednodušenou nápovědu:
if (!rsi || !IsA(rsi, ReturnSetInfo) || (rsi->allowedModes & SFRM_Materialize) == 0 || rsi->expectedDesc == NULL) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set-valued function called in context that cannot accept a set")));
V případě, že výjimka není zachycena, tak je přeposlána na klienta. Ještě před tím dojde ke sběru informací o jednotlivých kontextech - simuluje se tím výpis zásobníku call-stack traceback. Pokud vývojář chce poskytnout informaci o některém kontextu procesu může si zaregistrovat callback funkci. Registrací callback funkcí se vytváří vázaný seznam obsahující adresy funkcí. Voláním callback funkcí v opačném pořadí než byly zaregistrovány získáme seznam informačních textů chronologicky popisující jednotlivé chybové (funkční) kontexty (od nejmladšího po nejstarší). Tuto callback funkci nelze použít pro uvolnění zdrojů - nemusí být vždy volána (pokud je výjimka zachycena, tak nemá cenu sbírat ladící informace). Uvnitř této funkce nelze přistupovat k datům v tabulkách - jelikož došlo k přerušení transakce a teprve se čeká na ROLLBACK.
static void plpsm_exec_error_callback(void *arg) { DebugInfo dinfo = (DebugInfo) arg; StringInfoData ds; if (dinfo->is_signal) return; initStringInfo(&ds); appendStringInfo(&ds, "PLPSM function \"%s\" Oid %d line %d\n\n", dinfo->module->code->name, dinfo->module->oid, pcode->lineno); errcontext("%s", ds.data); pfree(ds.data); }
Registrace a deregistrace kontext callback funkce:
Datum plpsm_func_execute(Plpsm_module *mod, FunctionCallInfo fcinfo) { ErrorContextCallback plerrcontext; DebugInfoData dinfo; Datum result; /* * Registrace callback funkce. Stávající errcontext se připojí k nově vytvořenému * chybovému kontextu, a nově vytvořený kontext se prohlásí za aktuální. */ plerrcontext.callback = plpsm_exec_error_callback; plerrcontext.arg = &dinfo; plerrcontext.previous = error_context_stack; error_context_stack = &plerrcontext; /* * zde probíhá zpracování funkce */ result = (Datum) ... /* * K výjimce nedošlo, stávající chybový kontext zaniká. Vracíme se k přechozímu * chybovému kontextu. */ error_context_stack = plerrcontext.previous; return result; }
Zachycení a ošetření výjimek
Výjimky jsou v PostgreSQL zachytávány primárně kvůli potřebě uvolnit "zdroje", které by v případě chyby nebyly uvolněny jiným mechanismem. Uvnitř PostgreSQL je každá zachycená výjimka po uvolnění zdrojů znovu vyvolána, přičemž absolutní zachycení výjimky je ošetřeno až na nejvyšší úrovni odvoláním aktuální transakce (přerušením zpracovávaného dotazu) a odesláním chybového hlášení uživateli. Pokud nedojde k odvolání aktuální transakce, tak zásadní interní datové struktury mohou být neinicializované nebo nevalidní. Jakákoliv trochu složitější operace přistupující k datům v db pak obvykle skončí pádem serveru.. Blok, kde lze zachytit výjimku vytvoříme pomocí maker PG_TRY, PG_CATCH a PG_END_TRY:
PG_TRY(); { saveResourceOwner = CurrentResourceOwner; ... } PG_CATCH(); { /* * Po chybě vracíme resourceOwner na původní hodnotu. */ CurrentResourceOwner = saveResourceOwner; /* * Přeposlání výjimky */ PG_RE_THROW(); } PG_END_TRY();
Tato makra jsou definována jako:
#define PG_TRY() \ do { \ sigjmp_buf *save_exception_stack = PG_exception_stack; \ ErrorContextCallback *save_context_stack = error_context_stack; \ sigjmp_buf local_sigjmp_buf; \ if (sigsetjmp(local_sigjmp_buf, 0) == 0) \ { \ PG_exception_stack = &local_sigjmp_buf #define PG_CATCH() \ } \ else \ { \ PG_exception_stack = save_exception_stack; \ error_context_stack = save_context_stack #define PG_END_TRY() \ } \ PG_exception_stack = save_exception_stack; \ error_context_stack = save_context_stack; \ } while (0)
Uvnitř bloku zachycení výjimky můžeme přistupovat k datové struktuře ErrorData, která drží strukturu výjimky. V tento okamžik je také možné zavolat callback kontextové funkce:
typedef struct ErrorData { int elevel; /* error level */ bool output_to_server; /* will report to server log? */ bool output_to_client; /* will report to client? */ bool show_funcname; /* true to force funcname inclusion */ bool hide_stmt; /* true to prevent STATEMENT: inclusion */ const char *filename; /* __FILE__ of ereport() call */ int lineno; /* __LINE__ of ereport() call */ const char *funcname; /* __func__ of ereport() call */ const char *domain; /* message domain */ int sqlerrcode; /* encoded ERRSTATE */ char *message; /* primary error message */ char *detail; /* detail error message */ char *detail_log; /* detail error message for server log only */ char *hint; /* hint message */ char *context; /* context message */ int cursorpos; /* cursor index into query string */ int internalpos; /* cursor index into internalquery */ char *internalquery; /* text of internally-generated query */ int saved_errno; /* errno at entry */ } ErrorData;
Data atuální výjimky jsou nedostupná, jsou uložená v lokální proměnné modulu, přičemž ovšem lze získat kopii dat voláním funkce CopyErrorData.
Pokud výjimka nebude opětovně vyvolána, je nezbytné použít subtransakci, která bude případně odvolána uvnitř obsloužení výjimky:
MemoryContext oldcontext = CurrentMemoryContext; ResourceOwner oldowner = CurrentResourceOwner; BeginInternalSubTransaction(NULL); /* Want to run statements inside function's memory context */ MemoryContextSwitchTo(oldcontext); PG_TRY(); { ... /* Commit the inner transaction, return to outer xact context */ ReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldcontext); CurrentResourceOwner = oldowner; /* * AtEOSubXact_SPI() should not have popped any SPI context, but * just in case it did, make sure we remain connected. */ SPI_restore_connection(); } PG_CATCH(); { ErrorData *edata; ListCell *e; /* Save error info */ MemoryContextSwitchTo(oldcontext); edata = CopyErrorData(); FlushErrorState(); RollbackAndReleaseCurrentSubTransaction(); MemoryContextSwitchTo(oldcontext); CurrentResourceOwner = oldowner; .... if (e == NULL) ReThrowError(edata); else FreeErrorData(edata); } PG_END_TRY();
Funkce FlushErrorState() čistí chybový zásobník - volá se tehdy, když jsou data výjimky zkopírována.
Deskriptor složeného typu TupleDesc
Pokud chceme přistupovat k položkám kompozitních typů, tak musíme znát (mít k dispozici) popis té či oné kompozitní hodnoty. Tento popis je v PostgreSQL uložen ve struktuře TupleDesc. Ačkoliv tato struktura není rekurzivní, umožňuje, spolu s dalšími mechanismy, pracovat s vnořenými (nested) kompozitními hodnotami. Popis kompozitní hodnoty obsahuje primárně počet položek a jejich typ. Kompozitní hodnoty mohou být dočasné (anonymní) nebo persistentní neanonymní.
CREATE TYPE o_type AS (a integer, b int); CREATE TABLE x(a int, b int);
Výše uvedené příkazy, kromě jiného, vytvořily neanonymní typy o_type a x. Na tyto identifikátory lze použít pří přetypování anonymní hodnoty:
postgres=# select (row(10,20)::x).*; a | b ----+---- 10 | 20 (1 row) postgres=# select (row(10,20)::o_type).*; a | b ----+---- 10 | 20 (1 row)
Struktura těchto typů je uložená v tabulkách pg_type a pg_attributte - čímž je zafixovaná - perzistentní. V PostgreSQL vznikají i dočasné kompozitní hodnoty - např. jako výsledek funkce s OUT proměnnými. U těchto dočasných hodnot vznikají metadata zároveň se vznikem dat, a po zániku dat zanikají i metadata. Protože tato data rychle vznikají a rychle zanikají, nepracuje se s systémovými tabulkami, ale přímo s cache.
Struktura TupleDesc (access/tupdesc.h) vypadá následovně:
typedef struct tupleDesc { int natts; /* number of attributes in the tuple */ Form_pg_attribute *attrs; /* attrs[N] is a pointer to the description of Attribute Number N+1 */ TupleConstr *constr; /* constraints, or NULL if none */ Oid tdtypeid; /* composite type ID for tuple type */ int32 tdtypmod; /* typmod for tuple type */ bool tdhasoid; /* tuple has oid attribute in its header */ int tdrefcount; /* reference count, or -1 if not counting */ } *TupleDesc;
Kompozitní neanonymní typ má unikátní object identifier oid a je dohledatelný v systémové tabulce pg_type. Strukturu TupleDesc na základě oid typu vytvoří funkce lookup_rowtype_tupdesc(rectuptyp, rectuptypmod);. Tuto funkci můžeme použít i pro anonymní typy - typ RECORD.
static TupleDesc lookup_rowtype_tupdesc_internal(Oid type_id, int32 typmod, bool noError) { if (type_id != RECORDOID) { /* * It's a named composite type, so use the regular typcache. */ TypeCacheEntry *typentry; typentry = lookup_type_cache(type_id, TYPECACHE_TUPDESC); if (typentry->tupDesc == NULL && !noError) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("type %s is not composite", format_type_be(type_id)))); return typentry->tupDesc; } else { /* * It's a transient record type, so look in our record-type table. */ if (typmod < 0 || typmod >= NextRecordTypmod) { if (!noError) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("record type has not been registered"))); return NULL; } return RecordCacheArray[typmod]; } }
V případě typu RECORD je významná položka typmod. Na základě této položky se dohledává deskriptor v cache. Funkce, která vytvoří kompozitní hodnotu, která se dále distribuuje pod typem RECORD musí zaregistrovat deskriptor této složené hodnoty. Když se pak kompozit zpracovává, tak se přistupuje k této zaregistrované hodnotě. Registraci do cache zajišťuje funkce BlessTupleDesc - např. pokud funkce vrací kompozitní hodnotu typu RECORD, tak musí volat funkci BlessTupleDesc:
TupleDesc BlessTupleDesc(TupleDesc tupdesc) { if (tupdesc->tdtypeid == RECORDOID && tupdesc->tdtypmod < 0) assign_record_type_typmod(tupdesc); return tupdesc; /* just for notational convenience */ }
Kompozitní deskriptor získáme posloupností kroků:
HeapTupleHeader rec = PG_GETARG_HEAPTUPLEHEADER(0); rectuptyp = HeapTupleHeaderGetTypeId(rec); rectuptypmod = HeapTupleHeaderGetTypMod(rec); rectupdesc = lookup_rowtype_tupdesc(rectuptyp, rectuptypmod);
Typ HeapTupleHeader slouží jako interface mezi C a SQL (používá se jako abstrakce pro typ RECORD a ROW typy v SQL prostoru).
Naopak při vytváření kompozitní hodnoty musíme provést:
HeapTupleHeader result; resultTupleDesc = CreateTemplateTupleDesc(2, false); TupleDescInitEntry(resultTupleDesc, (AttrNumber) 1, "file_name", TEXTOID, -1, 0); TupleDescInitEntry(resultTupleDesc, (AttrNumber) 2, "file_offset", INT4OID, -1, 0); resultTupleDesc = BlessTupleDesc(resultTupleDesc); ... /* * Tuple jam: Having first prepared your Datums, then squash together */ resultHeapTuple = heap_form_tuple(resultTupleDesc, values, isnull); /* * We cannot return tuple->t_data because heap_form_tuple allocates it as * part of a larger chunk, and our caller may expect to be able to pfree * our result. So must copy the info into a new palloc chunk. */ result = (HeapTupleHeader) palloc(resultHeapTuple->t_len); memcpy(result, resultHeapTuple->t_data, resultHeapTuple->t_len); heap_freetuple(resultHeapTuple); ReleaseTupleDesc(resultTupleDesc); PG_RETURN_HEAPTUPLEHEADER(result);
Poznámka: HeapTupleHeader se používá, pokud pracujeme s individuálními kompozitními hodnotami (explicitně s typem RECORD). Uvnitř funkcí, které vrací tabulku můžeme používat úspornější zápis:
tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls); result = HeapTupleGetDatum(tuple); PG_RETURN_DATUM(result);
Tato konvence není příliš pochopitelná - ve skutečnosti je, ve velké většině případů, HeapTupleHeader částí HeapTuple - nedohledal jsem, proč pro RECORD se používá HeapTupleHeader a pro ostatní případy - interní funkce, SRF funkce, se používá HeapTuple.
Pro konverzi z typu HeapTuple na typ HeapTupleHeader lze použít funkci SPI_returntuple (za předpokladu, že se používá SPI API).
Poznámka 2: TupleDesc se při separaci položek z HeapTuple použije k určení pořadí položek a jejich typů (na jejich základě je určeno zarovnávání v paměti). Vlastní separace položek probíhá sekvenčně - z bloku dat se přečte vždy n bajtů, zjistí se obsah a velikost, a pokračuje se následujícími m bajty.
TupleDesc se používá i v případě, že funkce vrací kompozitní typ (případně množinu kompozitních typů). Tehdy je připravený TupleDesc výsledku a lze jej získat voláním funkce get_call_result_type:
estate.rtinfo = plpgpsm_make_typeinfo_typeid(&estate, rettypeid, -1); if (func->parser->noutargs > 1) get_call_result_type(fcinfo, NULL, &estate.rettupdesc); else estate.rettupdesc = NULL;
V případě, že funkce vrací množinu, pak je návratový TupleDesc dostupný pomocí struktury ReturnSetInfo:
plpgsql_estate_setup(&estate, func, (ReturnSetInfo *) fcinfo->resultinfo); ... ReturnSetInfo *rsi = estate->rsi; ... estate->rettupdesc = rsi->expectedDesc;