章 54. 编写一个过程语言处理器

所有对用不是当前"版本 1"接口(用于编译型语言)语言编写的函数(这包括用户定义的过程语言中的函数、SQL 编写的函数以及使用版本 0 编译型语言接口的函数)的调用都会流经一个用于指定语言的调用处理器。调用处理器负责以一种有意义的方式执行该函数,例如通过解释所提供的源文本。本章勾勒了如何编写一个新的过程语言调用处理器的轮廓。

一个过程语言的调用处理器是一个"正常的"函数,它必须以一种编译型语言(如 C)编写、使用版本-1接口并且在PostgreSQL中注册为无参数且返回类型language_handler。这种特殊的伪类型标识该函数是一个调用处理器并且阻止它在 SQL 命令中被直接调用。关于 C 语言调用惯例和动态载入的更多细节,请见第 36.9 节

调用处理器的调用方式和其他任何函数相同:它接收一个包含参数值和有关被调用函数信息的FunctionCallInfoData 结构,并且它被期望返回一个Datum结果(并且如果它希望返回一个 SQL 空值结果,它可能设置FunctionCallInfoData结构的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))
    {
        /*
         * Called as a trigger procedure
         */
        TriggerData    *trigdata = (TriggerData *) fcinfo->context;

        retval = ...
    }
    else
    {
        /*
         * Called as a function
         */

        retval = ...
    }

    return retval;
}

要完成该调用处理器,只需要加入几千行代码来替代点号即可。

在将处理器函数编译成一个可载入模块(第 36.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 参数影响的检查绝对应当被跳过。

如果一个过程语言提供了一个内联处理器,它必须被声明为一个采用一个单一internal类型参数的函数。内联处理器的结果会被忽略,因此它通常被声明为返回void。当一个DO语句被调用执行指定过程语言时,内联处理器将被调用。实际被传递的参数是一个指向一个InlineCodeBlock结构的指针,它包含有关DO语句参数的而信息,特别是将被执行的匿名代码块的文本。内联处理器应该执行该代码并返回。

我们推荐你包装所有这些函数声明,以及CREATE LANGUAGE命令本身到一个extension中,这样一个简单的CREATE EXTENSION命令就足以安装该语言。关于编写扩展的信息请见第 36.15 节

在尝试编写你自己的语言处理器时,包括在标准发布中的过程语言是很好的参考。看看源码树中的src/pl子目录。CREATE LANGUAGE参考页也有一些有用的细节。