3. 扩展开发入门
PostgreSQL 的扩展机制允许开发者把自定义函数、类型、操作符、索引访问方法等能力以“可安装模块”的形式挂载到数据库实例中。与直接修改内核源码相比,扩展开发更安全、更易分发,也更容易随版本升级而维护。本节从一个最小可用的 C 语言扩展出发,完整走通“写源码 → 编译成共享库 → 编写控制文件 → 安装到数据库 → 调用验证”的闭环,并重点解释 PG_MODULE_MAGIC、函数声明、PG_FUNCTION_INFO_V1 这三个几乎每个扩展都会出现的要素。
一、扩展的最小构成
一个可被 CREATE EXTENSION 安装的 PostgreSQL 扩展,通常至少包含三类文件:
- C 源码文件:实现具体逻辑,例如
add_one.c,编译后生成共享库add_one.so(Windows 下为add_one.dll)。 - 控制文件:
add_one.control,描述扩展的默认版本、模块路径、注释等元信息,是CREATE EXTENSION的入口。 - SQL 脚本文件:
add_one--1.0.sql,声明扩展对外暴露的 SQL 对象,例如CREATE FUNCTION。
这三者分工明确:C 代码负责“算”,控制文件负责“被识别”,SQL 脚本负责“被注册”。缺少任意一个,扩展都无法通过标准方式安装。
二、PG_MODULE_MAGIC:让服务器认识你的模块
PostgreSQL 在加载一个共享库时,会先检查库中是否存在一个名为 Pg_magic_func 的符号,并校验其返回的魔数结构是否与当前服务器 ABI 兼容。这个检查由宏 PG_MODULE_MAGIC 自动完成。它的作用是防止把为不同大版本、不同编译选项构建的扩展错误地加载进当前实例,从而避免内存布局不一致导致的崩溃。
因此,在每个扩展的 C 源文件中,必须且只能出现一次 PG_MODULE_MAGIC;,通常放在 #include "postgres.h" 之后、其他函数定义之前。它不产生运行时逻辑,但它是模块被服务器“接纳”的通行证。
三、函数声明与 PG_FUNCTION_INFO_V1
PostgreSQL 调用 C 函数时,并不直接使用普通的 C 调用约定,而是通过 Datum 作为统一的值载体,并借助 FunctionCallInfo 传递参数、空值标记和上下文。为了让服务器知道某个 C 函数应当以“版本 1”的调用约定被调用,需要在源码中使用 PG_FUNCTION_INFO_V1(funcname); 宏。它会为该函数生成一个 pg_finfo_funcname 符号,服务器在解析 CREATE FUNCTION 时据此确认调用接口。
与之配套的是函数定义形式:返回类型和参数类型都写成 Datum,并通过 PG_GETARG_*、PG_RETURN_* 系列宏完成取值与返回。下面是一个最小示例,实现整数加一:
#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(add_one);
Datum
add_one(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}
这里 PG_FUNCTION_ARGS 展开为 FunctionCallInfo fcinfo,PG_GETARG_INT32(0) 取出第 0 个参数并转换为 int32,PG_RETURN_INT32 把结果包装成 Datum 返回。注意:PG_FUNCTION_INFO_V1 必须与函数定义在同一编译单元中,且函数名大小写要与 SQL 中声明的一致(PostgreSQL 默认会把未加引号的标识符折叠为小写)。
四、控制文件与 SQL 脚本
控制文件 add_one.control 内容非常简短:
comment = 'add one to an integer'
default_version = '1.0'
module_pathname = '$libdir/add_one'
relocatable = true
其中 module_pathname 指向共享库在安装目录中的位置,$libdir 由 PostgreSQL 在安装时替换为实际的库目录。relocatable = true 表示扩展对象可以随 ALTER EXTENSION ... SET SCHEMA 迁移。
SQL 脚本 add_one--1.0.sql 负责把 C 函数注册为数据库函数:
CREATE FUNCTION add_one(integer)
RETURNS integer
AS 'MODULE_PATHNAME', 'add_one'
LANGUAGE C STRICT IMMUTABLE;
这里的 'MODULE_PATHNAME' 会被控制文件中的 module_pathname 替换,第二个字符串 'add_one' 是共享库中导出的 C 符号名。STRICT 表示任一参数为 NULL 时函数不执行、直接返回 NULL;IMMUTABLE 表示相同输入永远得到相同输出,便于优化器做常量折叠。
五、编译与安装流程
最省事的方式是使用 PostgreSQL 提供的 PGXS 构建框架。在扩展目录下放置一个 Makefile:
MODULES = add_one
EXTENSION = add_one
DATA = add_one--1.0.sql
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
随后执行:
make
sudo make install
make 会调用与当前 pg_config 匹配的编译器和头文件,生成 add_one.so;make install 会把共享库复制到 $libdir,把控制文件和 SQL 脚本复制到 share/extension。安装完成后,在数据库中执行:
CREATE EXTENSION add_one;
SELECT add_one(41);
若返回 42,说明整条链路已经打通。若报错 could not open extension control file,通常是控制文件未安装到 share/extension;若报错 undefined symbol,则多半是 SQL 脚本中的 C 符号名与源码不一致,或共享库未正确安装。
六、常见问题与判断依据
第一次写扩展时,最容易踩的坑集中在符号与路径上。可以用下面的对照快速定位:
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| could not open extension control file | 控制文件未安装到 share/extension | 检查 make install 是否成功、PG_CONFIG 是否指向目标实例 |
| undefined symbol: add_one | SQL 脚本中的 C 符号名与源码不一致 | 核对 PG_FUNCTION_INFO_V1 与 AS 后第二个字符串 |
| incompatible library version | 共享库与服务器 ABI 不匹配 | 确认 PG_MODULE_MAGIC 存在,且用同一 pg_config 编译 |
| 函数返回 NULL 而非预期值 | 声明了 STRICT 但传入了 NULL | 确认调用参数非空,或去掉 STRICT 并在函数内处理空值 |
add_one 这类纯计算函数跑通全流程,再逐步引入 PG_GETARG_TEXT_PP、PG_RETURN_TEXT_P 等处理可变长类型,最后再尝试返回集合、触发器函数或自定义类型。每增加一种参数类型,都要同步检查 SQL 声明与 C 宏是否匹配。
掌握这条最小链路后,后续无论是写性能监控函数、自定义聚合,还是实现索引访问方法,都只是在同一套骨架里替换函数体与 SQL 声明。真正需要反复确认的,始终是模块魔数、调用约定宏、符号名与安装路径这四件事。