开发自定义扩展
前面五篇讲的都是「用别人的扩展」。当你的业务里出现一段反复复制粘贴的 SQL、一个用 PL/pgSQL 写得又慢又难维护的函数、或者需要访问内核内部状态(如缓冲区、WAL、执行器钩子)时,就该考虑把它打包成自己的扩展。
PostgreSQL 的扩展开发门槛比多数人想象的低。一个只含 SQL 对象的扩展,只需要两个纯文本文件:一个控制文件(.control)和一个安装脚本(.sql)。不需要编译、不需要 C 语言、不需要重启数据库。而当你确实需要性能或内核能力时,再往上叠加 C 函数与 PGXS 构建系统。
本篇按「控制文件 → SQL 脚本 → 安装验证 → 版本升级 → C 函数 → PGXS 构建 → 发布」这条完整链路展开,每一步都给出可直接落地的文件与命令。
前置知识
本篇假定你已读过 Citus 分布式扩展,理解扩展如何以「元数据 + 对象集合」的形式存在;同时强烈建议先读 CREATE EXTENSION 扩展管理,因为本篇涉及的控制文件属性(superuser / trusted / relocatable)与 pg_extension 目录表都在那一篇详述过。
基础概念
一个扩展由什么组成
按「必须 / 可选」划分,一个扩展的文件构成如下:
| 文件 | 是否必须 | 作用 |
|---|---|---|
<name>.control | 必须 | 扩展的元信息:默认版本、schema、权限属性、依赖 |
<name>--<version>.sql | 必须 | 安装脚本,包含所有 SQL 对象的 CREATE 语句 |
<name>--<v1>--<v2>.sql | 可选 | 版本升级脚本,从 v1 升到 v2 的增量 DDL |
<name>.so / <name>.dylib | 可选 | C 共享库,供 SQL 中的 MODULE_PATHNAME 引用 |
<name>.c / .h / Makefile | 可选 | 源码与构建脚本(仅开发时存在,不随安装分发) |
README / LICENSE | 推荐 | 说明与许可 |
安装后,前几项会被复制到 PostgreSQL 的 share/extension/ 与 lib/ 目录:
pg_config --sharedir # /usr/local/pgsql/share
pg_config --pkglibdir # /usr/local/pgsql/lib扩展的 .control 与 .sql 放在 $(pg_config --sharedir)/extension/,.so 放在 $(pg_config --pkglibdir)/。
控制文件(Control File)的字段
控制文件是纯文本,语法是 key = value,每行一个(# 开头是注释)。所有字段:
| 字段 | 默认 | 说明 |
|---|---|---|
comment | 无 | 扩展的一句话描述,显示在 pg_available_extensions.comment |
default_version | 无 | 默认安装版本,必须 |
directory | 扩展名 | 脚本所在子目录(相对于 share/extension) |
module_pathname | 无 | 替换脚本中 MODULE_PATHNAME 的路径 |
requires | 无 | 依赖的其他扩展名(逗号分隔) |
superuser | true | 是否只有超级用户能安装 |
trusted | false | 是否为可信扩展(PG13+,允许非超级用户安装) |
relocatable | false | 是否可用 ALTER EXTENSION SET SCHEMA 整体搬家 |
schema | 无 | 强制安装到指定 schema(设置后扩展不可 relocatable) |
encoding | 无 | 脚本文件编码 |
一个最小可用的控制文件:
# pg_slug.control
comment = 'URL-safe slug generation helpers'
default_version = '1.0'
relocatable = trueSQL 脚本的命名规则
脚本文件名严格遵循 <扩展名>--<版本>.sql:--1.0.sql 是从零安装 1.0 的安装脚本,--1.0--1.1.sql 是从 1.0 升到 1.1 的升级脚本。规则要点:
- PostgreSQL 会尝试找一条从当前版本到目标版本的升级路径(可以多步,如 1.0 → 1.1 → 2.0)。
- 若只提供
--1.0--2.0.sql(一步跨越),那么 1.1 无法升级到 2.0。 --<版本>.sql与升级脚本可以共存,PostgreSQL 优先用最短路径。
MODULE_PATHNAME、$libdir 与占位符
SQL 脚本里引用共享库时不要写死路径:
CREATE FUNCTION @extschema@.slugify(text) RETURNS text
AS 'MODULE_PATHNAME', 'pg_slugify'
LANGUAGE C IMMUTABLE STRICT;MODULE_PATHNAME 会被控制文件的 module_pathname 替换;若未设置,则默认替换为 $libdir/<扩展名>。这是让扩展在不同安装路径下都能工作的关键。也可以用显式 $libdir(多库共享时有用):
AS '$libdir/pg_slug', 'pg_slugify'脚本里的两个特殊占位符:
@extschema@:会被替换成扩展安装的 schema 名。写跨对象引用时必须用它,否则脚本可能在非public的 schema 下失败。@extowner@:会被替换成执行CREATE EXTENSION的角色名(PG13+),用于需要显式ALTER ... OWNER TO的场景。
CREATE EXTENSION 的内部行为
理解这一点能解释很多「奇怪现象」。官方文档说明,安装时 PostgreSQL 会在一个受限的搜索路径下执行脚本,等价于:
SET LOCAL search_path TO @extschema@, pg_temp;含义:
- 脚本执行时,当前 schema 是
@extschema@,pg_temp排第二。 - 脚本里没有 schema 限定的对象会创建到
@extschema@;引用的其他对象若不带 schema,可能解析到pg_temp或直接找不到。 - 用户自己的
search_path设置不会影响脚本执行。
另外,PostgreSQL 会扫描脚本创建的所有对象并把它们登记进 pg_depend,使 DROP EXTENSION 能级联清理。
relocatable 与 schema 的关系
relocatable = true 的前提是脚本里所有对象引用都用 @extschema@ 限定,且不依赖任何硬编码的 schema 名。推论:
- 设了
schema = foo的扩展不能是 relocatable(因为强制了位置)。 - 一旦脚本里出现
public.something,即使声明relocatable = true也可能在搬 schema 后出错。 ALTER EXTENSION name SET SCHEMA new_schema之所以能把所有对象一起搬,靠的是pg_depend里的e依赖记录。
trusted 扩展的编写约束
要让扩展能被普通用户安装(trusted = true),需要满足(PG13+):
- 控制文件里
superuser = false或trusted = true。 - 安装脚本不执行任何需要超级用户的动作(不创建
LANGUAGE C函数、不改参数、不写文件系统)。 - 脚本里的对象不能被滥用提权 —— 这是核心:纯 SQL/PL 语言、且所有函数正确设置
SECURITY INVOKER、不给不必要权限的扩展才适合 trusted。
一句话:trusted 是对「脚本安全性」的人工承诺,PostgreSQL 不会验证你的承诺。
语法与用法
从零写一个纯 SQL 扩展
以「生成 URL 安全 slug」为例,只需两个文件。
第一步,控制文件:
# pg_slug.control
comment = 'URL-safe slug generation helpers'
default_version = '1.0'
relocatable = true第二步,安装脚本:
-- pg_slug--1.0.sql
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION pg_slug" to load this file. \quit
CREATE FUNCTION @extschema@.slugify(txt text)
RETURNS text
AS $$
SELECT regexp_replace(
regexp_replace(lower(trim(txt)), '[^a-z0-9]+', '-', 'g'),
'(^-|-$)', '', 'g'
);
$$
LANGUAGE sql
IMMUTABLE
STRICT
PARALLEL SAFE;第三步,把两个文件复制到扩展目录并安装:
cp pg_slug.control pg_slug--1.0.sql "$(pg_config --sharedir)/extension/"CREATE EXTENSION pg_slug;
SELECT pg_slug.slugify('Hello PostgreSQL World!');
-- hello-postgresql-world若脚本里要调用其他扩展提供的函数(例如 unaccent),必须在控制文件里用 requires 声明依赖,否则解析会失败。
标准的脚本头部与尾部
PostgreSQL 自己维护的 contrib 扩展都遵循这套约定,强烈建议照抄:
-- 1. 头部:防止脚本被 psql 直接 source
\echo Use "CREATE EXTENSION my_ext" to load this file. \quit-- 2. 尾部:把配置表标记为「需要被 pg_dump 导出」
SELECT pg_catalog.pg_extension_config_dump('my_ext_config', '');第二条非常关键:默认情况下 pg_dump 只导出 CREATE EXTENSION 一行,不会导出扩展自己创建的表里的数据。如果扩展带「配置表」或「字典表」,必须调用 pg_extension_config_dump() 把它标为「配置表」,否则备份恢复后数据全丢。它还支持 WHERE 条件,用于「只备份部分行」:
-- 只 dump 未处理完的行(注意 SQL 字符串里的单引号要写成两个)
SELECT pg_catalog.pg_extension_config_dump('my_ext_queue', 'WHERE status <> ''done''');版本升级脚本
假设 1.0 只有 slugify,1.1 给 slugify 加一个可选参数。新建升级脚本(只改动部分):
-- pg_slug--1.0--1.1.sql
-- PostgreSQL 不支持修改函数参数列表,必须先 DROP 再建
DROP FUNCTION @extschema@.slugify(text);
CREATE FUNCTION @extschema@.slugify(txt text, max_len int DEFAULT NULL)
RETURNS text
AS $$
SELECT CASE WHEN max_len IS NULL THEN base ELSE left(base, max_len) END
FROM (SELECT regexp_replace(
regexp_replace(lower(trim(txt)), '[^a-z0-9]+', '-', 'g'),
'(^-|-$)', '', 'g') AS base) s;
$$
LANGUAGE sql IMMUTABLE PARALLEL SAFE; -- 注意:max_len 默认 NULL,不能标 STRICT同时在 --1.1.sql(从零安装 1.1)里放完整定义,并更新控制文件的 default_version = '1.1':
-- pg_slug--1.1.sql(片段)
CREATE FUNCTION @extschema@.slugify(txt text, max_len int DEFAULT NULL) ...;执行升级并查看可选版本:
ALTER EXTENSION pg_slug UPDATE;
-- 或指定版本
ALTER EXTENSION pg_slug UPDATE TO '1.1';
SELECT name, version, installed FROM pg_available_extension_versions
WHERE name = 'pg_slug' ORDER BY version;注意
DROP FUNCTION 会连带删除依赖它的对象(视图、其他函数)。大版本升级时若涉及 DROP,先确认依赖图,必要时用 CREATE OR REPLACE 保持签名不变。
声明依赖:requires
如果新函数依赖 unaccent:
comment = 'URL-safe slug generation helpers'
default_version = '1.1'
relocatable = true
requires = 'unaccent'CREATE EXTENSION pg_slug; 会要求 unaccent 已安装(否则报错),DROP EXTENSION pg_slug; 会因依赖被拒(除非 CASCADE)。查看扩展依赖:
SELECT objid::regprocedure AS dependent,
refobjid::regprocedure AS depends_on
FROM pg_depend
WHERE deptype = 'e'
AND refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pg_slug');安装检查与删除
CREATE EXTENSION pg_slug;
-- 检查文件被识别
SELECT name, default_version, comment FROM pg_available_extensions WHERE name = 'pg_slug';
-- 检查对象归属
SELECT e.extname, e.extversion, e.extnamespace::regnamespace AS schema, e.extrelocatable
FROM pg_extension e WHERE e.extname = 'pg_slug';查看扩展拥有的所有对象,并确认 deptype:
SELECT classid::regclass AS catalog, objid, deptype
FROM pg_depend
WHERE refclassid = 'pg_extension'::regclass
AND refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pg_slug')
ORDER BY catalog; catalog | objid | deptype
---------------------+-------+---------
pg_namespace | 16385 | n
pg_proc | 16386 | e
pg_proc | 16387 | edeptype = 'e' 表示「由扩展拥有」,DROP EXTENSION 会删掉这些对象:
DROP EXTENSION pg_slug; -- 有依赖会报错
DROP EXTENSION pg_slug CASCADE; -- 连带删依赖对象日常排查用 psql 元命令最快:
\dx -- 已安装扩展一览
\dx+ pg_slug -- 某扩展拥有的所有对象
\dx pg_slug -- 某扩展的版本与 schema
\df pg_slug.* -- 扩展内的函数详细说明
控制文件逐字段实战
一个「真实感」较强的控制文件,把可用字段都用上:
# myapp_ext.control
comment = 'MyApp internal helpers: audit, slug, soft-delete'
default_version = '2.3'
module_pathname = '$libdir/myapp_ext'
requires = 'pgcrypto'
superuser = true
trusted = false
relocatable = false
schema = extensionsmodule_pathname:所有AS 'MODULE_PATHNAME', ...会被替换成$libdir/myapp_ext。requires = 'pgcrypto':CREATE EXTENSION myapp_ext;会要求pgcrypto已存在,但不会自动安装 —— 这与CASCADE的行为不同。schema = extensions:强制装到extensions,同时意味着relocatable = false(位置被钉死)。superuser = true:约定了只有超级用户能装。
脚本里必须写全限定名
这是新手最容易踩的坑。看一个反例:
-- 错误示范:引用 public 下的表,且不带 schema
CREATE FUNCTION @extschema@.log_change() RETURNS trigger AS $$
BEGIN
INSERT INTO audit_log (tbl, op, at) VALUES (TG_TABLE_NAME, TG_OP, now());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;问题:脚本执行时 search_path 是 @extschema@, pg_temp,audit_log 若在 public 下就找不到;更糟的是可能解析到 pg_temp 下的同名临时表(这是 PostGIS 等扩展历史上出现过的一类安全问题)。正确写法:
CREATE FUNCTION @extschema@.log_change() RETURNS trigger AS $$
BEGIN
INSERT INTO @extschema@.audit_log (tbl, op, at)
VALUES (TG_TABLE_NAME, TG_OP, now());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;铁律:脚本里所有对象引用(表、函数、类型、操作符)都写全限定名,且 schema 部分用 @extschema@。
SET search_path 的写法
给函数设置 search_path 是防御 SQL 注入与解析歧义的标准做法。注意两点:
-- 不推荐:只设 public,可能被 pg_temp 劫持
CREATE FUNCTION @extschema@.safe_fn() RETURNS int
LANGUAGE sql SECURITY DEFINER
SET search_path = public
AS $$ SELECT 1 $$;
-- 推荐:显式含 pg_temp,且放在最后
CREATE FUNCTION @extschema@.safe_fn() RETURNS int
LANGUAGE sql SECURITY DEFINER
SET search_path = @extschema@, pg_temp
AS $$ SELECT 1 $$;SET search_path 出现在 CREATE FUNCTION 的属性列表里,会随函数定义一起存进 pg_proc.proconfig,每次调用时生效,不受调用者环境影响。
pg_extension_config_dump() 的语义
SELECT pg_catalog.pg_extension_config_dump('myapp_ext.config', '');它做了两件事:往 pg_extension 的 extconfig 数组里加入该表的 OID,往 extcondition 数组里加入对应条件(第二个参数)。pg_dump 看到 extconfig 里的表,就会额外导出它的数据(而非只导出 CREATE EXTENSION)。
| 场景 | 是否该标记 |
|---|---|
| 用户自定义配置(阈值、开关) | 标记(必须) |
| 缓存表、临时结果 | 不标记 |
| 只读字典(随扩展发版) | 不标记 |
| 用户创建的业务数据(恰好放在扩展 schema) | 不标记 |
extcondition 支持 WHERE 子句,用于「只备份未处理的行」这类场景:
SELECT pg_catalog.pg_extension_config_dump('myapp_ext.job_queue',
'WHERE status <> ''done''');C 函数的接口约定
写 C 函数需要遵守几条固定约定。
第一,声明模块魔数(每个 .c 文件一次):
#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;PG18 引入了带 ABI 信息的 PG_MODULE_MAGIC_EXT,可在其中声明扩展名与版本,便于自省(具体字段与用法以所用版本的官方文档为准)。
第二,声明函数可见性,并用三件套签名:
PG_FUNCTION_INFO_V1(pg_add_one);
Datum pg_add_one(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}- 返回类型必须是
Datum;参数只有一个PG_FUNCTION_ARGS(它展开为一个参数列表)。 - 用
PG_GETARG_*取参数、PG_RETURN_*返回结果,不要手工构造Datum。
第三,常用宏速查:
| 宏 | 用途 |
|---|---|
PG_GETARG_INT32(n) | 取第 n 个参数为 int32 |
PG_GETARG_TEXT_PP(n) | 取 text(可能为短头,只读) |
PG_GETARG_CSTRING(n) | 取 cstring |
PG_GETARG_BOOL(n) | 取 bool |
PG_ARGISNULL(n) | 判断参数是否为 NULL(非 STRICT 函数需要) |
PG_RETURN_INT32(x) / PG_RETURN_BOOL(x) | 返回对应类型 |
PG_RETURN_TEXT_P(x) | 返回 text |
PG_RETURN_NULL() / PG_RETURN_VOID() | 返回 NULL / void |
第四,在 SQL 脚本里注册,并保证语义标注与实现一致:
CREATE FUNCTION @extschema@.pg_add_one(integer) RETURNS integer
AS 'MODULE_PATHNAME', 'pg_add_one'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;IMMUTABLE / STRICT / PARALLEL SAFE 必须与实际行为一致,否则会产生错误结果或崩溃。这三条是最常见的 C 扩展 bug 来源。
PGXS 构建系统与调试
PGXS(PostgreSQL Extension Building Infrastructure)让扩展的构建脚本只有几行。Makefile:
# 扩展名
EXTENSION = myapp_ext
# 要安装的数据文件(控制文件 + 所有 sql 脚本)
DATA = myapp_ext--1.0.sql myapp_ext--1.0--1.1.sql myapp_ext.control
# 要编译并安装的共享库
MODULE_big = myapp_ext
OBJS = myapp_ext.o
PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)常用目标:
make # 编译(若有 C 源码)
sudo make install # 安装到 share/extension 与 pkglibdir
make installcheck # 运行回归测试(需 REGRESS 变量)
make clean # 清理只有 SQL 脚本、没有 C 源码时,去掉 MODULE_big / OBJS 两行即可。make install 会自动把 DATA 里的文件复制到 $(pg_config --sharedir)/extension/,把 MODULE_big 的目标复制到 $(pg_config --pkglibdir)/。
调试 C 扩展的三种常用手段:往服务器日志输出(elog(LOG, ...)、ereport(ERROR, ...));只做 make 快速验证编译;用 gdb -p $(pgrep -f "postgres.*myapp") 附加到后端再 break 目标函数。建议在开发机上用以 --enable-debug --enable-cassert 编译的 PostgreSQL 测试:断言(cassert)能抓出很多内存与状态错误。
回归测试(REGRESS)与目录结构
PGXS 内置了回归测试框架。在 Makefile 里加一行 REGRESS = basic slug upgrade,并准备 sql/basic.sql(输入)与 expected/basic.out(期望输出),再运行:
make installcheck它会启动一个测试数据库、执行 sql/*.sql、把输出与 expected/*.out 逐字符比较。这是 PostgreSQL 生态里扩展测试的事实标准,写扩展就该配回归测试。
推荐的目录布局(与 contrib 扩展一致):
myapp_ext/
├── Makefile
├── myapp_ext.control
├── myapp_ext--1.0.sql
├── myapp_ext--1.0--1.1.sql
├── myapp_ext.c
├── sql/{basic,upgrade}.sql
├── expected/{basic,upgrade}.out
├── README.md
└── LICENSE权限:SECURITY DEFINER 与 pg_init_privs
若扩展需要以「扩展拥有者」身份执行(如写审计表),用 SECURITY DEFINER 并锁死 search_path:
CREATE FUNCTION @extschema@.log_audit(payload jsonb)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = @extschema@, pg_temp
AS $$
BEGIN
INSERT INTO @extschema@.audit_log (payload, at) VALUES (payload, now());
END;
$$;反模式(危险!):
-- 错误!没锁 search_path,可能被劫持
CREATE FUNCTION log_audit(payload jsonb) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
AS $$ INSERT INTO audit_log (payload) VALUES (payload); $$;攻击者只要在自己 schema 下建一个同名 audit_log 并把该 schema 放进 search_path,就能让这个「以属主身份运行」的函数往自己控制的表里写数据。
另外,扩展创建的对象上若有非默认权限,PostgreSQL 会把它们记进 pg_init_privs,以便 ALTER EXTENSION UPDATE 后权限保持一致:
-- 在安装脚本里授权,这条 GRANT 会被记录
GRANT EXECUTE ON FUNCTION @extschema@.slugify(text) TO PUBLIC;pg_dump 与扩展:必须知道的差异
| 对象类型 | pg_dump 行为 |
|---|---|
| 扩展本身 | 只输出 CREATE EXTENSION name WITH SCHEMA x; |
| 扩展拥有的对象 | 不输出(由 CREATE EXTENSION 重建) |
extconfig 标记的表的数据 | 输出 |
| 未标记的表的数据 | 不输出 |
| 用户在扩展 schema 里自己建的对象 | 输出(但它们不属于扩展) |
推论:恢复时目标库必须已安装同名同版本的扩展二进制,否则 CREATE EXTENSION 失败。这是 pg_upgrade 前必须核对扩展清单的原因。
实战示例
场景一:纯 SQL 工具扩展(无 C,trusted)
在 pg_slug/ 下放三个文件:控制文件(在前面「从零写一个纯 SQL 扩展」的基础上加 trusted = true)、安装脚本 pg_slug--1.0.sql,以及一个只有四行的 Makefile:
# Makefile
EXTENSION = pg_slug
DATA = pg_slug--1.0.sql pg_slug.control
PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)make install-- 验证 trusted:普通用户也能装
CREATE ROLE app_user LOGIN;
SET ROLE app_user;
CREATE EXTENSION pg_slug;
SELECT pg_slug.slugify('Hello World'); -- hello-world场景二:带配置表的扩展
只要扩展里出现了带数据的表,就必须在脚本末尾调用 pg_extension_config_dump():
-- pg_slug--1.1.sql
\echo Use "CREATE EXTENSION pg_slug" to load this file. \quit
CREATE TABLE @extschema@.slug_rules (
id serial PRIMARY KEY,
pattern text NOT NULL,
replacement text NOT NULL
);
CREATE FUNCTION @extschema@.slugify(txt text)
RETURNS text
AS $$
SELECT regexp_replace(
regexp_replace(lower(trim(txt)), '[^a-z0-9]+', '-', 'g'),
'(^-|-$)', '', 'g');
$$
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE;
-- 关键:把配置表标记为需要 dump
SELECT pg_catalog.pg_extension_config_dump('@extschema@.slug_rules', '');验证:
pg_dump --schema-only mydb | grep -n "slug_rules"场景三:从 1.0 升级到 1.1
升级脚本里重复一遍增量 DDL(内容与 --1.1.sql 中新增的部分一致,但只写增量):
-- pg_slug--1.0--1.1.sql
CREATE TABLE @extschema@.slug_rules (
id serial PRIMARY KEY,
pattern text NOT NULL,
replacement text NOT NULL
);
SELECT pg_catalog.pg_extension_config_dump('@extschema@.slug_rules', '');把控制文件的 default_version 改成 '1.1' 并 make install,然后:
SELECT extversion FROM pg_extension WHERE extname = 'pg_slug'; -- 1.0
ALTER EXTENSION pg_slug UPDATE;
SELECT extversion FROM pg_extension WHERE extname = 'pg_slug'; -- 1.1场景四:最小 C 扩展与 text/NULL 处理
/* myapp_ext.c */
#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(pg_add_one);
PG_FUNCTION_INFO_V1(pg_concat_text);
PG_FUNCTION_INFO_V1(pg_upper_or_null);
Datum
pg_add_one(PG_FUNCTION_ARGS)
{
int32 arg = PG_GETARG_INT32(0);
PG_RETURN_INT32(arg + 1);
}
Datum
pg_concat_text(PG_FUNCTION_ARGS)
{
text *a = PG_GETARG_TEXT_PP(0);
text *b = PG_GETARG_TEXT_PP(1);
char *sa = text_to_cstring(a);
char *sb = text_to_cstring(b);
int len = strlen(sa) + strlen(sb);
text *result = (text *) palloc(len + VARHDRSZ);
SET_VARSIZE(result, len + VARHDRSZ);
memcpy(VARDATA(result), sa, strlen(sa));
memcpy(VARDATA(result) + strlen(sa), sb, strlen(sb));
pfree(sa);
pfree(sb);
PG_RETURN_TEXT_P(result);
}
Datum
pg_upper_or_null(PG_FUNCTION_ARGS)
{
/* 非 STRICT 函数:必须自己判空 */
if (PG_ARGISNULL(0))
PG_RETURN_NULL();
text *arg = PG_GETARG_TEXT_PP(0);
char *s = text_to_cstring(arg);
text *out = cstring_to_text(s); /* 简化示例,实际可用 upper 语义函数 */
pfree(s);
PG_RETURN_TEXT_P(out);
}处理 varlena 的几个关键点:VARHDRSZ 是长度头的字节数,分配内存时必须加上;SET_VARSIZE() 必须调用,否则 PostgreSQL 读到的长度是垃圾值;palloc() 是 PostgreSQL 的内存分配,会在事务/函数结束时自动释放,不要用 malloc。
# myapp_ext.control
comment = 'MyApp helper functions'
default_version = '1.0'
module_pathname = '$libdir/myapp_ext'
relocatable = true-- myapp_ext--1.0.sql
\echo Use "CREATE EXTENSION myapp_ext" to load this file. \quit
CREATE FUNCTION @extschema@.pg_add_one(integer) RETURNS integer
AS 'MODULE_PATHNAME', 'pg_add_one'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION @extschema@.pg_concat_text(text, text) RETURNS text
AS 'MODULE_PATHNAME', 'pg_concat_text'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
-- 不能标 STRICT,因为需要自行处理 NULL
CREATE FUNCTION @extschema@.pg_upper_or_null(text) RETURNS text
AS 'MODULE_PATHNAME', 'pg_upper_or_null'
LANGUAGE C IMMUTABLE;# Makefile
EXTENSION = myapp_ext
DATA = myapp_ext--1.0.sql myapp_ext.control
MODULE_big = myapp_ext
OBJS = myapp_ext.o
PG_CONFIG ?= pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)make && sudo make installCREATE EXTENSION myapp_ext;
SELECT myapp_ext.pg_add_one(41); -- 42
SELECT myapp_ext.pg_concat_text('foo', 'bar'); -- foobar
SELECT myapp_ext.pg_upper_or_null(NULL); -- NULL,不报错场景五:加回归测试并跑通
# sql/basic.sql
CREATE EXTENSION pg_slug;
SELECT pg_slug.slugify('Hello PostgreSQL World!');
SELECT pg_slug.slugify('---weird spaces---');
DROP EXTENSION pg_slug;# expected/basic.out
CREATE EXTENSION pg_slug;
slugify
----------------
hello-postgresql-world
(1 row)
SELECT pg_slug.slugify('---weird spaces---');
slugify
-----------
weird-spaces
(1 row)
DROP EXTENSION pg_slug;在 Makefile 里加 REGRESS = basic,然后 make installcheck。输出应以 # All 1 tests passed. 结束。
注意事项
注意一:控制文件里 default_version 是必需的。
-- 错误!控制文件里只有 comment,没有 default_version
CREATE EXTENSION my_ext;
-- ERROR: extension "my_ext" has no default version注意二:脚本里不要用 public. 硬编码。
-- 错误!装到别的 schema 会失败,且破坏 relocatable
CREATE FUNCTION public.slugify(text) RETURNS text AS $$ ... $$ LANGUAGE sql;注意三:忘记加 \echo ... \quit 头会让脚本被误执行。
不推荐直接 psql -f pg_slug--1.0.sql:用户会把对象塞进当前库且没有任何保护。头部的 \echo Use "CREATE EXTENSION x" to load this file. \quit 正是为了拦住这种用法。
注意四:配置表不加 pg_extension_config_dump() 会导致备份丢数据。
-- 错误!表数据不会被 pg_dump 导出
CREATE TABLE @extschema@.slug_rules (...);
-- 缺少:SELECT pg_catalog.pg_extension_config_dump('@extschema@.slug_rules', '');注意五:升级脚本里删除对象前先想清楚依赖。
-- 不推荐:直接 DROP FUNCTION @extschema@.slugify(text); —— 若已被视图或其他函数引用,会连带失败或级联删除。
注意六:CREATE FUNCTION 的语义标注必须与实现一致。
-- 错误!实现对时间敏感却标了 IMMUTABLE
CREATE FUNCTION @extschema@.now_str() RETURNS text
AS 'MODULE_PATHNAME', 'now_str'
LANGUAGE C IMMUTABLE; -- 会让规划器错误地常量折叠注意七:SECURITY DEFINER 函数必须锁 search_path。
-- 错误!可被 schema 劫持
CREATE FUNCTION log_audit(jsonb) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER AS $$ INSERT INTO audit_log VALUES ($1); $$;注意八:C 代码里不能用 malloc / free。
/* 错误!内存不归 PostgreSQL 管理,可能泄漏或与异常处理冲突 */
char *buf = malloc(100);
free(buf);
/* 正确 */
char *buf = palloc(100);
pfree(buf);注意九:varlena 结果必须设置 SET_VARSIZE。
/* 错误!长度字段是垃圾值 */
text *r = (text *) palloc(len + VARHDRSZ);
memcpy(VARDATA(r), s, len);
/* 缺少:SET_VARSIZE(r, len + VARHDRSZ); */注意十:MODULE_PATHNAME 未配置会指向默认路径。
若 C 库名与扩展名不同,必须在控制文件里显式写 module_pathname = '$libdir/myapp_ext';否则 AS 'MODULE_PATHNAME', ... 会展开成 $libdir/<扩展名>,加载失败。
最佳实践
从纯 SQL 扩展起步。90% 的内部工具需求用
LANGUAGE sql或plpgsql就能满足,不要为了「性能」提前上 C。脚本头部固定加
\echo Use "CREATE EXTENSION x" to load this file. \quit,与 PostgreSQL contrib 的写法保持一致。脚本内所有引用一律全限定,schema 部分用
@extschema@而不是扩展名或public。默认开启
relocatable = true(前提是脚本规范),给使用者留出ALTER EXTENSION SET SCHEMA的余地。带数据表的扩展必须调用
pg_extension_config_dump(),并用extcondition过滤掉不该备份的行。维护完整的版本脚本链(
--1.0.sql、--1.0--1.1.sql、--1.1.sql…),保证「从任意旧版本升级」与「从零安装最新版」都能走通。为每个版本写回归测试(
REGRESS+sql/+expected/),make installcheck通过才算完成。CREATE FUNCTION的属性(IMMUTABLE/STRICT/PARALLEL SAFE/COST)必须与实现语义严格一致,这是最容易引入生产事故的地方。所有
SECURITY DEFINER函数显式SET search_path = @extschema@, pg_temp,杜绝 schema 劫持。C 代码只用
palloc/pfree,并对所有varlena结果调用SET_VARSIZE,避免内存与长度相关的崩溃。在
--enable-cassert的 PostgreSQL 上做开发测试,用断言提前暴露内存、状态与并发问题,再上生产验证。把扩展纳入 CI 与发布流程:源码、
Makefile、control、sql全部进版本控制,产物由 CI 构建,安装版本由pg_available_extension_versions核对。
下一节
扩展系统这一组到此结束。下一篇进入 行级安全策略 RLS 概述,看 PostgreSQL 如何用 CREATE POLICY 把「谁能看哪些行」的规则下沉到数据库,为多租户与合规场景提供最后一道防线。
参考链接
- PostgreSQL 18 Documentation — Packaging Related Objects into an Extension
- PostgreSQL 18 Documentation — Extension Building Infrastructure (PGXS)
- PostgreSQL 18 Documentation — CREATE EXTENSION
- PostgreSQL 18 Documentation — CREATE FUNCTION
- PostgreSQL 18 Documentation — C-Language Functions
- PostgreSQL 18 Documentation — pg_extension
- PostgreSQL 18 Documentation — pg_depend
- PostgreSQL 18 Documentation — pg_init_privs
- PostgreSQL 18 Documentation — Writing a Procedural Language Handler