选择 打开 改范围 完整检索页
受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10
当前 PostgreSQL 版本不在支持生命周期内。
您可以参阅当前版本的对应页面,或其他在上面列出的活跃大版本。

Chapter 55. 编写过程语言处理器

凡是不是使用当前针对编译型语言的版本 1接口编写的函数,在被调用时都会经过该语言专用的调用处理器函数。这包括用户定义过程语言中的函数,以及用 SQL 编写的函数。调用处理器负责以恰当的方式执行该函数,例如解释所提供的源文本。本章概述如何编写新的过程语言调用处理器。

过程语言的调用处理器是一个普通函数,必须使用诸如 C 这样的编译型语言、按照版本 1 接口编写,并在 PostgreSQL 中注册为不接受参数且返回 language_handler 类型。这个特殊伪类型会将该函数标识为调用处理器,并阻止它在 SQL 命令中被直接调用。有关 C 语言调用约定和动态装载的更多细节,见 Section 37.9

调用处理器与其他函数的调用方式相同:它接收一个指向 FunctionCallInfoData struct 的指针,其中包含参数值和被调用函数的信息,并应返回一个 Datum 结果(如果要返回 SQL 空值,还可能需要设置 FunctionCallInfoData 结构的 isnull 字段)。调用处理器与普通被调用函数的区别在于,FunctionCallInfoData 结构的 flinfo->fn_oid 字段包含的是实际要调用的函数的 OID,而不是调用处理器自身的 OID。调用处理器必须使用该字段确定应执行哪个函数。此外,传入的参数列表也是按照目标函数的声明设置的,而不是按照调用处理器的声明。

调用处理器必须从系统目录 pg_proc 中取出该函数的条目,并分析被调用函数的参数类型和返回类型。该函数的 AS 子句可在其 CREATE FUNCTION 命令对应的 prosrc 列里找到,也就是 pg_proc 行中的该列。这里通常是一段过程语言源文本,但理论上也可以是其他内容,例如某个文件的路径名,或者任何能详细告诉调用处理器该做什么的信息。

同一个函数在执行一条 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;
}

只需在省略号处添加几千行代码,就能完成这个调用处理器。

将处理器函数编译为可加载模块后(参见 Section 37.9.5),可用以下命令注册示例过程语言:

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 命令存在明显错误时立即通知用户。)虽然具体检查哪些内容主要由验证器函数自行决定,但要注意,核心 CREATE FUNCTION 代码只有在 check_function_bodies 开启时,才执行函数附带的 SET 子句。因此,关闭 check_function_bodies 时,必须跳过那些结果可能受 GUC 参数影响的检查,以免重新装载转储时误报错误。

如果过程语言提供了内联处理器,它必须声明为一个接受单个 internal 参数的函数。内联处理器的返回值会被忽略,因此习惯上将其声明为返回 void。当执行指定了该过程语言的 DO 语句时,就会调用内联处理器。实际传入的参数是一个指向 InlineCodeBlock 结构体的指针,其中包含 DO 语句参数的相关信息,特别是待执行的匿名代码块文本。内联处理器应执行这段代码并返回。

建议你将以上所有函数声明以及 CREATE LANGUAGE 命令本身都封装进一个扩展中,这样只需执行一条简单的 CREATE EXTENSION 命令就足以安装该语言。关于如何编写扩展,见 Section 37.15

标准发行版中附带的各过程语言,在尝试编写自己的语言调用处理器时都是很好的参考资料。请查看源码树中的 src/pl 子目录。CREATE LANGUAGE 参考页中也包含一些有用的细节。