PostgreSQL 已经有超过1000个插件, 怎么感觉什么人都能开发插件?
很多原因
你是不是也蠢蠢欲动了?
不会C啊, 怎么开发? 没关系啊, 现在AI coding这么发达, 你把想法告诉AI就可以了.
不懂PG内核代码? 也没关系啊, 可以通过deepwiki来进行解读, 太方便了.
但是开发插件的流程最好先熟悉一下, 下面就翻译一篇<Postgres扩展开发简介>的文章
https://www.pgedge.com/blog/introduction-to-postgres-extension-development
运行 Postgres 集群最显著的优势或许就是可以访问其庞大的扩展生态系统,包括PL、FDW、INDEX和存储引擎、便捷的实用函数等等。扩展涵盖了从列式表存储、高效的向量函数、BM25全文搜索分析,到UUIDv7等更基础的扩展功能,无所不包。
但是,实际开发 一个扩展需要做些什么呢?需要调用哪些晦涩难懂的 Postgres API?安装过程是如何进行的?这需要天才吗?本文将尝试回答所有这些问题以及其他一些问题,并希望到最后,您也能通过为 Postgres 贡献一个有用的功能而获得永垂不朽的声誉。
我之前写过两个相对简单的纯 SQL 扩展。这种情况并不罕见,因为将一些 PL/pgSQL 函数打包成一个实用程序库是一种方便的分发方式。既然插件的潜在应用范围如此广泛,那么用扩展插件来添加哪些有趣的功能会比较好呢?例如,有些 DBA 可能希望阻止用户执行 DDL,那么我们可以这样做吗?
老司机可能已经知道,可以使用 Postgres 事件触发器来拒绝 DDL 啊,如下所示:
CREATE OR REPLACE FUNCTION f_block_ddl()
RETURNS event_trigger AS $$
BEGIN
RAISE EXCEPTION 'DDL is not allowed on this server.';
END;
$$ LANGUAGE plpgsql;
CREATE EVENT TRIGGER t_block_all_ddl
ON ddl_command_start
EXECUTE FUNCTION f_block_ddl();
然而讽刺的是,DROP EVENT TRIGGER 命令本身并不会调用这个触发器。如果我们想要一个耦合度更高的方法,通过一个用 C 语言编写的扩展插件直接嵌入到 Postgres 中,该怎么办?这会有什么改变呢?
根据 Postgres 扩展文档,所有扩展都需要一个控制文件来定义有关扩展本身的各种元数据。
因此让我们为扩展创建一个文件夹:
mkdir noddl
cd noddl
并创建一个包含以下内容的文件 noddl.control :
comment = 'The Infamous DDL Blocker'
default_version = '1.0'
module_pathname = '$libdir/noddl'
这实际上是控制文件所需的最低要求 。它提供了扩展的名称、版本以及库本身的路径。在本例中,库路径就是名为 “noddl” 的库的默认位置 $libdir 。
现在,Postgres 知道在安装我们的新扩展后该做什么。
由于这是一个 C 扩展,我们需要进一步准备开发环境。Postgres 构建环境至少需要 gcc、make 和 Postgres 开发头文件。这些内容完全依赖于操作系统和发行版,因此这超出了本指南的范围。
不过,Postgres 下载 仓库通常包含所有受支持操作系统的开发包。例如,如果是 Debian、Ubuntu 或 Mint 系统,只需一个便捷的命令即可添加 Postgres 开发包:
sudo apt install postgresql-server-dev-17
Red Hat、Fedora 或 Rocky 系统可能会这样做:
sudo dnf install postgresql17-devel
一旦安装了 postgres 开发头文件,请仔细检查pg_config命令的可用性 ,因为这将指导我们的扩展的构建和安装过程(编译插件时的依赖包通过pg_config输出的路径中过去)。
pg_config --pgxs
该 pgxs 应显示扩展 Makefile 的位置,该扩展 Makefile 将完成设置头路径、安装到正确的库目录等大部分艰苦的工作。
最好 保留 Postgres 源代码的本地副本。Postgres C 扩展开发通常依赖于调用未记录的内部函数、引用多个结构体以及调用多个接口宏。最好的方法是检查定义这些内容的源代码,因为通常有一个非常有用的注释标题,甚至是一个 README 文件,可以更详细地解释这些内容。
因此,请前往 GitHub 并获取一份副本(可通过-b选择对应稳定分支):
git clone --depth 1 -b REL_18_STABLE https://github.com/postgres/postgres
相信我,你会很高兴你这么做的。
说到 Makefile,现在该为 noddl 扩展创建一个了。PGXS文档 提供了一个 Makefile 示例,并解释了所有可用的 Makefile 参数。以此为指导,我们的 Makefile 应该如下所示:
MODULES = noddl
PGFILEDESC = "Extension to block DDL for no reason."
EXTENSION = noddl
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)
与控制文件类似,我们实际上只是描述扩展。请注意,文件中的最后三行用于包含 Postgres pgxs makefile,它负责完成所有实际工作。当你不知道自己在做什么时,没有什么比委派职责更好的了。
Postgres C 扩展有两个关键组件。
PG_MODULE_MAGIC 宏。别担心,这很神奇!PGinit 函数,作为扩展的入口点,用于设置钩子、创建 GUC、启动工作进程等。鉴于此模块将阻塞 DDL,因此应该有一种便捷的方法来启用和禁用它,以便超级用户在必要时执行 DDL。这意味着我们至少需要一个 GUC。如果考虑到这一点,该扩展的早期框架将如下所示:
// Standard set of includes for an extension.
#include "postgres.h"
#include "utils/builtins.h"
#include "utils/elog.h"
#include "utils/guc.h"
#include "tcop/utility.h" // We'll need this later
// The module code
PG_MODULE_MAGIC;
static bool deny_ddl = false;
void
_PG_init(void)
{
DefineCustomBoolVariable(
"noddl.enable",
"Deny All DDL statements",
NULL,
&deny_ddl,
False, // Disabled by default.
PGC_SUSET, // Only allow superusers to modify
0, // No flags for this GUC
NULL, NULL, NULL // No variable hooks necessary
);
MarkGUCPrefixReserved("noddl");
}
这实际上已经是一个功能性扩展了。它目前唯一能做的事情就是设置 GUC noddl.enable,但它将成功编译并安装为一个功能性 Postgres 扩展。
我们接下来要做的就是让我们的扩展变得有用。
理解如何捕获 DDL 语句说起来容易做起来难。我们知道,所有语句在被分发到引擎执行部分之前都必须进行解析。这意味着我们需要在 Postgres 执行过程中调用的钩子列表中添加某种函数。
事实证明,Postgres 调度程序使用的类别之一是为 utility 命令保留的。如果我们查看 Postgres 源代码,甚至有一个专门利用此功能的测试模块:test_oat_hooks。如果我们稍微检查一下 test_oat_hooks.c ,我们会找到 REGRESS_utility_command :
if (REGRESS_deny_utility_commands && !superuser_arg(GetUserId()))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: %s", action)));
这段代码本质上会检查一个自定义的 GUC 来决定是否完全阻止语句,并且只会阻止非超级用户的命令。如果这两个条件都满足,则会抛出错误,而这正是该函数的功能范围。此示例演示了如何不加区分地拒绝所有 utility 程序命令,但这只是隔离 DDL 事件的第一步。
我们如何具体地隔离 DDL?如果我们在 Postgres 源代码中查找字符串“DDL”,src/backend/tcop/utility.c 会非常醒目。该 GetCommandLogLevel 函数将所有命令标签分类到 GUC log_statement 的不同类别中。REGRESS_utility_command 签名包含一个PlannedStmt参数, 包含utilityStmt 字段。如果我们将该参数传递给GetCommandLogLevel 函数,我们只需要处理标识为 LOGSTMT_DDL 的语句即可。
如果我们考虑到这一点,阻塞 DDL 看起来是这样的:
Node *parsetree = pstmt->utilityStmt;
if (deny_ddl && GetCommandLogLevel(parsetree) == LOGSTMT_DDL)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("DDL is not allowed on this system."),
errhint("Set the noddl.enable GUC to false to continue.")));
我们要设置的特定钩子名为ProcessUtility_hook。假设我们的回调函数名为noddl_ProcessUtility,我们只需要将其添加到函数PGinit末尾 :
ProcessUtility_hook = noddl_ProcessUtility;
如果我们到此为止,我们的钩子函数就可以完全运行,我们的工作就完成了。不幸的是,事情并没有那么简单。
与某些可扩展软件不同,Postgres 不提供用于在某种专用回调栈中注册钩子的工具函数。相反,它定义了几个全局变量,扩展程序可以选择覆盖这些全局变量,其中一个变量我们在上一节中设置过。但是,这些全局变量可以被加载到进程环境中的任何 扩展程序操作,这些扩展程序可以代表用户会话、后台工作程序或其他某种混合上下文。
如果 shared_preload_libraries 这样设置会怎样:
shared_preload_libraries = "pg_stat_statements, noddl"
现在,如果 noddl 扩展设置了 ProcessUtility_hook 钩子会发生什么?在这种情况下,它实际上会停用(覆盖) shared_preload_libraries 前面设置的 pg_stat_statements 扩展插件中的同名钩子(ProcessUtility_hook)!我们真正需要做的是保留任何先前设置的钩子,并在一种模拟堆栈中传播调用。每个行为良好的扩展都会这样做,因此理论上每个扩展的钩子最终都会得到遵守。这意味着我们需要另一个全局变量:
static ProcessUtility_hook_type next_ProcessUtility_hook = NULL;
然后,我们需要在覆盖它之前保留任何PGinit先前定义的ProcessUtility钩子 :
if (ProcessUtility_hook)
next_ProcessUtility_hook = ProcessUtility_hook;
ProcessUtility_hook = noddl_ProcessUtility;
最后,我们自己的 ProcessUtility 函数钩子的完整主体需要物理调用默认的 Postgres ProcessUtility 函数或先前定义的钩子:
static void
noddl_ProcessUtility(
PlannedStmt *pstmt,
const char *queryString,
bool readOnlyTree,
ProcessUtilityContext context,
ParamListInfo params,
QueryEnvironment *queryEnv,
DestReceiver *dest,
QueryCompletion *qc)
{
Node *parsetree = pstmt->utilityStmt;
// Reject DDL if enabled
if (deny_ddl && GetCommandLogLevel(parsetree) == LOGSTMT_DDL)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("DDL is not allowed on this system."),
errhint("Disable the noddl.enable GUC to continue."))
);
// Call downstream hooks, 避免钩子冲突
if (next_ProcessUtility_hook)
next_ProcessUtility_hook(
pstmt, queryString, readOnlyTree, context, params,
queryEnv, dest, qc
);
else
standard_ProcessUtility(
pstmt, queryString, readOnlyTree, context, params,
queryEnv, dest, qc
);
} // noddl_ProcessUtility
理想情况下,Postgres 会直接处理钩子堆栈,只是为了防止某个行为不当的扩展毁掉所有扩展。讽刺的是,我们的扩展本身的代码比维护钩子链不中断的调用还少。
信不信由你,这就是完整的扩展。谁能想到为 Postgres 编写一个 C 扩展会如此简单?既然我们已经有了一个 makefile,那么最后的步骤就是创建、安装并激活扩展。让我们开始构建它:
make clean
make
sudo make install
这会将扩展存放到相应的库目录中,并将控制文件复制到 Postgres 用于跟踪可用扩展的目录中。接下来,我们通过将扩展添加到 shared_preload_libraries 来激活它:
shared_preload_libraries = "noddl"
最后,重启 Postgres。基于 Debian 的系统应该使用如下命令:
sudo systemctl restart postgresql@17-main
Red Hat 系列系统将使用如下命令:
sudo systemctl restart postgresql-17
或者你可以用传统的方式来做:
pg_ctl -D /path/to/pgdata restart -m fast
Postgres 重启后,noddl 扩展即可使用并生效。但请记住,我们的 GUC 默认是禁用的,因此我们需要启用它。以超级用户身份连接到 Postgres 并执行以下命令:
ALTER SYSTEM SET noddl.enable = true;
SELECT pg_reload_conf();
然后尝试执行任何 DDL 语句:
postgres=# CREATE TEMP TABLE foo(id INT);
ERROR: DDL is not allowed on this system.
HINT: Disable the noddl.enable GUC to continue.
太粗鲁了!幸好我们使用了PGC_SUSET GUC,这意味着超级用户可以在会话级别覆盖它:
postgres=# SET noddl.enable = false;
SET
postgres=# CREATE TEMP TABLE foo(id INT);
CREATE TABLE
与事件触发器不同,无法通过禁用event_triggers GUC 来规避此扩展。如果我们移除noddl.enable GUC,重新启用 DDL 的唯一方法是卸载扩展本身。在极少数情况下,需要结构静态部署,而这正是强制执行该限制的一种方法。
Postgres 扩展几乎可以利用 Postgres 源代码中的所有函数、结构体、宏、回调钩子以及一些全局会话变量和专用 API。它拥有令人难以置信的灵活性和强大功能,但使用它需要一定的克制力和负责任的治理。当你意识到 Postgres 扩展的“兔子洞”究竟有多深时,你也会感到一阵狂喜。哦,我们可以做的事情真多!
这里演示的 noddl 扩展相对来说比较简单,几乎没有意义,仅仅触及了 Postgres 扩展性的表面。Postgres 有超过 1000 个扩展是有原因的,而且这个列表每天都在增长。除了与核心团队合作修补 Postgres 源代码本身之外,这是向 Postgres 添加功能的最简单方法。
https://www.pgedge.com/blog/introduction-to-postgres-extension-development