DeepSeek总结的无需编译器:编写纯 SQL 的 Postgres 扩展

来源:https://www.pgedge.com/blog/no-compiler-required-writing-sql-only-postgres-extensions

无需编译器:编写纯 SQL 的 Postgres 扩展

作者: Shaun Thomas
日期: 2026 年 5 月 8 日

最近在圣何塞举办的 2026 年 Postgres 会议上,我做了一个题为"让我们构建一个 Postgres 扩展!"的演讲。由于整个演讲主要聚焦于编写 C 扩展,同时探索 Postgres 源代码,所以我只是顺便提到了纯 SQL 扩展。但在 Postgres 社区中,哪种人更常见?C 开发者,还是懂 SQL 的人?

事实证明,你可以利用函数、触发器、视图、表和许多其他 Postgres 原生功能做很多事情。扩展系统并不关心其内容是编译的 C 代码还是纯 SQL。它只需要一个控制文件、一个 SQL 脚本和一个可选的 Makefile 来帮助安装。

因此,让我们完全用 SQL 构建一个相对简单的扩展。

我们想要什么?

首先,我们需要一个计划。这个扩展到底应该做什么?我之前写过一篇关于用 C 扩展阻塞 DDL 的文章,为什么不使用 SQL 重新审视这个例子呢?

由于这是纯 SQL,我们可以毫不费力地添加其他有用的元素,例如:

  • 一个启用或禁用扩展的设置。
  • 一个允许或阻止超级用户执行 DDL 的设置。
  • 一个允许其成员绕过 DDL 限制的角色。
  • 一个将用户添加到绕过角色的函数。
  • 一个将用户从绕过角色中移除的函数。
  • 一个查看哪些用户在绕过角色中的视图。
  • 一个实际阻止 DDL 尝试的事件触发器。

我们不是在构建一个简单的事件触发器来阻止 DDL 执行,而是在构建一个 DDL 执行管理套件。这应该有望展示纯 SQL 实现的能力有多强。

三个文件和一个梦想

每个 Postgres 扩展,无论复杂程度如何,都可以归结为相同的基本结构:

  • 一个描述扩展的控制文件
  • 一个用于创建表、视图、函数等的 SQL 脚本
  • 一个可选的 Makefile,用于将 SQL 脚本和控制文件复制到正确的位置。与 C 项目不同,纯 SQL 扩展没有构建步骤,因为没有什么需要编译。

这是我们的项目目录结构:

复制代码
block_ddl/
├── block_ddl--1.0.sql
├── block_ddl.control
└── Makefile

让我们从控制文件开始。它告诉 Postgres 扩展的名称、版本、描述以及一些行为标志的设置。我们的控制文件如下所示:

bash 复制代码
# block_ddl extension
comment = 'DDL blocking for Postgres'
default_version = '1.0'
superuser = true
relocatable = false
  • comment 会显示在 \dxpg_extension 目录视图中。
  • default_version 告诉 Postgres 当有人运行 CREATE EXTENSION block_ddl 而未指定版本时,加载哪个 SQL 脚本。
  • superuser = true 标志意味着只有超级用户可以安装或更新此扩展。这是默认设置,但明确指定更好。
  • relocatable = false 标志值得简要解释。可重定位扩展可以在安装后通过 ALTER EXTENSION ... SET SCHEMA 在模式之间移动。我们的扩展不能,因为 SQL 脚本使用 @extschema@ 替换标记在内部引用了特定的模式。在安装期间定义模式是可行的(也是推荐的),但之后不行。

接下来是 Makefile。对于 C 扩展,Makefile 负责协调编译和链接。对于纯 SQL 扩展,它只需将控制文件和 SQL 文件复制到 Postgres 存放扩展的库文件夹。整个文件内容如下:

makefile 复制代码
EXTENSION = block_ddl
DATA = block_ddl--1.0.sql

PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

通常还有一个 MODULES 行来指定要编译的 C 源文件。没有它,make install 就只是将控制文件和 SQL 脚本复制到正确的目录。PGXS 构建基础设施负责处理其余部分。

样板文件处理完毕,是时候找点乐子了。

一些簿记工作

在我们真正开始之前,扩展需要存在于一个模式中。该模式中的一些对象需要是公共可访问的。因此,我们文件中的第一件事需要如下所示:

sql 复制代码
GRANT USAGE ON SCHEMA @extschema@ TO PUBLIC;

USAGE 仅意味着模式对象是可见的。除非特别授权,否则用户将无法创建对象,甚至无法从表中选择。

之后,我们需要处理配置设置。您可能认为首选是使用会话变量,但这是一个微妙的陷阱。这里的问题是纯 SQL 扩展无法访问系统变量的更精细控制点,例如将它们限制为超级用户、系统启动、服务重载等。这意味着无法阻止用户通过简单的 SET 语句覆盖它们。

下一个选项是配置表。扩展文档说我们可以注册这些表,以便在转储和恢复数据库时保留值,并且控制表更新很简单。所以让我们用以下内容开始我们的扩展:

sql 复制代码
CREATE TABLE @extschema@.ext_config (
  name     TEXT PRIMARY KEY,
  setting  TEXT NOT NULL
);

INSERT INTO @extschema@.ext_config 
VALUES ('enabled', 'off'), ('allow_super', 'on');

SELECT pg_catalog.pg_extension_config_dump('@extschema@.ext_config', '');

GRANT SELECT ON @extschema@.ext_config TO PUBLIC;

CREATE OR REPLACE FUNCTION @extschema@.alter_config(p_name TEXT, p_setting TEXT)
RETURNS BOOLEAN AS
$$
BEGIN
  IF p_name IN ('enabled', 'allow_super') THEN
    UPDATE @extschema@.ext_config
       SET setting = (CASE WHEN p_setting = 'on' THEN 'on' ELSE 'off' END)
     WHERE name = p_name;
  END IF;

  RETURN true;
END;
$$ LANGUAGE plpgsql;

REVOKE EXECUTE ON FUNCTION @extschema@.alter_config(TEXT, TEXT) FROM PUBLIC;

现在只有超级用户可以配置扩展!普通用户仍然需要能够读取配置表,因为事件触发器是以该用户身份运行的。无论如何,我们现在有了一个方便的配置接口。

角色设计

下一步是允许某些用户绕过 DDL 限制。最简单的方法是创建一个角色,超级用户可以将这些被允许的用户授予该角色。我们还可以在这里处理我们有用的授权/撤销函数:

sql 复制代码
CREATE ROLE block_ddl_allowed_user;

CREATE OR REPLACE FUNCTION @extschema@.add_ddl_bypass_user(p_user TEXT)
RETURNS BOOLEAN AS
$$
BEGIN
  EXECUTE format('GRANT block_ddl_allowed_user TO %I', p_user);
  RETURN true;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION @extschema@.remove_ddl_bypass_user(p_user TEXT)
RETURNS BOOLEAN AS
$$
BEGIN
  EXECUTE format('REVOKE block_ddl_allowed_user FROM %I', p_user);
  RETURN true;
END;
$$ LANGUAGE plpgsql;

REVOKE EXECUTE ON FUNCTION @extschema@.add_ddl_bypass_user(TEXT) FROM PUBLIC;
REVOKE EXECUTE ON FUNCTION @extschema@.remove_ddl_bypass_user(TEXT) FROM PUBLIC;

使用包含扩展名的长名称 block_ddl_allowed_user 是为了防止名称冲突。这个角色可能尚未被使用,并且其目的显而易见。这些函数意味着管理员不需要记住角色名称本身,但也不是必需的。

最后要添加的是列出绕过用户的视图:

sql 复制代码
CREATE VIEW @extschema@.v_ddl_bypass_users AS
SELECT u.rolname AS user_name
  FROM pg_authid x
  JOIN pg_auth_members m on (m.roleid=x.oid)
  JOIN pg_authid u on (m.member=u.oid)
 WHERE x.rolname = 'block_ddl_allowed_user';

GRANT SELECT ON @extschema@.v_ddl_bypass_users TO PUBLIC;

这是一个你能凭空知道的查询吗?可能不是。现在扩展帮你处理了,所以你不需要。

禁止通行

我们扩展的核心是一个 DDL 阻塞器:一个在 ddl_command_start 上触发的事件触发器,除非会话用户是超级用户,否则它会引发异常。这个阻塞例程的 C 版本比我们在这里构建的要复杂得多。

这是我们用于阻塞 DDL 的函数:

sql 复制代码
CREATE OR REPLACE FUNCTION @extschema@.fn_block_ddl()
RETURNS event_trigger AS
$$
DECLARE
  enabled TEXT;
  allow_super TEXT;
BEGIN
  -- 获取我们当前的配置设置
  SELECT setting INTO enabled
    FROM @extschema@.ext_config WHERE name = 'enabled';
  SELECT setting INTO allow_super
    FROM @extschema@.ext_config WHERE name = 'allow_super';

  -- 仅在以下情况下阻塞:
  -- 1. 扩展已启用
  IF enabled != 'on' THEN
    RETURN;
  -- 2. 允许超级用户且当前用户是超级用户
  ELSIF allow_super = 'on' AND
        (SELECT rolsuper FROM pg_catalog.pg_roles
          WHERE rolname = CURRENT_USER) THEN
    RETURN;
  -- 3. 用户是 block_ddl_allowed_user 的成员
  ELSIF EXISTS (SELECT * FROM @extschema@.v_ddl_bypass_users
          WHERE user_name = CURRENT_USER) THEN
    RETURN;
  END IF;

  RAISE EXCEPTION 'DDL command "%" denied by block_ddl', tg_tag
      USING HINT = 'Connect as a superuser, '
                   'or a user with block_ddl_allowed_user access';

END;
$$ LANGUAGE plpgsql;

RETURNS event_trigger 声明使此函数有资格与 CREATE EVENT TRIGGER 一起使用。这是一种特殊的返回类型,向 Postgres 指示如何调用该函数。

超级用户检查查询 pg_catalog.pg_roles 以获取 current_user。这允许超级用户出于测试目的模拟其他用户,并且可能阻止意外的 DDL 执行,前提是他们先执行 SET ROLE some_other_user。最后的检查是针对我们创建的 v_ddl_bypass_users 视图。我们可能会想使用 pg_has_role 信息函数来实现这一点,但该函数显示的是有效权限,而不是实际成员资格。超级用户拥有所有权限,因此如果我们不显式验证角色成员资格,他们会自动通过此检查。

函数就位后,创建事件触发器只需一行代码来调用该函数:

sql 复制代码
CREATE EVENT TRIGGER block_ddl ON ddl_command_start
  EXECUTE FUNCTION @extschema@.fn_block_ddl();

ddl_command_start 事件在任何 DDL 命令执行之前触发。如果我们的函数此时引发异常,命令将永远不会运行。简单易行。

在 Postgres 看来,什么算作"DDL"?实际上,相当多。ddl_command_start 事件会为 CREATEALTERDROPGRANTREVOKECOMMENTREINDEXREFRESH MATERIALIZED VIEWSECURITY LABELSELECT INTO 触发。它不会为针对数据库、角色、表空间,或者具有讽刺意味的是,针对事件触发器本身的命令触发。

我们也可以使用 WHEN 子句过滤特定的命令标签:

sql 复制代码
CREATE EVENT TRIGGER block_ddl ON ddl_command_start
  WHEN TAG IN ('CREATE TABLE', 'DROP TABLE', 'ALTER TABLE')
  EXECUTE FUNCTION @extschema@.fn_block_ddl();

但这还有什么乐趣呢?

试运行

是时候看看这东西是否真能工作了。首先,安装扩展文件:

bash 复制代码
$ cd block_ddl
$ sudo make install

这会将 block_ddl.controlblock_ddl--1.0.sql 复制到扩展目录。现在连接到一个数据库并创建扩展:

sql 复制代码
CREATE SCHEMA block_ddl;
CREATE EXTENSION block_ddl WITH SCHEMA block_ddl;

\dx block_ddl

                          List of installed extensions
   Name    | Version | Default version |  Schema   |        Description        
-----------+---------+-----------------+-----------+---------------------------
 block_ddl | 1.0     | 1.0             | block_ddl | DDL blocking for Postgres

扩展已安装。让我们验证事件触发器是否就位:

sql 复制代码
SELECT evtname, evtevent, evtenabled
  FROM pg_event_trigger
 WHERE evtname = 'block_ddl';

  evtname  |      evtevent       | evtenabled
-----------+---------------------+------------
 block_ddl | ddl_command_start   | O

evtenabled 中的 O 表示"origin",这是默认的启用状态(在除复制之外的所有上下文中触发)。是时候了!

测试阻塞器

默认情况下,阻塞是关闭的。让我们通过创建一个临时表来确认:

sql 复制代码
CREATE TABLE scratch (id int);
-- CREATE TABLE

DROP TABLE scratch;
-- DROP TABLE

没有报错。现在让我们启用阻塞器:

sql 复制代码
SELECT block_ddl.alter_config('enabled', 'on');

然后再次测试:

sql 复制代码
CREATE TABLE scratch (id int);
-- CREATE TABLE

仍然有效。默认情况下,超级用户可以免费通行。让我们堵上这个漏洞:

sql 复制代码
SELECT block_ddl.alter_config('allow_super', 'off');
CREATE TABLE scratch (id int);

ERROR:  DDL command "CREATE TABLE" denied by block_ddl
HINT:  Connect as a superuser, or a user with block_ddl_allowed_user access
CONTEXT:  PL/pgSQL function block_ddl.fn_block_ddl() line 27 at RAISE

现在 DDL 命令被完全阻止。这应该适用于任何潜在的 DDL:

sql 复制代码
CREATE INDEX ON scratch (id);
ERROR:  DDL command "CREATE INDEX" denied by block_ddl

ALTER TABLE scratch ADD COLUMN name text;
ERROR:  DDL command "ALTER TABLE" denied by block_ddl

我们的绕过系统有效吗?

sql 复制代码
SELECT block_ddl.add_ddl_bypass_user('postgres');
CREATE TABLE scratch (id int);
-- CREATE TABLE

显式绕过现在允许 DDL。普通用户呢?让我们创建一个用户并再次测试:

sql 复制代码
CREATE USER app_user;
SET ROLE app_user;

CREATE TABLE nope (id int);
ERROR:  DDL command "CREATE TABLE" denied by block_ddl

完全符合预期!

注意事项

纯 SQL 扩展功能强大,但它们不能完全替代 C。在您决定采用哪种方法之前,需要了解一些权衡。

  • GUC 安全差距 。在这个扩展的 C 版本中,GUC 使用 PGC_SUSET 上下文注册,这意味着只有超级用户可以更改它。在我们纯 SQL 版本中,block_ddl.enabled 将是一个自定义参数,任何会话都可以修改。我们不得不通过使用配置表来为此设计一个有些迂回的解决方案。如果存在某种为扩展注册真正变量的 SQL 接口,这就没有必要了。
  • 事件触发器盲点 。一些 DDL 命令根本不会触发事件触发器。对数据库、角色、表空间以及事件触发器本身的操作是豁免的。像 CREATE DATABASEALTER ROLE 这样的操作完全豁免。这就是 Postgres 的内置权限系统(或 pg_hba.conf 限制)应该承担重任的地方。再次强调,C 扩展可以访问我们 SQL 版本只能梦想的功能。
  • 没有后台工作进程或钩子。C 扩展可以注册后台工作进程、拦截查询计划、挂接到执行器,并在基础层面修改服务器行为。纯 SQL 扩展完全在 SQL 层内运行。如果您的用例涉及任何这些更深层次的功能,那么 C 是唯一的选择。

对于其他一切呢?函数、触发器、事件触发器、视图、类型、域、操作符、聚合、表等等都可以存在于纯 SQL 扩展中。这涵盖了相当多的领域。

总结

Postgres 扩展系统通常被认为需要 C 专业知识、编译器工具链和对服务器内部机制的深入理解。只有当您需要深入内部时,情况才确实如此。如果您曾经编写过一系列实用函数,并希望可以通过一条命令安装它们,那么您已经在考虑扩展了。打包的意义正在于此。

我们的 block_ddl 扩展演示了自定义配置表、角色、函数、视图和事件触发器。所有这些都是任何 Postgres 用户已经知道的标准 SQL 原语。唯一的新增部分是最小化的控制文件和 Makefile。只需要几行额外开销,就能获得干净的安装和卸载、版本管理和依赖跟踪。

如果您有一批函数、视图或触发器需要部署到环境中的每个数据库,请考虑花一个下午的时间将它们包装成一个扩展。您未来的自己,以及任何其他继承这些数据库的人,可能会感谢您这样做。

相关推荐
【心态好不摆烂】1 小时前
MySQL数据类型
数据库·mysql
码云骑士1 小时前
jwt入门介绍
linux·运维·数据库
努力努力再努力wz1 小时前
【Redis 入门系列】为什么需要 Redis?一文串起缓存、分布式、读写分离、分库分表与微服务
数据库·redis·分布式·sql·mysql·缓存·微服务
得闲喝茶1 小时前
SQL处理数据的常用语法语句
数据库·笔记·sql·数据分析·excel
金玉满堂@bj1 小时前
PostgreSQL:企业级全能开源数据库
数据库·postgresql·开源
todoitbo2 小时前
CTE 与外层 JOIN 的条件下推:原理、边界与验证方法
数据库·join·cte
TEC_INO2 小时前
Linux56:读取人脸图片并把特征值保存到sqlite3数据库
数据库·oracle
刘~浪地球2 小时前
MongoDB分片集群实战:水平扩展海量数据
数据库·mongodb
鸽芷咕2 小时前
KingbaseES中的PL_SQL编程:存储过程、函数、触发器与包的开发指南
数据库·sql·oracle