PostgreSQL SQL扩展 ---- C语言函数(三)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云原生内存数据库 Tair,内存型 2GB
简介: 可以用C(或者与C兼容,比如C++)语言编写用户自定义函数(User-defined functions)。这些函数被编译到动态可加载目标文件(也称为共享库)中并被守护进程加载到服务中。“C语言函数”与“内部函数”的区别就在于动态加载这个特性,二者的实际编码约定本质上是相同的(因此,标准的内部函数库为用户自定义C语言函数提供了丰富的示例代码)

八、返回数据集

C语言函数有两种返回数据集(多行数据)的方法:一种称为 ValuePerCall 方式,重复调用返回数据集的函数(每次调用传递相同的参数),每次调用返回一行,最后返回NULL表示没有更多的行。每次调用时,返回数据集的函数(SRF)必须保存足够多的状态以在整个调用中记住该做什么并且返回正确的下一条目。另一种称为 Materialize 方式,SRF填充并返回包含全部结果的tuplestore对象;一次调用获取全部结果,也无需保存内部调用状态。

在使用ValuePerCall模式时,需要注意查询可能不会执行完成;也就是说,由于LIMIT等选项,执行器可能会在获取完所有行之前停止调用返回值函数。这意味着在最后一个调用中执行清理操作是不安全的,因为这可能永远不会发生。对于需要访问外部资源(例如文件描述符)的函数,建议使用Materialize模式。

本节剩余部分介绍了一套常用助手宏(虽然不需要使用),这些宏用于ValuePerCall模式下的SRF(可视化数据查询)。有关Materialize模式的详细信息,请参见src/backend/utils/fmgr/README。此外,PostgreSQL源代码中的contrib模块包含许多使用ValuePerCall和Materialize模式的SRF示例。

要使用此处描述的ValuePerCall支持宏,请包括funcapi.h。这些宏与一个结构FuncCallContext配合使用,该结构包含跨调用需要保存的状态信息。在调用SRF中,fcinfo->flinfo->fn_extra用于在调用之间保持指向FuncCallContext的指针。宏会在第一次使用时自动填充该字段,并在后续使用时预期找到相同的指针。

typedef struct FuncCallContext
{
    /*
     * Number of times we've been called before
     *
     * call_cntr is initialized to 0 for you by SRF_FIRSTCALL_INIT(), and
     * incremented for you every time SRF_RETURN_NEXT() is called.
     */
    uint64 call_cntr;

    /*
     * OPTIONAL maximum number of calls
     *
     * max_calls is here for convenience only and setting it is optional.
     * If not set, you must provide alternative means to know when the
     * function is done.
     */
    uint64 max_calls;

    /*
     * OPTIONAL pointer to miscellaneous user-provided context information
     *
     * user_fctx is for use as a pointer to your own data to retain
     * arbitrary context information between calls of your function.
     */
    void *user_fctx;

    /*
     * OPTIONAL pointer to struct containing attribute type input metadata
     *
     * attinmeta is for use when returning tuples (i.e., composite data types)
     * and is not used when returning base data types. It is only needed
     * if you intend to use BuildTupleFromCStrings() to create the return
     * tuple.
     */
    AttInMetadata *attinmeta;

    /*
     * memory context used for structures that must live for multiple calls
     *
     * multi_call_memory_ctx is set by SRF_FIRSTCALL_INIT() for you, and used
     * by SRF_RETURN_DONE() for cleanup. It is the most appropriate memory
     * context for any memory that is to be reused across multiple calls
     * of the SRF.
     */
    MemoryContext multi_call_memory_ctx;

    /*
     * OPTIONAL pointer to struct containing tuple description
     *
     * tuple_desc is for use when returning tuples (i.e., composite data types)
     * and is only needed if you are going to build the tuples with
     * heap_form_tuple() rather than with BuildTupleFromCStrings().  Note that
     * the TupleDesc pointer stored here should usually have been run through
     * BlessTupleDesc() first.
     */
    TupleDesc tuple_desc;

} FuncCallContext;

使用此基础设施的SRF将使用的宏如下:

SRF_IS_FIRSTCALL() : 用于确定您的函数是首次被调用还是后续调用。在第一次调用(仅限于第一次)中,调用:

SRF_FIRSTCALL_INIT() 初始化FuncCallContext。 在每次函数调用中,包括第一次调用,请调用:

SRF_PERCALL_SETUP() 为使用FuncCallContext做好准备。

如果您的函数在当前调用中需要返回数据,则使用:

SRF_RETURN_NEXT(funcctx, result) 将其返回给调用者。(result 必须是 Datum 类型,可以是一个单个值或像上面描述的那样预处理的元组。)最后,当您的函数完成返回数据时,请使用:

SRF_RETURN_DONE(funcctx) 清理并结束 SRF。

当 SRF 被调用时,当前的内存上下文是一个短暂的上下文,将在每次调用之间被清除。这意味着您不需要为使用 palloc 分配的所有内容调用 pfree;无论如何它都会消失。但是,如果您想分配任何跨越调用的数据结构,则需要将其放在其他地方。multi_call_memory_ctx 引用的内存上下文是任何需要在 SRF 完成运行之前生存的数据的合适位置。 在大多数情况下,这意味着您应该在进行第一次调用设置时切换到 multi_call_memory_ctx。 使用 funcctx->user_fctx 存储指向任何跨调用数据结构的指针。(您在 multi_call_memory_ctx 中分配的数据将在查询结束时自动消失,因此无需手动释放这些数据。)

一个完整的伪代码示例如下:

Datum
my_set_returning_function(PG_FUNCTION_ARGS)
{
    FuncCallContext  *funcctx;
    Datum             result;
    further declarations as needed

    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext oldcontext;

        funcctx = SRF_FIRSTCALL_INIT();
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
        /* One-time setup code appears here: */
        user code
        if returning composite
            build TupleDesc, and perhaps AttInMetadata
        endif returning composite
        user code
        MemoryContextSwitchTo(oldcontext);
    }

    /* Each-time setup code appears here: */
    user code
    funcctx = SRF_PERCALL_SETUP();
    user code

    /* this is just one way we might test whether we are done: */
    if (funcctx->call_cntr < funcctx->max_calls)
    {
        /* Here we want to return another item: */
        user code
        obtain result Datum
        SRF_RETURN_NEXT(funcctx, result);
    }
    else
    {
        /* Here we are done returning items, so just report that fact. */
        /* (Resist the temptation to put cleanup code here.) */
        SRF_RETURN_DONE(funcctx);
    }
}

一个简单SRF返回复合类型的完整示例如下:

PG_FUNCTION_INFO_V1(retcomposite);

Datum
retcomposite(PG_FUNCTION_ARGS)
{
    FuncCallContext     *funcctx;
    int                  call_cntr;
    int                  max_calls;
    TupleDesc            tupdesc;
    AttInMetadata       *attinmeta;

    /* stuff done only on the first call of the function */
    if (SRF_IS_FIRSTCALL())
    {
        MemoryContext   oldcontext;

        /* create a function context for cross-call persistence */
        funcctx = SRF_FIRSTCALL_INIT();

        /* switch to memory context appropriate for multiple function calls */
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        /* total number of tuples to be returned */
        funcctx->max_calls = PG_GETARG_INT32(0);

        /* Build a tuple descriptor for our result type */
        if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
            ereport(ERROR,
                    (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                     errmsg("function returning record called in context "
                            "that cannot accept type record")));

        /*
         * generate attribute metadata needed later to produce tuples from raw
         * C strings
         */
        attinmeta = TupleDescGetAttInMetadata(tupdesc);
        funcctx->attinmeta = attinmeta;

        MemoryContextSwitchTo(oldcontext);
    }

    /* stuff done on every call of the function */
    funcctx = SRF_PERCALL_SETUP();

    call_cntr = funcctx->call_cntr;
    max_calls = funcctx->max_calls;
    attinmeta = funcctx->attinmeta;

    if (call_cntr < max_calls)    /* do when there is more left to send */
    {
        char       **values;
        HeapTuple    tuple;
        Datum        result;

        /*
         * Prepare a values array for building the returned tuple.
         * This should be an array of C strings which will
         * be processed later by the type input functions.
         */
        values = (char **) palloc(3 * sizeof(char *));
        values[0] = (char *) palloc(16 * sizeof(char));
        values[1] = (char *) palloc(16 * sizeof(char));
        values[2] = (char *) palloc(16 * sizeof(char));

        snprintf(values[0], 16, "%d", 1 * PG_GETARG_INT32(1));
        snprintf(values[1], 16, "%d", 2 * PG_GETARG_INT32(1));
        snprintf(values[2], 16, "%d", 3 * PG_GETARG_INT32(1));

        /* build a tuple */
        tuple = BuildTupleFromCStrings(attinmeta, values);

        /* make the tuple into a datum */
        result = HeapTupleGetDatum(tuple);

        /* clean up (this is not really necessary) */
        pfree(values[0]);
        pfree(values[1]);
        pfree(values[2]);
        pfree(values);

        SRF_RETURN_NEXT(funcctx, result);
    }
    else    /* do when there is no more left */
    {
        SRF_RETURN_DONE(funcctx);
    }
}

在SQL中声明此函数的一种方法是:

CREATE TYPE __retcomposite AS (f1 integer, f2 integer, f3 integer);

CREATE OR REPLACE FUNCTION retcomposite(integer, integer)
    RETURNS SETOF __retcomposite
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

另一种方法是使用OUT参数:

CREATE OR REPLACE FUNCTION retcomposite(IN integer, IN integer,
    OUT f1 integer, OUT f2 integer, OUT f3 integer)
    RETURNS SETOF record
    AS 'filename', 'retcomposite'
    LANGUAGE C IMMUTABLE STRICT;

需要注意的是,这种方法下函数的输出类型在形式上是一个匿名record 类型。

九、多态参数和返回类型

C语言函数可以声明接受和返回第38.2.5节中描述的多态类型。当一个函数的参数或返回值被定义为多态类型时,函数作者无法事先知道它将被什么数据类型调用,或者需要返回什么类型。fmgr.h 提供了两个例程,允许版本 1 的 C 函数发现其参数的实际数据类型以及它期望返回的类型。这些例程称为 get_fn_expr_rettype(FmgrInfo *flinfo)get_fn_expr_argtype(FmgrInfo *flinfo, int argnum)。它们返回结果或参数类型的 OID,或者如果信息不可用则返回 InvalidOid。结构 flinfo 通常通过 fcinfo->flinfo 访问。参数 argnum 是零基的。get_call_result_type 也可作为get_fn_expr_rettype 的替代方案使用。还有一种 get_fn_expr_variadic,它可以用于找出是否已将变长参数合并到数组中。这对于 VARIADIC "any" 函数非常有用,因为对于接受普通数组类型的变长函数,这种合并总是会发生的。

例如,假设我们要编写一个接受任何类型单个元素的函数,并返回该类型的单维数组:

PG_FUNCTION_INFO_V1(make_array);
Datum
make_array(PG_FUNCTION_ARGS)
{
    ArrayType  *result;
    Oid         element_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
    Datum       element;
    bool        isnull;
    int16       typlen;
    bool        typbyval;
    char        typalign;
    int         ndims;
    int         dims[MAXDIM];
    int         lbs[MAXDIM];

    if (!OidIsValid(element_type))
        elog(ERROR, "could not determine data type of input");

    /* get the provided element, being careful in case it's NULL */
    isnull = PG_ARGISNULL(0);
    if (isnull)
        element = (Datum) 0;
    else
        element = PG_GETARG_DATUM(0);

    /* we have one dimension */
    ndims = 1;
    /* and one element */
    dims[0] = 1;
    /* and lower bound is 1 */
    lbs[0] = 1;

    /* get required info about the element type */
    get_typlenbyvalalign(element_type, &typlen, &typbyval, &typalign);

    /* now build the array */
    result = construct_md_array(&element, &isnull, ndims, dims, lbs,
                                element_type, typlen, typbyval, typalign);

    PG_RETURN_ARRAYTYPE_P(result);
}

如下命令是在SQL中声明 make_array 函数:

CREATE FUNCTION make_array(anyelement) RETURNS anyarray
    AS 'DIRECTORY/funcs', 'make_array'
    LANGUAGE C IMMUTABLE;

有一种仅限于 C 语言函数的多态类型变体:它们可以声明接受名为"any"的类型的参数。(请注意,此类型名称必须用双引号括起来,因为它也是一个 SQL 保留字。)这类似于 anyelement,除了它不会约束不同的 "any" 参数具有相同的类型,也无法帮助确定函数的结果类型。C 语言函数还可以将最后一个参数声明为VARIADIC "any"。这将匹配一个或多个任何类型的实际参数(不一定相同)。这些参数将不会像普通的变长函数那样被收集到数组中;它们将单独传递给函数。使用 PG_NARGS() 宏和上述方法可以确定实际参数的数量及其类型,当使用此功能时。此外,这类函数的用户可能会在函数调用中使用 VARIADIC 关键字,期望该函数将处理数组元素作为单独的参数。如果需要,函数本身必须实现这种行为,并在使用 get_fn_expr_variadic 检测到实际参数标记为 VARIADIC 后执行。

十、共享内存和LWLock

插件可以在服务器启动时预留 LWLock 和共享内存分配。插件的共享库必须通过在 `shared_preload_libraries` 中指定它来提前加载。共享库应该在其 _PG_init 函数中注册一个 shmem_request_hook。这个 shmem_request_hook 可以预留 LWLock 或共享内存。共享内存是通过你`shmem_request_hook`调用:

void RequestAddinShmemSpace(int size)

LWLock 从你的shmem_request_hook调用:

void RequestNamedLWLockTranche(const char *tranche_name, int num_lwlocks)

这将确保在名为 `tranche_name` 的下,可用一个大小为 `num_lwlocks` 的 LWLocks 数组。使用GetNamedLWLockTranche获取指向此数组的指针。

可以在 PostgreSQL 源代码树中的 contrib/pg_stat_statements/pg_stat_statements.c 中找到一个 `shmem_request_hook` 示例。 为了避免可能的竞态条件,每个后台应在连接并初始化其共享内存分配时使用 LWLock AddinShmemInitLock,如下所示:

static mystruct *ptr = NULL;

if (!ptr)
{
    bool    found;

    LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
    ptr = ShmemInitStruct("my struct name", size, &found);
    if (!found)
    {
        initialize contents of shmem area;
        acquire any requested LWLocks using:
        ptr->locks = GetNamedLWLockTranche("my tranche name");
    }
    LWLockRelease(AddinShmemInitLock);
}

十一、在扩展中使用C++

尽管PostgreSQL后端是用C语言编写的,但只要遵循以下规则,也可以用C++编写扩展:

  • 所有要被后端调用的函数需提供一个C语言接口,后端通过这些C函数调用C++函数。比如:可被后端访问的函数必须加上 extern C 标志。任何在后端与C++代码间通过指针传递的函数都必须这样声明。
  • 使用合适的方法释放内存。比如:后端多使用 palloc()分配内存,因此要使用 pfree() 释放。此场景如果使用C++的 delete 则会失败。
  • 避免将异常传递到C代码 (在所有顶级extern C 函数中使用catch-all块)。即使C++代码没有显示的抛出任何异常这也是必要的,因为诸如内存不足(OOM)之类的事件会持续抛出异常。任何异常都必须被捕获并传递适当的错误到C接口。如果必要,编译C++代码时使用-fno-exceptions 选项以完全消除异常,在此种场景下,你必须检查C++代码中的错误,比如使用 new()后检查返回结果是否为NULL。
  • 如果从C++代码中调用后端函数,要确保C++调用栈中仅包含plain old data (POD)结构。这很必要,因为后端错误会生成一个长距离longjmp() ,它无法正确展开带有非POD对象的C++调用栈。

总之,供后端调用的C++接口函数要声明成 extern C ,并且要避免异常、内存、调用栈泄漏。

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
15天前
|
SQL 数据处理 数据库
|
15天前
|
SQL Oracle 关系型数据库
SQL 中的大小写处理函数详解
【8月更文挑战第31天】
32 0
|
15天前
|
SQL 数据采集 数据挖掘
为什么要使用 SQL 函数?详尽分析
【8月更文挑战第31天】
11 0
|
15天前
|
SQL 存储 关系型数据库
COALESCE 函数:SQL中的空值处理利器
【8月更文挑战第31天】
61 0
|
关系型数据库 分布式数据库 PolarDB
|
关系型数据库 分布式数据库 PolarDB
《阿里云产品手册2022-2023 版》——PolarDB for PostgreSQL
《阿里云产品手册2022-2023 版》——PolarDB for PostgreSQL
341 0
|
存储 缓存 关系型数据库
|
存储 SQL 并行计算
PolarDB for PostgreSQL 开源必读手册-开源PolarDB for PostgreSQL架构介绍(中)
PolarDB for PostgreSQL 开源必读手册-开源PolarDB for PostgreSQL架构介绍
392 0
|
存储 算法 安全
PolarDB for PostgreSQL 开源必读手册-开源PolarDB for PostgreSQL架构介绍(下)
PolarDB for PostgreSQL 开源必读手册-开源PolarDB for PostgreSQL架构介绍
361 0
|
关系型数据库 分布式数据库 开发工具