Typ varchar simulující varchar z MySQL

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

17.3.2009, Autor: Pavel Stěhule

Pro vlastní testování doporučuji vycházet z contrib modulu citext.

V1 volající konvence

Správa funkcí (Function Management) je popsána v pgsql/src/backend/utils/fmgr/README.

Ačkoliv je PostgreSQL psané v C, interně pro SQL funkce nepoužívá C volající konvenci. C konvence je jednoduchá, rychlá a bohužel příliš jednoduchá - nedokáže přenášet hodnoty NULL, nedokáže přenést informace o parametrech. Proto se používá jiná volající konvence nazvaná v1. Teoreticky může v budoucnu existovat další volající konvence - aktuálně se ale žádná v2 a vyšší konvence nepoužívá a zatím se o ni neuvažuje. Funkce, která se volá ve v1 konvenci je taková funkce, která má jeden parametr fcinfo, což je ukazatel na strukturu FunctionCallInfoData (viz fmgr.h). Název parametru je důležitý - v makrech se s ním počítá:

typedef struct FunctionCallInfoData *FunctionCallInfo;

typedef Datum (*PGFunction) (FunctionCallInfo fcinfo);

/*
 * This struct holds the system-catalog information that must be looked up
 * before a function can be called through fmgr.  If the same function is
 * to be called multiple times, the lookup need be done only once and the
 * info struct saved for re-use.
 */
typedef struct FmgrInfo
{
        PGFunction      fn_addr;                /* pointer to function or handler to be called */
        Oid                     fn_oid;                 /* OID of function (NOT of handler, if any) */
        short           fn_nargs;               /* 0..FUNC_MAX_ARGS, or -1 if variable arg
                                                                 * count */
        bool            fn_strict;              /* function is "strict" (NULL in => NULL out) */
        bool            fn_retset;              /* function returns a set */
        unsigned char fn_stats;         /* collect stats if track_functions > this */
        void       *fn_extra;           /* extra space for use by handler */
        MemoryContext fn_mcxt;          /* memory context to store fn_extra in */
        fmNodePtr       fn_expr;                /* expression parse tree for call, or NULL */
} FmgrInfo;

/*
 * This struct is the data actually passed to an fmgr-called function.
 */
typedef struct FunctionCallInfoData
{
        FmgrInfo   *flinfo;                     /* ptr to lookup info used for this call */
        fmNodePtr       context;                /* pass info about context of call */
        fmNodePtr       resultinfo;             /* pass or return extra info about result */
        bool            isnull;                 /* function must set true if result is NULL */
        short           nargs;                  /* # arguments actually passed */
        Datum           arg[FUNC_MAX_ARGS];             /* Arguments passed to function */
        bool            argnull[FUNC_MAX_ARGS]; /* T if arg[i] is actually NULL */
} FunctionCallInfoData;

Přenášená datová struktura je poměrně komplikovaná - na druhou stranu, volaná funkce má k dispozici dostatek informací o parametrech, o návratové hodnotě - a lze se dopracovat i k volajícímu SQL příkazu.

#define PG_FUNCTION_ARGS        FunctionCallInfo fcinfo
#define PG_NARGS() (fcinfo->nargs)
#define PG_ARGISNULL(n)  (fcinfo->argnull[n])
#define PG_GETARG_DATUM(n)       (fcinfo->arg[n])

/* To return a NULL do this: */
#define PG_RETURN_NULL()  \
        do { fcinfo->isnull = true; return (Datum) 0; } while (0)

/* A few internal functions return void (which is not the same as NULL!) */
#define PG_RETURN_VOID()         return (Datum) 0
#define PG_RETURN_DATUM(x)       return (x)

Typ Datum který se přenáší v poli arg je polymorfní typ - buďto 4byte hodnota nebo 4byte ukazatel (na 64bit platformách 8byte). Je na volané funkci, aby se správně rozhodla, co vlastně parametr obsahuje. Volaná funkce má k dispozici i typy parametrů (identifikované skrze jejich oid).

O strukturu fcinfo se stará systém. V1 funkce lze volat přímo, ale snazší je nepřímé volání s použitím funkcí DirectFunctionCallX (viz pgsql/src/backend/utils/fmgr/fmgr.c). Vestavěné funkce nejsou polymorfní - předpokládá se, že se volají vždy se správnými parametry (o přetypování se opět stará systém). Proto není nutné nastavovat typy předávaných parametrů:

Datum
DirectFunctionCall2(PGFunction func, Datum arg1, Datum arg2)
{
        FunctionCallInfoData fcinfo;
        Datum           result;

        InitFunctionCallInfoData(fcinfo, NULL, 2, NULL, NULL);

        fcinfo.arg[0] = arg1;
        fcinfo.arg[1] = arg2;
        fcinfo.argnull[0] = false;
        fcinfo.argnull[1] = 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;
}

Volání funkcí z dynamických knihoven

Definice všech v1 vestavěných funkcí jsou k dispozici prostřednictvím definičního souboru pgsql/src/include/utils/bultins.h.

Kromě toho můžeme volat funkce prostřednictvím jejich oid. Tento způsob volání se používá pro volání funkcí z dynamických knihoven. Systémová tabulka pg_proc obsahuje informaci o oid funkce, její název, parametry a název binární knihovny. Její zavedení je pak starostí loaderu, prostřednictvím funkce fmgr_info. Programátor volá dynamické funkce se známým oid prostřednictvím funkce OidFunctionCallX:

Datum
OidFunctionCall2(Oid functionId, Datum arg1, Datum arg2)
{
        FmgrInfo        flinfo;
        FunctionCallInfoData fcinfo;
        Datum           result;

        fmgr_info(functionId, &flinfo);

        InitFunctionCallInfoData(fcinfo, &flinfo, 2, NULL, NULL);

        fcinfo.arg[0] = arg1;
        fcinfo.arg[1] = arg2;
        fcinfo.argnull[0] = false;
        fcinfo.argnull[1] = 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;
}

Registraci funkce (tj zápis do catalogu pg_proc) řeší příkaz CREATE FUNCTION. Tento příkaz se pokuší volat info funkci, která je skrytě vytvořená makrem:

#define CppConcat(x, y)			x##y

typedef struct
{
        int                     api_version;    /* specifies call convention version number */
        /* More fields may be added later, for version numbers > 1. */
} Pg_finfo_record;

/* Expected signature of an info function */
typedef const Pg_finfo_record *(*PGFInfoFunction) (void);

/*
 *      Macro to build an info function associated with the given function name.
 *      Win32 loadable functions usually link with 'dlltool --export-all', but it
 *      doesn't hurt to add PGDLLIMPORT in case they don't.
 */
#define PG_FUNCTION_INFO_V1(funcname) \
extern PGDLLIMPORT const Pg_finfo_record * CppConcat(pg_finfo_,funcname)(void); \
const Pg_finfo_record * \
CppConcat(pg_finfo_,funcname) (void) \
{ \
        static const Pg_finfo_record my_finfo = { 1 }; \
        return &my_finfo; \
} \

Pokud nepoužijeme makro PG_FUNCTION_INFO_V1, pak se bude systém pokoušet volat naši funkci klasickou c notací, což, zákonitě, musí skončit špatně.

Dohledání oid funkce na základě parametrů a jména provádí parser, konkrétně funkce func_get_detail (viz psql/src/backend/parser/parse_func.c):

FuncDetailCode
func_get_detail(List *funcname,
                                List *fargs,
                                int nargs,
                                Oid *argtypes,
                                bool expand_variadic,
                                bool expand_defaults,
                                Oid *funcid,    /* return value */
                                Oid *rettype,   /* return value */
                                bool *retset,   /* return value */
                                int *nvargs,    /* return value */
                                Oid **true_typeids,              /* return value */
                                List **argdefaults)              /* optional return value */

Pokud je výsledkem volání funkce hodnota FUNCDETAIL_NORMAL, pak výstupní parametry platí. Je ovšem na nás, abychom zajistili přetypování reálných parametrů do typů odpovídajících parametrům funkce (true_typeids).

Jednoduchá funkce napsaná ve v1 konvenci může vypadat následujícím způsobem:

Datum my_sum_int(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(my_sum_int);

Datum
my_sum_int(PG_FUNCTION_ARGS)
{
    int a = PG_GETARG_INT32(0);
    int b = PG_GETARG_INT32(1);
    int c;

    c = a + b;
    PG_RETURN_INT32(c);
}

Lokálně můžeme tuto funkci volat skrz DirectFunctionCall:

  int result;

  result = DatumGetInt32(DirectFunctionCall2(my_sum_int, 
                                                        Int32GetDatum(10),
                                                        Int32GetDatum(20));

Správa paměti

Každá funkce spouštěná z prostředí SQL má přidělený blok paměti tzv memory context v rámci tzv. Expr Eval Contextu. Po vyhodnocení výrazu se tato paměť blokově uvolňuje. Proto není chybou, když se explicitně neuvolňuje alokovaná paměť. To platí za předpokladu, že funkce "rozumně" alokuje paměť. Pokud by funkce měla vyšší paměťové nároky, pak je výhodnější vytvořit si odvozený paměťový kontext a s ním pak pracovat (případně jen explicitně uvolňovat významnější kusy paměti). V tomto případě se nejedná o memory leak.

bool
execTuplesMatch(TupleTableSlot *slot1,
                                TupleTableSlot *slot2,
                                int numCols,
                                AttrNumber *matchColIdx,
                                FmgrInfo *eqfunctions,
                                MemoryContext evalContext)
{
        MemoryContext oldContext;
        bool            result;
        int                     i;

        /* Reset and switch into the temp context. */
        MemoryContextReset(evalContext);
        oldContext = MemoryContextSwitchTo(evalContext);
        ....
        MemoryContextSwitchTo(oldContext);
      

Forwarding v1 funkcí

Při volání v1 funkcí můžeme využít existující proměnnou fcinfo. Ukázkou zapouzdření v1 funkce je následující kód. Obaluje input funkci typu varchar.

Datum myvarchar(PG_FUNCTION_ARGS)
{
   return bpcharin(fcinfo);
}

Tato technika se intenzivně používá např. v contrib modulu tsearch2, kdy se volání funkcí modulu převádí na volání integrovaných funkcí.

Input/Output functions

Input funkce transformuje vstup (cstring) do nativního PostgreSQL formátu. Output funkce je inverzní k input funkci. Prvním parametrem je zadaný text v c formátu (nulou ukončený řetězec). Druhým parametrem je oid typu, a třetím parametrem je typmod - modifikátor typu. Obvykle je input funkce označena jako STRICT, pokud není, tak první parametr může obsahovat hodnotu NULL. V tom případě musí vrátit NULL. Output funkce není aktivována pro hodnotu NULL. Pokud typmod není určen, pak třetí parametr nabývá hodnoty -1. Více v nápovědě k příkazu CREATE TYPE.

Typmodin, Typmodout funkce

Datové typy v SQL lze parametrizovat. Namátkou varchar(x), numeric(x[,x]). V PostgreSQL lze takto parametrizovat i vlastní datové typy. Každému datovému typu lze přiřadit tzv type modifier in funkci a type modifier out funkci. Do jisté míry jsou tyto funkce analogické vstupním a výstupním funkcím datového typu. Vstupem typmodin funkce je pole c řetězců (cstring). Výstupem je kladné celé číslo. Pro parsování klasických type modifikátorů lze použít funkci ArrayGetIntegerTypmods:

/* common code for bpchartypmodin and varchartypmodin */
static int32
anychar_typmodin(ArrayType *ta, const char *typename)
{
        int32           typmod;
        int32      *tl;
        int                     n;

        tl = ArrayGetIntegerTypmods(ta, &n);

        /*
         * we're not too tense about good error message here because grammar
         * shouldn't allow wrong number of modifiers for CHAR
         */
            .....
        /*
         * For largely historical reasons, the typmod is VARHDRSZ plus the number
         * of characters; there is enough client-side code that knows about that
         * that we'd better not change it.
         */
        typmod = VARHDRSZ + *tl;
        
        return typmod;
}

/* common code for bpchartypmodout and varchartypmodout */
static char *
anychar_typmodout(int32 typmod)
{
        char       *res = (char *) palloc(64);

        if (typmod > VARHDRSZ)
                snprintf(res, 64, "(%d)", (int) (typmod - VARHDRSZ));
        else
                *res = '\0';

        return res;
}

MySQL varchar

Při implementaci čehokoliv je dobré se poohlédnout, co můžeme použít. Při průzkumu implementace varcharu - interně typ varchar zjistíme, že vše, co potřebujeme, je již k dispozici. I ansi varchar dovede tiše ořezávat. Zkuste si:

postgres=# select 'abcd'::varchar(2);
┌─────────┐
│ varchar │
├─────────┤
│ ab      │
└─────────┘
(1 row)

První pokus

Teď jde jen o to, použít tuto vlastnost. K dispozici máme funkci varchar(), která zajistí zkrácení textu. V podstatě jedinou funkci, kterou musíme napsat je myvarcharin:

#include "postgres.h"

#include "fmgr.h"
#include "utils/builtins.h"

Datum mvarcharin(PG_FUNCTION_ARGS);

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(mvarcharin);

Datum
mvarcharin(PG_FUNCTION_ARGS)
{
        return DirectFunctionCall3(bpchar,
                                            DirectFunctionCall3(varcharin,
                                                                            PG_GETARG_DATUM(0),
                                                                            PG_GETARG_DATUM(1),
                                                                            Int32GetDatum(-1)),
                                            PG_GETARG_DATUM(2),         /* original typmod */
                                            BoolGetDatum(true));        /* explit casting, quite truncate */
}

Druhý pokus

Docela jednoduché. Ale hned při prvním testu zjistíme, že to nefunguje. Parsing jakýchkoliv konstant (vyjma typu interval) proběhne s typmodem rovno -1. Až po parsování se PostgreSQL pokusí nasadit typmod konverzí typ::typ(typmod). Proto potřebujeme ještě jednu funkci - nazveme si ji mvarchar s třemi parametry. První parametr je vlastní hodnota, druhý typmod a třetí flag, zda-li se jedná o explicitní nebo implicitní konverzi (viz dokumentace k příkazu CREATE CAST).

Datum mvarchar(PG_FUNCTION_ARGS);

PG_FUNCTION_INFO_V1(mvarchar);

Datum
mvarchar(PG_FUNCTION_ARGS)
{
        return DirectFunctionCall3(varchar,
                                        PG_GETARG_DATUM(0),
                                        PG_GETARG_DATUM(1),
                                        BoolGetDatum(true)); /* every time use rules for explicit cast */
}

Binární funkce musíme zaregistrovat (včetně nového datového typu). Registrací se vytvoří záznamy v systémových tabulkách pg_type a pg_proc. Minimální registrační soubor (mvarchar.sql.in) bude vypadat asi takto:

SET search_path = public;

CREATE OR REPLACE FUNCTION mvarcharin(cstring, oid, integer)
        RETURNS mvarchar
        AS 'MODULE_PATHNAME'
        LANGUAGE 'C'
        IMMUTABLE STRICT;

CREATE OR REPLACE FUNCTION mvarcharout(mvarchar)
        RETURNS cstring
        AS 'varcharout'
        LANGUAGE internal
        IMMUTABLE STRICT;

CREATE OR REPLACE FUNCTION mvarchar(mvarchar, int, bool)
        RETURNS mvarchar
        AS 'MODULE_PATHNAME'
        LANGUAGE 'C'
        IMMUTABLE STRICT;

CREATE TYPE mvarchar (
        INPUT = mvarcharin,
        OUTPUT = mvarcharout,
        LIKE = pg_catalog.varchar,
        typmod_in = pg_catalog.varchartypmodin,
        typmod_out = pg_catalog.varchartypmodout,
        CATEGORY = 'S',
        PREFERRED = false,
        INTERNALLENGTH = VARIABLE
);

CREATE CAST (mvarchar AS text)          WITHOUT FUNCTION AS IMPLICIT;
CREATE CAST (mvarchar AS varchar)       WITHOUT FUNCTION AS IMPLICIT;
CREATE CAST (mvarchar AS bpchar)        WITHOUT FUNCTION AS IMPLICIT;
CREATE CAST (bpchar AS mvarchar)        WITHOUT FUNCTION AS ASSIGNMENT;
CREATE CAST (varchar AS mvarchar)       WITHOUT FUNCTION AS ASSIGNMENT;
CREATE CAST (text AS mvarchar)          WITHOUT FUNCTION AS ASSIGNMENT;

CREATE CAST (mvarchar AS mvarchar)      WITH FUNCTION mvarchar(mvarchar, int, bool) AS IMPLICIT;

Tento soubor postačí pro testování. A i pro jednoduchý provoz. Jelikož z existujících CASTů je zřejmé, že typ je binárně kompatibilní s typem text, použijí se operace a podpora indexů pro typ text.

Závěr

V PostgreSQL není problém vytvořit libovolný datový typ, pro který platí, že výsledkem parsování je buďto validní hodnota nebo výjimka. Takže podobně by bylo možné vytvořit MySQL typ date. Tam by to už bylo asi pracnější. Museli bychom přepsat konverzní rutiny, které kontrolují validitu zadané hodnoty. Co zatím nejde je simulace Oraclovského varcharu, pro který platí, že prázdná hodnota odpovídá NULL. Bohužel implementace v Oraclu je natolik divoká, že se zatím snažíme o pouhé pochopení logiky.