Hacking PLpgSQL

Z PostgreSQL
Verze z 2. 3. 2016, 14:44, kterou vytvořil imported>Pavel
(rozdíl) ← Starší verze | zobrazit aktuální verzi (rozdíl) | Novější verze → (rozdíl)
Skočit na navigaci Skočit na vyhledávání

Autor: Pavel Stěhule, 2016

Zajímavou (a důležitou vlastností PostgreSQL je možnost docela jednoduše integrovat runtime programovacích jazyků a umožnit tak exekuci uložených procedur v dalších programovacích jazycích. Případně si vzít za vzor PL/pgSQL, a napsat si interpret vlastního programovacího jazyka. Pro pochopení problematiky je nutné rozumět způsobu, jakým PostgreSQL pracuje s funkcemi, tj rozumět volajícím konvencím, správě paměti (paměťovým kontextům) a datovým typům PostgreSQL (více v článku C a PostgreSQL - interní mechanismy. V následujícím článku se zaměřím na základní složky použití SQL uložených procedur v PostgreSQL.

Mechanizmus volání správného interpretu a předání parametrů

Po syntaktické analýze přichází na řadu analýza syntaktická - v terminologii PostgreSQL proces "analyze" nebo také "transformace". Pro tento článek je zajímavá etapa, kdy názvu funkce a seznamu parametrů je přiřazena konkrétní funkce ze systémového katalogu. Díky tomu, že Postgres umožňuje přetěžování funkcí a pracuje s typem unknown, tak tato úloha není úplně triviální. Hledá se nejlepší shoda mezi typy proměnných použitých jako parametry a mezi typy parametrů funkce viz kód funkcí transformFuncCall, ParseFuncOrColumn, FuncnameGetCandidates. Výsledkem může být Node typu FuncExpr, který již obsahuje funcid - tj oid funkce.

Dalším krokem je již exekuce - v přípravě se vytvoří stavová proměnná FuncExprState viz funkce init_fcache, která obsahuje odkaz na strukturu fmgr_info. Zde se na základě nastavení hodnoty prolang nastaví adresa funkce finfo->fn_addr. Pro interní funkce obsahuje skutečnou adresu, pro ostatní adresu handleru jazyka. Pomocí funkce FunctionCallInvoke je odkazovaná funkce (či handler) vykonána (vykonán). V případě zákaznických PL funkcí psaných v jiném jazyce než C, je vždy volán handler.

postgres=# select * from pg_language where lanname = 'plpgsql';
┌─[ RECORD 1 ]──┬──────────┐
│ lanname       │ plpgsql  │
│ lanowner      │ 10       │
│ lanispl       │ t        │
│ lanpltrusted  │ t        │
│ lanplcallfoid │ 13316    │
│ laninline     │ 13317    │
│ lanvalidator  │ 13318    │
│ lanacl        │ ( null ) │
└───────────────┴──────────┘

postgres=# select * from pg_proc where oid = 13316;
┌─[ RECORD 1 ]────┬──────────────────────┐
│ proname         │ plpgsql_call_handler │
│ pronamespace    │ 11                   │
│ proowner        │ 10                   │
│ prolang         │ 13                   │
│ procost         │ 1                    │
│ prorows         │ 0                    │
│ provariadic     │ 0                    │
│ protransform    │ -                    │
│ proisagg        │ f                    │
│ proiswindow     │ f                    │
│ prosecdef       │ f                    │
│ proleakproof    │ f                    │
│ proisstrict     │ f                    │
│ proretset       │ f                    │
│ provolatile     │ v                    │
│ proparallel     │ u                    │
│ pronargs        │ 0                    │
│ pronargdefaults │ 0                    │
│ prorettype      │ 2280                 │
│ proargtypes     │                      │
│ proallargtypes  │ ( null )             │
│ proargmodes     │ ( null )             │
│ proargnames     │ ( null )             │
│ proargdefaults  │ ( null )             │
│ protrftypes     │ ( null )             │
│ prosrc          │ plpgsql_call_handler │
│ probin          │ $libdir/plpgsql      │
│ proconfig       │ ( null )             │
│ proacl          │ ( null )             │
└─────────────────┴──────────────────────┘

Úkolem handleru je a) inicializovat runtime, b) načíst kód funkce z systémové cache (známe funcid), c) zkonvertovat vstupní parametry, d) provést kód s danými parametry, e) zkonvertovat výsledek a vrátit jej zpět Postgresu. Každý handler je zaregistrován v tabulce pg_language, kde jsou uložena funcid C čkové implementace handleru - funkcí volání, volání inline kódu (anonymní blok) a validace. Viz příkaz CREATE LANGUAGE. Implicitně je zaregistrován handler programovacího jazyka PL/pgSQL.

Extenze pro konverze datových typů mezi PostgreSQL a runtime programovacího jazyka

Datové typy v PostgreSQL a v prostředí PL nemusí být binárně kompatibilní (a vyjma PL/pgSQL také zpravidla nejsou). Proto jedním z úkolů handlerů je právě konverze parametrů funkce pro hostitelský jazyk a konverze výsledku do Postgresu. Relativně čerstvou novinkou (2015) je možnost dynamicky podporovat konverze typů dynamicky pomocí tzv transformačních extenzí (CREATE TRANSFORM). V klasickém ANSI SQL se transformační extenze používají aktivněji - jelikož tam částečně suplují funkcionalitu handleru. Naopak v PostgreSQL většinu práce provede handler, a transformační extenze se používají primárně pro datové typy, které jsou do PostgreSQL přidány ve formě extenze: ltree, hstore.

LOAD 'plperl';
SELECT NULL::hstore;


CREATE FUNCTION hstore_to_plperl(val internal) RETURNS internal
LANGUAGE C STRICT IMMUTABLE
AS 'MODULE_PATHNAME';

CREATE FUNCTION plperl_to_hstore(val internal) RETURNS hstore
LANGUAGE C STRICT IMMUTABLE
AS 'MODULE_PATHNAME';

CREATE TRANSFORM FOR hstore LANGUAGE plperl (
    FROM SQL WITH FUNCTION hstore_to_plperl(internal),
    TO SQL WITH FUNCTION plperl_to_hstore(internal)
);

Z důvodu výkonu a zachování kompatibility (do stávající aplikace lze přidat transformace bez obav z problémů se zpětnou kompatibilitou) musí být u každé funkce explicitně vyjmenované transformační extenze:

CREATE FUNCTION test2() RETURNS hstore
LANGUAGE plperl
TRANSFORM FOR TYPE hstore
AS $$
$val = {a => 1, b => 'boo', c => undef};
return $val;
$$;

SELECT test2();

Stále platí pravidlo, že se funkce píší pro konkrétní typy parametrů, a že se, až na malé výjimky, uvnitř funkce nepoužívá sebereflexe. Tj - u každé funkce vím, že N-tým parametrem je např. string nebo hash, který získám transformací ze stringu. Tuto transformaci zajistí systém. Úkolem vývojáře je napsat funkce pro první nebo druhou variantu - ale nelze v jedné variantě podporovat naráz několik různých typů jednoho parametru.

Historie PL/pgSQL

Autorem návrhu a originální implementace je Jan Wieck. První verze se objevila v PostgreSQL 6.4 v roce 1998. Návrh jazyka vychází z PL/SQL fy Oracle. Vlastní implementace je ovšem úplně jiná. Oracle převzal kompletní implementaci jazyka ADA - kterou následně mírně upravil a rozšířil o SQL. Jan napsal implementaci PL/pgSQL nad nástrojem pro tvorbu překladačů Bison (který již v Postgresu použitý bylee). Jeho implementace byla poměrně triviální - kód se přeložil do AST, a následně se AST interpretovalo. S embedded SQL se zacházelo jako s řetězcem - pokud se některé slovo v tomto řetězci shodovalo s názvem proměnné, tak došlo k náhradě slova parametrem. Neřešila se syntaxe SQL a ani platné umístění parametrů. Přes svoji jednoduchost v návrhu a jistá omezení se PL/pgSQL začal aktivně používat. Výhodou byla i značná podobnost s PL/SQL a skrze toto dědictví i s programovacím jazykem Ada (programovací jazyk, který je navržen s ohledem na použití v kritických aplikacích).

V dalších verzích se zlepšila kontrola embedded SQL. Se zásadním vylepšením PL/pgSQL přišla verze PostgreSQL 9.0, kdy se úplně přepracoval mechanismus, jakým se do SQL příkazů očkují proměnné PL/pgSQL. Původně se pracovalo s pouhou shodou textu s názvem proměnné v PL/pgSQL. Tento způsob ovšem neumožňoval identifikovat kolize identifikátorů SQL a PL/pgSQL a chybové hlášky z důvodu špatného umístění proměnných byly matoucí. Počínaje 9.0 o umístění proměnných rozhoduje SQL planner, který je napojen na runtime PL/pgSQL a má tudíž má informace o všech proměnných PL/pgSQL. Ve výchozím nastavení kolize identifikátorů vedou k vyvolání chyby.

DECLARE city_name text;
BEGIN
  FOR r IN SELECT city_name FROM cities; -- variable collision

Ve verzi 8.4, a následně ve verzi 9.0 došlo k dopracování možností jak funkci v PL/pgSQL předávat parametry. Vývojář může definovat své vlastní variadické funkce, může používat poziční i jmennou notaci předávání parametrů, může definovat výchozí hodnoty parametrů.

CREATE OR REPLACE FUNCTION foo(a int, b int DEFAULT 100) ..

SELECT foo(10,20); -- classic position notation
SELECT foo(b => 1000, a => 10); -- named notation
SELECT foo(10);  -- using default parameter
SELECT foo(10, b => 1000); -- mixed notation

Vývoj jazyka (prostředí) je poměrně klidný - respektuje se zpětná kompatibilita a statický charakter jazyka. Počínaje verzí 9.2 je k dispozici validátor embedded SQL - plpgsql_check.

Komponenty PLpgSQL, životní cyklus funkce

Zdrojový kód PL/pgSQL obsahuje několik souborů: pl_scanner.c (implementace scanneru s možností vracení (a opakovaného čtení) tokenů (push_back_token)), pl_handler.c (implementace funkcí handleru PL jazyka, definice GUC, inicializace pluginu), pl_funcs.c (evidence jmenných prostorů (proměnných, popisek (labels)) a vyhledávání v nich, serializace (zobrazení) AST a uvolnění AST z paměti), pl_compile.c (management cache AST - kód funkce je parsován pouze při prvním volání funkce (pokud nedojde ke změně kódu), inicializace automatických proměnných, příprava před voláním parseru a volání parsersu, identifikace typů a identifikace proměnných), pl_exec.c (implementace AST executoru a executoru výrazů) a gram.y (parsování PL/pgSQL, SQL, sestavení AST (Abstract Syntax Tree)).

Při vytvoření funkce se volá validátor (parser) a pokud je funkce validní, tak se uloží do záznamu v tabulce pg_proc. Při volání funkce se přečte záznam z pg_proc a opět se zavolá parser. Tentokrát ale AST zůstane persistentní v paměti procesu (session). Následně se volá executor, který se řídí AST, a pro každý uzel AST volá odpovídající implementaci:

/* ----------
 * exec_stmt_while          Loop over statements as long
 *                  as an expression evaluates to
 *                  true or an exit occurs.
 * ----------
 */
static int
exec_stmt_while(PLpgSQL_execstate *estate, PLpgSQL_stmt_while *stmt)
{
    for (;;)
    {
        int         rc;
        bool        value;
        bool        isnull;
    
        value = exec_eval_boolean(estate, stmt->cond, &isnull);
        exec_eval_cleanup(estate);
    
        if (isnull || !value)
            break;
        
        rc = exec_stmts(estate, stmt->body);
                            
        switch (rc)
        {
            case PLPGSQL_RC_OK:
                break;
    
            case PLPGSQL_RC_EXIT:
                if (estate->exitlabel == NULL)
                    return PLPGSQL_RC_OK;
                if (stmt->label == NULL)
                    return PLPGSQL_RC_EXIT;
                if (strcmp(stmt->label, estate->exitlabel) != 0)
                    return PLPGSQL_RC_EXIT;
                estate->exitlabel = NULL;
                return PLPGSQL_RC_OK;
                            
            case PLPGSQL_RC_CONTINUE:
                if (estate->exitlabel == NULL)
                    /* anonymous continue, so re-run loop */
                    break;
                else if (stmt->label != NULL &&
                         strcmp(stmt->label, estate->exitlabel) == 0)
                    /* label matches named continue, so re-run loop */
                    estate->exitlabel = NULL;
                else
                    /* label doesn't match named continue, propagate upward */
                    return PLPGSQL_RC_CONTINUE;
                break;
    
            case PLPGSQL_RC_RETURN:
                return rc;
        
            default:
                elog(ERROR, "unrecognized rc: %d", rc);
        }
    }
      
    return PLPGSQL_RC_OK;
}

Při prvním provedení výrazu nebo SQL příkazu se vytváří prováděcí plán, který se opakovaně používá do invalidace cache nebo do konce session.

static Datum
exec_eval_expr(PLpgSQL_execstate *estate,
               PLpgSQL_expr *expr,
               bool *isNull,
               Oid *rettype,
               int32 *rettypmod)
{
    Datum       result = 0;
    int         rc;

    /*
     * If first time through, create a plan for this expression.
     */
    if (expr->plan == NULL)
        exec_prepare_plan(estate, expr, 0);
   
    /*
     * If this is a simple expression, bypass SPI and use the executor
     * directly
     */
    if (exec_eval_simple_expr(estate, expr,
                              &result, isNull, rettype, rettypmod))
        return result;

Vyhodnocení výrazů a SQL příkazů v PLpgSQL

Implementace zpracování SQL příkazů v PL/pgSQL není jednoduchá a to z následujících důvodů: a) podpora jednoduchého vyhodnocení výrazů (většinu výrazů lze vyhodnotit pouze executorem, přičemž není nutné volat API pro SQL příkazy a tudíž vykonání výrazu je výrazně rychlejší), b) automatická invalidace plánů (při některých změnách databázových objektů dojde k invalidaci uložených prováděcích plánů, tak aby se předešlo nežádoucím artefaktům z důvodu odkazu na neexistující objekty v plánu), c) řízení vytváření a opakované použití prováděcích plánů.

Středobodem exekuce je funkce exec_eval_expr. Hlavními parametry je proměnná estate, která udržuje aktuální stav interpretu PLpgSQL (včetně odkazu na vektor proměnných a metadata funkce), a proměnná expr typu PLpgSQL_expr. Ta obsahuje jednak text výrazu nebo SQL příkazu a odkaz na perzistentní plán (pokud již existuje). První operací v exec_eval_expr je zajištění prováděcího plánu (volání funkce exec_prepare_plan). K této funkci se ještě vrátím - prováděcí plán pro prvních 5 iterací není persistentní - a logika generování adhoc plánů je implementována právě zde (resp. v SPI).

Dále následuje pokus o vyhodnocení v jednoduchém režimu (viz funkce exec_eval_simple_expr), a pokud je tento pokus úspěšný, okamžitě se vrátí výsledek. Výraz může být vyhodnocen přímo v tomto režimu pokud neobsahuje rekurzivní dotaz, jedná se pouze o výsledek evaluace (nikoliv scan) bez filtrů a subplánů. Výsledek musí obsahovat pouze jednu hodnotu, přičemž povolené jsou pouze vybrané Nodes - funkce exec_simple_check_plan. Kontrola, zda-li lze výraz vyhodnotit zjednodušeně se provádí pouze jednou. Výsledek je cacheován ve struktuře PLpgSQL_expr.

Pokud výraz nebo SQL příkaz nelze vyhodnotit zjednodušeně, přijde na řadu volání funkce exec_run_select, kde se připraví portál (kurzor - struktura pro získání výsledků SQL příkazů) a volá funkce SPI_execute_plan_with_paramlist.

Zpět k funkci exec_prepare_plan (potažmo SPI_prepare_params). Jedním ze obtížných problémů spojených s opakovaným použitím (reuse) prováděcích plánů je rozhodnutí, zda-li použít generický prováděcí plán (planner neví o skutečných hodnotách proměnných) nebo použít specifický plán (optimalizovaný pro jeden z vektorů parametrů). Generický prováděcí plán je v Postgresu optimalizovaný pro nejčastější hodnoty proměnných. Nesprávné použití generického plánu může vést k nechtěnému použití seq scanů (optimalizuje se na nejčastější hodnotu). V _SPI_prepare_params se volá funkce CreateCachedPlan a funkce CompleteCachedPlan. K plánu v cache se přistupuje voláním funkce GetCachedPlan, kde dochází k rozhodnutí jaký plán zvolit (fce: choose_custom_plan) - generický nebo zákaznický. Aktuálně se 5x vždy volá zákaznický plán - optimalizuje se pokaždé. Následně se vypočítá průměrná cena a porovná se s cenou generického plánu. Generický plán se použije, pokud je levnější než cena prúměrného plánu. Jakmile se jednou začne používat generický plán, už se používá stále. Toto řešení je citlivé na prvních 5 vektorů parametrů - a na to, jak cena dotazu koreluje s realitou (jak jsou věrohodné odhady). V krajním případě je nutné použít dynamické SQL.

Parametrizace SQL příkazů v PLpgSQL - použití proměnných PLpgSQL v embedded SQL

Specifickou vlastností PL/pgSQL je průnik dvou jmenných prostorů (SQL) a PL/pgSQL. SQL se v kódu PL/pgSQL může vyskytnout explicitně (embedded SQL) nebo implicitně (každý výraz). Parser PL/pgSQL dokáže identifikovat kód SQL příkazů a výrazy. Každý výraz je doplněný o klíčové slovo SELECT a převeden na standardní příkaz SELECT. Identifikovaný SQL příkaz se musí doplnit o placeholders "$x" všude tam, kde jsou použité PL/pgSQL proměnné. Při exekuci se použijí jako parametry SQL příkazu (prepared statement). Před verzí 9.0 se používala technika explicitní náhrady identifikátoru placeholdrem. Tj placeholder se objevil všude tam, kde se v řetězci SQL příkazu objevil podřetězec odpovídající identifikátoru proměnné.

Toto jednoduché řešení mělo několik nevýhod. 1) nebylo možné identifikovat kolize identifikátorů v PL/pgSQL a SQL. PL/pgSQL mělo vždy větší prioritu. Což vedlo k některým extrémně těžko odhalitelným problémům. 2) při špatném pozicování, případně nevhodném názvu proměnné došlo k syntaktické chybě - ta se ale obtížně interpretovala. Počínaje verzí 9.0 se tato úloha kompletně přepracovala. Nyní je řízena SQL analyzátorem, který se na všech místech, kde může být umístěn parametr plánu dotazuje hostitelského prostředí, zda-li existuje taková proměnná, a pokud ano, tak sám interně pracuje s placeholdrem. Zdrojový text SQL příkazu se již nemodifikuje. Díky tomu, že SQL analyzátor má přehled o SQL identifikátorech, je možné identifikovat a zablokovat kolize identifikátorů.

Pozn. parsování a analýza dotazu běží z hlediska PL/pgSQL executoru v jednom kroku - ve volání funkce SPI_prepare_params. Druhým parametrem této funkce odkaz na zákaznickou funkci, které se volá při inicializaci SQL parseru - plpgsql_parser_setup, kde se nastavují hooky p_pre_columnref_hook, p_post_columnref_hook a p_paramref_hook. V případě, že existuje vhodná proměnná, tak obsluha hooku vytvoří Node typu Param, který se uloží do AST dotazu (funkce make_datum_param). Zároveň se aktualizuje vektor parametrů výrazu (nebo SQL příkazu) paramnos.

Práce s proměnnými

PostgreSQL coby databáze nezná koncept proměnné, případně lokální proměnné. Veškerá práce s proměnnými je implementována v runtime PL/pgSQL. Každá funkce získá při startu stavovou proměnnou, která kromě jiného obsahuje i vektor všech proměnných. Proměnné se očíslují indexem v tomto vektoru - a později, při jejich použití se kopírují do vektoru parametrů SQL příkazu.

CREATE OR REPLACE FUNCTION public.foo(a integer)
 RETURNS integer
 LANGUAGE plpgsql
AS $function$ #option dump
declare b int; c int = 10;
begin
  b := a + 1;
  c := b + 1;
  return c;
end;
$function$ ;

Execution tree of successfully compiled PL/pgSQL function foo(integer):

Function's data area:
    entry 0: VAR $1               type int4 (typoid 23) atttypmod -1
    entry 1: VAR found            type bool (typoid 16) atttypmod -1
    entry 2: VAR b                type int4 (typoid 23) atttypmod -1
    entry 3: VAR c                type int4 (typoid 23) atttypmod -1

Function's statements:
  3:BLOCK <<*unnamed*>>
  4:  ASSIGN var 2 := 'SELECT a + 1'
  5:  ASSIGN var 3 := 'SELECT b + 1'
  6:  RETURN variable 3
    END -- *unnamed*

End of execution tree of function foo(integer)

V průběhu kompilace (vytváření AST) se plní dynamicky alokované pole proměnných, jejich tříd a typů. Na závěr kompilace (ve funkci plpgsql_finish_datums) je toto pole zkopírováno do struktury funkce (položky ndatums a datums). Odkazy na proměnné tříd ROW, REC a RECFIELD se vloží do množiny resettable_datums. Vektor proměnných estate.datums obsahuje všechny lokální proměnné funkce včetně lokálních kopií parametrů funkce (a v některých případech proměnné pro uložení výsledku). Základním abstraktním předkem pro uložení proměnných je PLpgSQL_datum (obsahuje dtype, dno). Z něj vychází PLpgSQL_variable (s přidanou položkou refname). Z PLpgSQL_variable vychází PLpgSQL_var, PLpgSQL_row, a PLpgSQL_rec.

typedef struct
{                               /* Scalar variable */
    int         dtype;
    int         dno;
    char       *refname;
    int         lineno;

    PLpgSQL_type *datatype;
    int         isconst;
    int         notnull;
    PLpgSQL_expr *default_val;
    PLpgSQL_expr *cursor_explicit_expr;
    int         cursor_explicit_argrow;
    int         cursor_options;

    Datum       value;
    bool        isnull;
    bool        freeval;
} PLpgSQL_var;

PLpgSQL_var obsahuje jak metadata, tak data. Při vstupu do bloku se inicializují proměnné deklarované v bloku - pole dno indexů initvarnos o velikosti n_initvars. Stavová kompozitní proměnná estate obsahuje i pomocnou strukturu ParamListInfo (položka ParamLI). Tato struktura obsahuje callback paramFetch, který v PLpgSQL obsahuje odkaz na funkce plpgsql_param_fetch. Zde dochází ke kopírování hodnot z vektoru proměnných do vektoru parametrů SQL příkazu.

Návrh a implementace vlastního SQL příkazu

SQL/PSM obsahuje příkaz REPEAT .... UNTIL podmínka END REPEAT. V PL/pgSQL stejně tak v jazyku ADA žádná taková konstrukce není (a nebude), ale můžeme si ji ze cvičných důvodů vymyslet.

REPEAT
 ...
 ...
UNTIL podminka END REPEAT;

Parser

Musíme zavést dvě nová rezervovaná slova REPEAT a UNTIL. PL/pgSQL parser zná pouze konstrukce PL/pgSQL a nemá zakódovanou podporu SQL příkazů a výrazů. Za výraz se považuje text za klíčovým slovem ukončený speciálním symbolem - často ';' nebo speciálním klíčovým slovem. Je výhodné, pokud se toto slovo nevyskytuje v SQL. Celý kód se postaví podle implementace příkazu WHILE. Dívám se, kde všude je implementován tento příkaz a analogicky přidávám kód pro REPEAT.

stmt_repeat     : opt_loop_label K_REPEAT proc_sect K_UNTIL expr_until_end K_REPEAT opt_label ';'
                    {
                        PLpgSQL_stmt_repeat *new;
                        
                        new = palloc0(sizeof(PLpgSQL_stmt_repeat));
                        new->cmd_type = PLPGSQL_STMT_REPEAT;
                        new->lineno   = plpgsql_location_to_lineno(@2);
                        new->label    = $1;
                        new->cond     = $5;
                        new->body     = $3;
                
                        check_labels($1, $7, @7);
                        plpgsql_ns_pop();
                        
                        $$ = (PLpgSQL_stmt *)new;
                    }
                ;

expr_until_end :
                    { $$ = read_sql_expression(K_END, "END"); }

V našem případě se END vyskytuje, nicméně je to podobná situace jako u příkazu IF.

postgres=# do $$ begin if case 1 when 1 then 1 end then raise notice 'kuku'; end if; end $$ language plpgsql;
ERROR:  42601: syntax error at end of input
LINE 1: do $$ begin if case 1 when 1 then 1 end then raise notice 'k...

-- zde pomohou závorky
postgres=# do $$ begin if (case 1 when 1 then 1 end) then raise notice 'kuku'; end if; end $$ language plpgsql;
NOTICE:  00000: kuku
LOCATION:  exec_stmt_raise, pl_exec.c:3165
DO

REPEAT musí být rezervované klíčové slovo, UNTIL nerezervované klíčové slovo. Vždy se snažíme omezovat nová rezervovaná klíčová slova, která mohou způsobit problémy se zpětnou kompatibilitou. Pozor - při definování klíčových slov (v pl_scanner.c) musíme zachovat abecední pořadí (v klíčových slovech se vyhledává půlením intervalu).

Executor

/* ----------
 * exec_stmt_repeat         Loop over statements as long
 *                  as an expression evaluates to
 *                  true or an exit occurs.
 * ----------
 */
static int
exec_stmt_repeat(PLpgSQL_execstate *estate, PLpgSQL_stmt_repeat *stmt)
{
    for (;;)
    {
        int         rc;
        bool        value;
        bool        isnull;
            
        rc = exec_stmts(estate, stmt->body);
        
        switch (rc)
        {
            case PLPGSQL_RC_OK:
                break;
            
            case PLPGSQL_RC_EXIT:
                if (estate->exitlabel == NULL)
                    return PLPGSQL_RC_OK;
                if (stmt->label == NULL)
                    return PLPGSQL_RC_EXIT;
                if (strcmp(stmt->label, estate->exitlabel) != 0)
                    return PLPGSQL_RC_EXIT;
                estate->exitlabel = NULL;
                return PLPGSQL_RC_OK;
                    
            case PLPGSQL_RC_CONTINUE:
                if (estate->exitlabel == NULL)
                    /* anonymous continue, so re-run loop */
                    break;
                else if (stmt->label != NULL &&
                         strcmp(stmt->label, estate->exitlabel) == 0)
                    /* label matches named continue, so re-run loop */
                    estate->exitlabel = NULL;
                else
                    /* label doesn't match named continue, propagate upward */
                    return PLPGSQL_RC_CONTINUE;
                break;
        
            case PLPGSQL_RC_RETURN:
                return rc;
                  
            default:
                elog(ERROR, "unrecognized rc: %d", rc);
        }
                        
        value = exec_eval_boolean(estate, stmt->cond, &isnull);
        exec_eval_cleanup(estate);
            
        if (!isnull && value)
            break;
    }
            
    return PLPGSQL_RC_OK;
}     

Ostatní

Podpora free a dumpu

free_repeat(PLpgSQL_stmt_repeat *stmt)
{
    free_expr(stmt->cond);
    free_stmts(stmt->body);
}

dump_repeat(PLpgSQL_stmt_repeat *stmt)
{
    dump_ind();
    printf("REPEAT\n");
    dump_stmts(stmt->body);
    dump_ind();
    printf("UNTIL ");
    dump_expr(stmt->cond);
    printf("\n");
            
    dump_ind();
    printf("    ENDREPEAT\n");
}

Nesmíme zapomenout na regresní testy:

--
-- test REPEAT
--
do $$
declare i int := 0;
begin
  repeat
    i = i + 1;
    raise notice '%', i;
  until i = 10 end repeat;
end;
$$;
NOTICE:  1
NOTICE:  2
NOTICE:  3
NOTICE:  4
NOTICE:  5
NOTICE:  6
NOTICE:  7
NOTICE:  8
NOTICE:  9
NOTICE:  10

Kompletní patch

diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 9415321..7b90f85
*** a/src/pl/plpgsql/src/pl_exec.c
--- b/src/pl/plpgsql/src/pl_exec.c
*************** static int exec_stmt_loop(PLpgSQL_execst
*** 149,154 ****
--- 149,156 ----
  			   PLpgSQL_stmt_loop *stmt);
  static int exec_stmt_while(PLpgSQL_execstate *estate,
  				PLpgSQL_stmt_while *stmt);
+ static int exec_stmt_repeat(PLpgSQL_execstate *estate,
+ 				PLpgSQL_stmt_repeat *stmt);
  static int exec_stmt_fori(PLpgSQL_execstate *estate,
  			   PLpgSQL_stmt_fori *stmt);
  static int exec_stmt_fors(PLpgSQL_execstate *estate,
*************** exec_stmt(PLpgSQL_execstate *estate, PLp
*** 1459,1464 ****
--- 1461,1470 ----
  			rc = exec_stmt_while(estate, (PLpgSQL_stmt_while *) stmt);
  			break;
  
+ 		case PLPGSQL_STMT_REPEAT:
+ 			rc = exec_stmt_repeat(estate, (PLpgSQL_stmt_repeat *) stmt);
+ 			break;
+ 
  		case PLPGSQL_STMT_FORI:
  			rc = exec_stmt_fori(estate, (PLpgSQL_stmt_fori *) stmt);
  			break;
*************** exec_stmt_while(PLpgSQL_execstate *estat
*** 1904,1909 ****
--- 1910,1978 ----
  	}
  
  	return PLPGSQL_RC_OK;
+ }
+ 
+ 
+ /* ----------
+  * exec_stmt_repeat			Loop over statements as long
+  *					as an expression evaluates to
+  *					true or an exit occurs.
+  * ----------
+  */
+ static int
+ exec_stmt_repeat(PLpgSQL_execstate *estate, PLpgSQL_stmt_repeat *stmt)
+ {
+ 	for (;;)
+ 	{
+ 		int			rc;
+ 		bool		value;
+ 		bool		isnull;
+ 
+ 		rc = exec_stmts(estate, stmt->body);
+ 
+ 		switch (rc)
+ 		{
+ 			case PLPGSQL_RC_OK:
+ 				break;
+ 
+ 			case PLPGSQL_RC_EXIT:
+ 				if (estate->exitlabel == NULL)
+ 					return PLPGSQL_RC_OK;
+ 				if (stmt->label == NULL)
+ 					return PLPGSQL_RC_EXIT;
+ 				if (strcmp(stmt->label, estate->exitlabel) != 0)
+ 					return PLPGSQL_RC_EXIT;
+ 				estate->exitlabel = NULL;
+ 				return PLPGSQL_RC_OK;
+ 
+ 			case PLPGSQL_RC_CONTINUE:
+ 				if (estate->exitlabel == NULL)
+ 					/* anonymous continue, so re-run loop */
+ 					break;
+ 				else if (stmt->label != NULL &&
+ 						 strcmp(stmt->label, estate->exitlabel) == 0)
+ 					/* label matches named continue, so re-run loop */
+ 					estate->exitlabel = NULL;
+ 				else
+ 					/* label doesn't match named continue, propagate upward */
+ 					return PLPGSQL_RC_CONTINUE;
+ 				break;
+ 
+ 			case PLPGSQL_RC_RETURN:
+ 				return rc;
+ 
+ 			default:
+ 				elog(ERROR, "unrecognized rc: %d", rc);
+ 		}
+ 
+ 		value = exec_eval_boolean(estate, stmt->cond, &isnull);
+ 		exec_eval_cleanup(estate);
+ 
+ 		if (!isnull && value)
+ 			break;
+ 	}
+ 
+ 	return PLPGSQL_RC_OK;
  }
  
  
diff --git a/src/pl/plpgsql/src/pl_funcs.c b/src/pl/plpgsql/src/pl_funcs.c
new file mode 100644
index 27ebebc..dc83102
*** a/src/pl/plpgsql/src/pl_funcs.c
--- b/src/pl/plpgsql/src/pl_funcs.c
*************** plpgsql_stmt_typename(PLpgSQL_stmt *stmt
*** 245,250 ****
--- 245,252 ----
  			return "LOOP";
  		case PLPGSQL_STMT_WHILE:
  			return "WHILE";
+ 		case PLPGSQL_STMT_REPEAT:
+ 			return "REPEAT";
  		case PLPGSQL_STMT_FORI:
  			return _("FOR with integer loop variable");
  		case PLPGSQL_STMT_FORS:
*************** static void free_if(PLpgSQL_stmt_if *stm
*** 343,348 ****
--- 345,351 ----
  static void free_case(PLpgSQL_stmt_case *stmt);
  static void free_loop(PLpgSQL_stmt_loop *stmt);
  static void free_while(PLpgSQL_stmt_while *stmt);
+ static void free_repeat(PLpgSQL_stmt_repeat *stmt);
  static void free_fori(PLpgSQL_stmt_fori *stmt);
  static void free_fors(PLpgSQL_stmt_fors *stmt);
  static void free_forc(PLpgSQL_stmt_forc *stmt);
*************** free_stmt(PLpgSQL_stmt *stmt)
*** 402,407 ****
--- 405,413 ----
  		case PLPGSQL_STMT_EXIT:
  			free_exit((PLpgSQL_stmt_exit *) stmt);
  			break;
+ 		case PLPGSQL_STMT_REPEAT:
+ 			free_repeat((PLpgSQL_stmt_repeat *) stmt);
+ 			break;
  		case PLPGSQL_STMT_RETURN:
  			free_return((PLpgSQL_stmt_return *) stmt);
  			break;
*************** free_while(PLpgSQL_stmt_while *stmt)
*** 528,533 ****
--- 534,546 ----
  }
  
  static void
+ free_repeat(PLpgSQL_stmt_repeat *stmt)
+ {
+ 	free_expr(stmt->cond);
+ 	free_stmts(stmt->body);
+ }
+ 
+ static void
  free_fori(PLpgSQL_stmt_fori *stmt)
  {
  	free_expr(stmt->lower);
*************** static void dump_if(PLpgSQL_stmt_if *stm
*** 756,761 ****
--- 769,775 ----
  static void dump_case(PLpgSQL_stmt_case *stmt);
  static void dump_loop(PLpgSQL_stmt_loop *stmt);
  static void dump_while(PLpgSQL_stmt_while *stmt);
+ static void dump_repeat(PLpgSQL_stmt_repeat *stmt);
  static void dump_fori(PLpgSQL_stmt_fori *stmt);
  static void dump_fors(PLpgSQL_stmt_fors *stmt);
  static void dump_forc(PLpgSQL_stmt_forc *stmt);
*************** dump_stmt(PLpgSQL_stmt *stmt)
*** 811,816 ****
--- 825,833 ----
  		case PLPGSQL_STMT_WHILE:
  			dump_while((PLpgSQL_stmt_while *) stmt);
  			break;
+ 		case PLPGSQL_STMT_REPEAT:
+ 			dump_repeat((PLpgSQL_stmt_repeat *) stmt);
+ 			break;
  		case PLPGSQL_STMT_FORI:
  			dump_fori((PLpgSQL_stmt_fori *) stmt);
  			break;
*************** dump_while(PLpgSQL_stmt_while *stmt)
*** 1027,1032 ****
--- 1044,1064 ----
  }
  
  static void
+ dump_repeat(PLpgSQL_stmt_repeat *stmt)
+ {
+ 	dump_ind();
+ 	printf("REPEAT\n");
+ 	dump_stmts(stmt->body);
+ 	dump_ind();
+ 	printf("UNTIL ");
+ 	dump_expr(stmt->cond);
+ 	printf("\n");
+ 
+ 	dump_ind();
+ 	printf("    ENDREPEAT\n");
+ }
+ 
+ static void
  dump_fori(PLpgSQL_stmt_fori *stmt)
  {
  	dump_ind();
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
new file mode 100644
index b14c22d..634e6f5
*** a/src/pl/plpgsql/src/pl_gram.y
--- b/src/pl/plpgsql/src/pl_gram.y
*************** static	void			check_raise_parameters(PLp
*** 176,182 ****
  %type <list>	decl_cursor_arglist
  %type <nsitem>	decl_aliasitem
  
! %type <expr>	expr_until_semi expr_until_rightbracket
  %type <expr>	expr_until_then expr_until_loop opt_expr_until_when
  %type <expr>	opt_exitcond
  
--- 176,182 ----
  %type <list>	decl_cursor_arglist
  %type <nsitem>	decl_aliasitem
  
! %type <expr>	expr_until_semi expr_until_rightbracket expr_until_end
  %type <expr>	expr_until_then expr_until_loop opt_expr_until_when
  %type <expr>	opt_exitcond
  
*************** static	void			check_raise_parameters(PLp
*** 196,202 ****
  %type <stmt>	stmt_return stmt_raise stmt_assert stmt_execsql
  %type <stmt>	stmt_dynexecute stmt_for stmt_perform stmt_getdiag
  %type <stmt>	stmt_open stmt_fetch stmt_move stmt_close stmt_null
! %type <stmt>	stmt_case stmt_foreach_a
  
  %type <list>	proc_exceptions
  %type <exception_block> exception_sect
--- 196,202 ----
  %type <stmt>	stmt_return stmt_raise stmt_assert stmt_execsql
  %type <stmt>	stmt_dynexecute stmt_for stmt_perform stmt_getdiag
  %type <stmt>	stmt_open stmt_fetch stmt_move stmt_close stmt_null
! %type <stmt>	stmt_case stmt_foreach_a stmt_repeat
  
  %type <list>	proc_exceptions
  %type <exception_block> exception_sect
*************** static	void			check_raise_parameters(PLp
*** 317,322 ****
--- 317,323 ----
  %token <keyword>	K_QUERY
  %token <keyword>	K_RAISE
  %token <keyword>	K_RELATIVE
+ %token <keyword>	K_REPEAT
  %token <keyword>	K_RESULT_OID
  %token <keyword>	K_RETURN
  %token <keyword>	K_RETURNED_SQLSTATE
*************** static	void			check_raise_parameters(PLp
*** 335,340 ****
--- 336,342 ----
  %token <keyword>	K_THEN
  %token <keyword>	K_TO
  %token <keyword>	K_TYPE
+ %token <keyword>	K_UNTIL
  %token <keyword>	K_USE_COLUMN
  %token <keyword>	K_USE_VARIABLE
  %token <keyword>	K_USING
*************** proc_stmt		: pl_block ';'
*** 863,868 ****
--- 865,872 ----
  						{ $$ = $1; }
  				| stmt_while
  						{ $$ = $1; }
+ 				| stmt_repeat
+ 						{ $$ = $1; }
  				| stmt_for
  						{ $$ = $1; }
  				| stmt_foreach_a
*************** stmt_while		: opt_loop_label K_WHILE exp
*** 1254,1259 ****
--- 1258,1281 ----
  					}
  				;
  
+ stmt_repeat		: opt_loop_label K_REPEAT proc_sect K_UNTIL expr_until_end K_REPEAT opt_label ';'
+ 					{
+ 						PLpgSQL_stmt_repeat *new;
+ 
+ 						new = palloc0(sizeof(PLpgSQL_stmt_repeat));
+ 						new->cmd_type = PLPGSQL_STMT_REPEAT;
+ 						new->lineno   = plpgsql_location_to_lineno(@2);
+ 						new->label	  = $1;
+ 						new->cond	  = $5;
+ 						new->body	  = $3;
+ 
+ 						check_labels($1, $7, @7);
+ 						plpgsql_ns_pop();
+ 
+ 						$$ = (PLpgSQL_stmt *)new;
+ 					}
+ 				;
+ 
  stmt_for		: opt_loop_label K_FOR for_control loop_body
  					{
  						/* This runs after we've scanned the loop body */
*************** expr_until_loop :
*** 2326,2331 ****
--- 2348,2356 ----
  					{ $$ = read_sql_expression(K_LOOP, "LOOP"); }
  				;
  
+ expr_until_end :
+ 					{ $$ = read_sql_expression(K_END, "END"); }
+ 
  opt_block_label	:
  					{
  						plpgsql_ns_push(NULL, PLPGSQL_LABEL_BLOCK);
*************** unreserved_keyword	:
*** 2457,2462 ****
--- 2482,2488 ----
  				| K_TABLE
  				| K_TABLE_NAME
  				| K_TYPE
+ 				| K_UNTIL
  				| K_USE_COLUMN
  				| K_USE_VARIABLE
  				| K_VARIABLE_CONFLICT
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
new file mode 100644
index bb0f25b..ba7a0d1
*** a/src/pl/plpgsql/src/pl_scanner.c
--- b/src/pl/plpgsql/src/pl_scanner.c
*************** static const ScanKeyword reserved_keywor
*** 84,89 ****
--- 84,90 ----
  	PG_KEYWORD("not", K_NOT, RESERVED_KEYWORD)
  	PG_KEYWORD("null", K_NULL, RESERVED_KEYWORD)
  	PG_KEYWORD("or", K_OR, RESERVED_KEYWORD)
+ 	PG_KEYWORD("repeat", K_REPEAT, RESERVED_KEYWORD)
  	PG_KEYWORD("strict", K_STRICT, RESERVED_KEYWORD)
  	PG_KEYWORD("then", K_THEN, RESERVED_KEYWORD)
  	PG_KEYWORD("to", K_TO, RESERVED_KEYWORD)
*************** static const ScanKeyword unreserved_keyw
*** 166,171 ****
--- 167,173 ----
  	PG_KEYWORD("table", K_TABLE, UNRESERVED_KEYWORD)
  	PG_KEYWORD("table_name", K_TABLE_NAME, UNRESERVED_KEYWORD)
  	PG_KEYWORD("type", K_TYPE, UNRESERVED_KEYWORD)
+ 	PG_KEYWORD("until", K_UNTIL, UNRESERVED_KEYWORD)
  	PG_KEYWORD("use_column", K_USE_COLUMN, UNRESERVED_KEYWORD)
  	PG_KEYWORD("use_variable", K_USE_VARIABLE, UNRESERVED_KEYWORD)
  	PG_KEYWORD("variable_conflict", K_VARIABLE_CONFLICT, UNRESERVED_KEYWORD)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
new file mode 100644
index f4e9f62..ed37e1b
*** a/src/pl/plpgsql/src/plpgsql.h
--- b/src/pl/plpgsql/src/plpgsql.h
*************** enum PLpgSQL_stmt_types
*** 104,109 ****
--- 104,110 ----
  	PLPGSQL_STMT_RETURN_NEXT,
  	PLPGSQL_STMT_RETURN_QUERY,
  	PLPGSQL_STMT_RAISE,
+ 	PLPGSQL_STMT_REPEAT,
  	PLPGSQL_STMT_ASSERT,
  	PLPGSQL_STMT_EXECSQL,
  	PLPGSQL_STMT_DYNEXECUTE,
*************** enum PLpgSQL_stmt_types
*** 115,121 ****
  	PLPGSQL_STMT_PERFORM
  };
  
- 
  /* ----------
   * Execution node return codes
   * ----------
--- 116,121 ----
*************** typedef struct
*** 477,482 ****
--- 477,492 ----
  
  
  typedef struct
+ {								/* REPEAT statements UNTIL cond 	*/
+ 	int			cmd_type;
+ 	int			lineno;
+ 	char	   *label;
+ 	PLpgSQL_expr *cond;
+ 	List	   *body;			/* List of statements */
+ } PLpgSQL_stmt_repeat;
+ 
+ 
+ typedef struct
  {								/* FOR statement with integer loopvar	*/
  	int			cmd_type;
  	int			lineno;
diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
new file mode 100644
index e30c579..51f1baa
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** end;
*** 5573,5575 ****
--- 5573,5597 ----
  $$;
  ERROR:  unhandled assertion
  CONTEXT:  PL/pgSQL function inline_code_block line 3 at ASSERT
+ --
+ -- test REPEAT
+ --
+ do $$
+ declare i int := 0;
+ begin
+   repeat
+     i = i + 1;
+     raise notice '%', i;
+   until i = 10 end repeat;
+ end;
+ $$;
+ NOTICE:  1
+ NOTICE:  2
+ NOTICE:  3
+ NOTICE:  4
+ NOTICE:  5
+ NOTICE:  6
+ NOTICE:  7
+ NOTICE:  8
+ NOTICE:  9
+ NOTICE:  10
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
new file mode 100644
index 7ffef89..acd030f
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** exception when others then
*** 4386,4388 ****
--- 4386,4402 ----
    null; -- do nothing
  end;
  $$;
+ 
+ --
+ -- test REPEAT
+ --
+ 
+ do $$
+ declare i int := 0;
+ begin
+   repeat
+     i = i + 1;
+     raise notice '%', i;
+   until i = 10 end repeat;
+ end;
+ $$;

Extenze pro PLpgSQL

Dlouho nebylo možné psát extenze pro PL/pgSQL. Teprve s větším rozšířením PostgreSQL a PL/pgSQL byla stále zjevnější potřeba debuggeru PL/pgSQL. Implementace debuggeru byla postavena na plugin API.

typedef struct
{
    /* Function pointers set up by the plugin */
    void        (*func_setup) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*func_beg) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*func_end) (PLpgSQL_execstate *estate, PLpgSQL_function *func);
    void        (*stmt_beg) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
    void        (*stmt_end) (PLpgSQL_execstate *estate, PLpgSQL_stmt *stmt);
    
    /* Function pointers set by PL/pgSQL itself */
    void        (*error_callback) (void *arg);
    void        (*assign_expr) (PLpgSQL_execstate *estate, PLpgSQL_datum *target,
                                            PLpgSQL_expr *expr);
} PLpgSQL_plugin;

Ukazatel na ukazatel na tuto strukturu je uložený v proměnné plugin_ptr, a inicializuje se prostřednictvím tzv randezvous proměnných (viz funkce find_rendezvous_variable("PLpgSQL_plugin")). Jistým omezením tohoto API je skutečnost, že v jeden moment může být aktivní pouze jeden plugin. Plugin API je nekomplikované, a evidentně zaměřené na ladění. Aktuálně existuje několik živých extenzí, které toto API používají - debugger, profiler a SQL checker.

static PLpgSQL_plugin plugin_funcs = { NULL, check_on_func_beg, NULL, NULL, NULL};

void
_PG_init(void)
{
    PLpgSQL_plugin ** var_ptr = (PLpgSQL_plugin **) find_rendezvous_variable( "PLpgSQL_plugin" );

    *var_ptr = &plugin_funcs;
  ..
}

Popis extenze PLpgSQL profiler

Korry Douglas, autor PL/pgSQL debuggeru, také napsal PL/pgSQL profiler. Před několika lety byla tato extenze revitalizována https://bitbucket.org/openscg/plprofiler. Tato extenze využívá plugin API. Nejvíce kódu v této extenzi se věnuje přepočtu naměřených časů z PL/pgSQL příkazů na řádky kódu PL/pgSQL funkce.

postgres=# CREATE EXTENSION plprofiler;
CREATE EXTENSION
postgres=# SELECT pl_profiler_enable(true);
┌────────────────────┐
│ pl_profiler_enable │
╞════════════════════╡
│ t                  │
└────────────────────┘
(1 row)

CREATE OR REPLACE FUNCTION public.test(integer)
 RETURNS void
 LANGUAGE plpgsql
AS $function$
declare s int = 0;
begin
  for i in 1..$1
  loop
    s := s + 1;
  end loop;
  raise notice '%', s;
end;
$function$

postgres=# SELECT * FROM pl_profiler;
┌──────────┬─────────────┬────────────────────────┬────────────┬────────────┬──────────────┐
│ func_oid │ line_number │          line          │ exec_count │ total_time │ longest_time │
╞══════════╪═════════════╪════════════════════════╪════════════╪════════════╪══════════════╡
│    18201 │           1 │                        │          0 │          0 │            0 │
│    18201 │           2 │ declare s int = 0;     │          0 │          0 │            0 │
│    18201 │           3 │ begin                  │          0 │          0 │            0 │
│    18201 │           4 │   for i in 1..$1       │          1 │       1536 │         1536 │
│    18201 │           5 │   loop                 │          0 │          0 │            0 │
│    18201 │           6 │     s := s + 1;        │       1000 │       1239 │           98 │
│    18201 │           7 │   end loop;            │          0 │          0 │            0 │
│    18201 │           8 │   raise notice '%', s; │          1 │        122 │          122 │
│    18201 │           9 │ end;                   │          0 │          0 │            0 │
└──────────┴─────────────┴────────────────────────┴────────────┴────────────┴──────────────┘
(9 rows)

Popis extenze plpgsql_check

Validace SQL příkazů a výrazů je v PL/pgSQL omezená na kontrolu syntaxe - již se nekontroluje, zda-li existuje použitý databázový objekt, případně zda-li má použité atributy. Důvodem je snaha o nezavedení nových závislostí mezi funkcemi a dalšími objekty v databázi. Není důležité jestli databázový objekt existuje v době vytvoření funkce, důležité je aby existoval v době vyhodnocení výrazu nebo SQL příkazu. Kromě výhody, že nemusíme řešit závislosti, to má i nepříjemné důsledky. Překlepy se identifikují až v době běhu, a pro některé větve výpočtu může být obtížné tyto chyby nalézt. Proto jsem napsal plpgsql_check, což byla původně jednoduchá extenze, která traverzovala po AST funkce, a pro každý SQL příkaz nebo výraz vynucovala vytvoření prováděcích plánů - potažmo kontrolu SQL identifikátorů. S rozšířením o performance kontroly a alespoň částečnou podporu typu RECORD se složitost extenze výrazně zvětšila.

Kontroly v plpgsql_checku mohou běžet v aktivním nebo v pasivním režimu. V aktivním režimu uživatel volá funkce plpgsql_check_function (výsledkem je text) nebo funkci plpgsql_check_function_tb (výsledkem je tabulka). V pasivním režimu se použije plugin API a zkontroluje se volaná funkce (resp AST volané funkce).

Středobodem extenze je funkce check_stmt, která obaluje masivní case, kterým se traverzuje přes AST a v každém uzlu se provádí odpovídající kontroly.

            case PLPGSQL_STMT_IF:
                {
                    PLpgSQL_stmt_if *stmt_if = (PLpgSQL_stmt_if *) stmt;
                    ListCell   *l;

                    check_expr_with_expected_scalar_type(cstate,
                                         stmt_if->cond, BOOLOID, true);
                    check_stmts(cstate, stmt_if->then_body);

                    foreach(l, stmt_if->elsif_list)
                    {
                        PLpgSQL_if_elsif *elif = (PLpgSQL_if_elsif *) lfirst(l);
   
                        check_expr_with_expected_scalar_type(cstate,
                                             elif->cond, BOOLOID, true);
                        check_stmts(cstate, elif->stmts);
                    }
   
                    check_stmts(cstate, stmt_if->else_body);
                }
                break;

Ukázka:

select * from plpgsql_check_function_tb('f1()', fatal_errors := false);
 functionid | lineno |   statement   | sqlstate |                  message                   | detail | hint | level | position |        query
------------+--------+---------------+----------+--------------------------------------------+--------+------+-------+----------+--------------------
 f1         |      4 | SQL statement | 42703    | column "c" of relation "t1" does not exist |        |      | error |       15 | update t1 set c = 3
 f1         |      7 | RAISE         | 42P01    | missing FROM-clause entry for table "r"    |        |      | error |        8 | SELECT r.c
(2 rows)