章 52. 书写一个过程语言处理器

调用函数的时候,如果函数的书写语言不是目前的"版本-1"的编译语言接口 (这包括用户定义的过程语言写的函数,用SQL写的函数,以及用版本0的编译语言接口写的函数),都会通过一个调用处理器处理具体的语言。 调用处理器有责任用一种有意义的方法执行这个函数,比如说解析所提供的文本等等。 本章简介如何书写一个新的过程语言调用处理器。

过程语言的调用处理器是一个"普通"的函数,必须使用一种编译语言来写,比如 C ,使用版本-1的接口,并且在PostgreSQL里注册成接受零个参数并且返回类型为language_handler。 这个特殊的伪类型标识该函数为一个调用处理器并且避免它直接在 SQL 命令中被调用。 关于C语言调用规范以及动态加载的更过细节请参考第 35.9 节

调用处理器的调用方式和其它函数一样:它接受一个指向一个 FunctionCallInfoData struct的指针, 这个指针包含参数值和有关被调用的函数的信息,并且预期它返回一个Datum结果(如果它希望返回一个 SQL 的空结果,那么可能设置 isnull 字段)。 调用处理器和普通的被调函数的区别是 FunctionCallInfoData 结构的 flinfo->fn_oid 字段包含实际要调用的函数的 OID ,而不是调用处理器自身。 调用处理器必须使用这个字段判断要执行哪个函数。通常,传递进来的参数列表也是按照目标函数的声明设置的,而不是给调用处理器设置的。

从系统表pg_proc里抓取函数入口以及分析被调函数的参数和返回类型就是调用处理器的事了。 来自CREATE FUNCTION命令中的AS 子句将会在pg_proc 行的 prosrc 字段中找到。 这个通常是过程语言本身的源文本,但也可以是别的东西,比如一个指向某个文件的路径名,或者任何告诉调用处理器如何详细处理的东西。

通常,每个 SQL 语句里面可能要调用同一个函数多次。 调用处理器可以利用flinfo->fn_extra字段避免重复地查找有关被调函数的信息。 这个字段初始为NULL,但是可以被调用处理器设置为指向有关被调函数的信息。 在随后的调用中,如果flinfo->fn_extra已经为非 NULL ,那么就可以直接使用它而免于重新查找信息。 调用处理器必须确保flinfo->fn_extra是用于指向一块至少会生存到当前查询结束的内存区里,因为一个FmgrInfo 数据结构将会保存那么长的时间。 一个实现这个要求的方法是在 flinfo->fn_mcxt声明的内存环境里分配一块额外的数据;这样的数据通常和 FmgrInfo 自己有一样的生命期。 但是处理器也可以同样选择使用一个更长生存期的环境,这样它就可以跨查询缓存函数定义。

在过程语言函数以触发器的形式调用的时候,就没有什么参数以通常的方式传递进来, 而是 FunctionCallInfoDatacontext 字段指向一个 TriggerData 结构,而不是像普通函数调用里面的 NULL那样。 一个语言处理器应该为过程语言函数提供获取触发器信息的机制。

下面是一个用 C 写的存储过程语言处理器的模版:

#include "postgres.h"
#include "executor/spi.h"
#include "commands/trigger.h"
#include "fmgr.h"
#include "access/heapam.h"
#include "utils/syscache.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(plsample_call_handler);

Datum
plsample_call_handler(PG_FUNCTION_ARGS)
{
    Datum          retval;

    if (CALLED_AS_TRIGGER(fcinfo))
    {
        /*
         * 作为触发器过程调用
         */
        TriggerData    *trigdata = (TriggerData *) fcinfo->context;

        retval = ...
    }
    else
    {
        /*
         * 作为函数调用
         */

        retval = ...
    }

    return retval;
}

在打点的地方放上几千行代码就可以完成调用处理器。

在把处理器函数编译成一个可加载的模块(参阅第 35.9.6 节)之后,下面的命令就可以注册这个例子过程语言:

CREATE FUNCTION plsample_call_handler() RETURNS language_handler
    AS 'filename'
    LANGUAGE C;
CREATE LANGUAGE plsample
    HANDLER plsample_call_handler;

尽管提供一个调用处理器对创建一个最小的过程语言已经足够了,但是还有另外两个可选的函数如果也能提供的话将会让这个语言更加容易使用。 它们是有效性验证函数内联处理器。 提供有效性验证函数将会允许CREATE FUNCTION时进行语言方面的检查。 提供内联处理器将会允许该语言支持通过DO命令执行的匿名代码块。

如果过程语言提供了有效性验证函数,必须将它声明为带单个oid类型参数的函数。 有效性验证函数的返回结果会被忽略,因此习惯上将其声明为返回void。 有效性验证函数在CREATE FUNCTION命令执行的末尾被调用,这时已经创建或者更新了一个使用这个过程语言书写的函数。 传入的OID是这个函数在pg_proc中对应行的OID。 有效性验证函数必须以通常的方式获取这一行并做一些恰当的检查。 首先,调用CheckFunctionValidatorAccess() 诊断对验证函数的明确调用,看看用户是否能通过CREATE FUNCTION实现。 通常的检查包括:确认函数的参数和结果类型是被该语言支持的,函数体是符合该语言语法的。 如果有效性验证函数发现函数是正确的,只需要返回就可以了。 如果发现了错误应该通过ereport()报告错误。 抛出一个错误将强行回滚事务并避免了不正确的函数定义被提交。

有效性验证函数通常应该遵循check_function_bodies参数的设置。 如果被设置为无效,任何昂贵的或依赖于上下文的检查应该被跳过。 如果语言在编译时提供了代码执行,那么验证函数必须抑制将要包含这种执行的检查。特别的, 这个参数会被pg_dump关闭,这样它就可以加载过程语言函数, 而不用担心副作用或者函数体对其他数据库中的对象有依赖关系了。 (因为这个要求,调用处理器不应该假定有效性验证函数已经做过了完整的检查。 有效性验证函数存在的目的并不是让调用处理器可以省略检查,而是为了在CREATE FUNCTION命令中包含明显的错误时可以立即通知用户) 精确的选择要检查哪些东西是由有效性验证函数决定的,注意当check_function_bodies有效时,核心的CREATE FUNCTION代码仅执行附加在函数上SET子句。 因此,当check_function_bodies无效时,很明显应该跳过哪些东西的结果将被GUC参数影响的检查,以避免加载dump时出现错误的失败。

如果过程语言提供了内联处理器,必须将它声明为带单个internal类型参数的函数。 内联处理器的返回结果会被忽略,因此习惯上将其声明为返回void。 当一个使用该语言的DO语句执行时,内联处理器将被调用。 实际传递的参数是一个InlineCodeBlock结构的指针。 这个结构包含了DO语句的参数信息,尤其是要执行的匿名代码块的文本。 内联处理器将执行这个代码并返回。

建议打包所有这些函数的声明以及CREATE LANGUAGE命令自身到一个扩展中, 这样只需要一个简单的CREATE EXTENSION命令就可以安装这个语言了。 关于书写扩展的方法,请参考第 35.15 节

如果想书写自己的调用处理器,那么包含在标准发布里面的过程语言是很好的例子。参考一下源代码树中的src/pl 子目录。 CREATE LANGUAGE参考页面中也有一些有用的信息。