使用 pgrx 开发自定义 PostgreSQL Linter 规则
pglinter 提供了强大的内建检查,但项目总有特殊需求。本文将介绍如何使用 Rust 和 pgrx 框架来开发独立的自定义 linting 规则,以强制执行项目特定的数据库模式约定。
前言:当内建规则不够用时
PostgreSQL 扩展 pglinter
是一个出色的数据库健康检查工具,它通过一系列预定义的规则,帮助开发者和运维团队发现潜在的设计缺陷、性能问题和安全隐患。然而,任何标准化的工具都无法覆盖所有组织的内部规范。例如,您可能要求所有表都必须以 tbl_
开头,或者特定的列(如 created_at
)必须带有默认值。
pglinter
本身是一个使用 Rust 和 pgrx
框架构建的闭环扩展,它并未提供一个公开的插件 API 来让开发者动态添加自己的规则。但这并不意味着我们束手无策。我们可以借鉴其实现思路,利用同样强大的 pgrx
框架,构建一个独立的、轻量级的 PostgreSQL 扩展,来承载我们自己的自定义 linting 规则。
本文将详细介绍如何从零开始,使用 Rust 和 pgrx
创建一个自定义的 linting 规则。我们将以一个具体的例子——检查所有表名是否都以 app_
为前缀——来贯穿整个开发和部署流程。
pgrx
:Rust 与 PostgreSQL 的桥梁
pgrx
是一个用于安全、高效地构建 PostgreSQL 扩展的 Rust 框架。它极大地简化了开发过程,屏蔽了与 PostgreSQL C 语言 FFI (Foreign Function Interface) 交互的复杂性。借助 pgrx
,我们可以利用 Rust 的内存安全、强类型系统和现代化的工具链来编写高性能的数据库函数。
核心优势包括:
- 安全至上:
pgrx
会自动将 Rust 的panic!
转化为 PostgreSQL 的ERROR
,这意味着你的扩展中的一个错误只会中止当前事务,而不会导致整个数据库进程崩溃。 - 自动化代码生成: 通过简单的宏(如
#[pg_extern]
),pgrx
能够自动生成将 Rust 函数暴露为 SQL 用户定义函数 (UDF) 所需的全部粘合代码。 - 简化的开发流程:
cargo-pgrx
插件提供了一站式的解决方案,涵盖了项目创建、环境初始化、测试和部署打包等所有环节。
实战:开发一个表名规范检查器
我们的目标是创建一个 SQL 函数 find_non_compliant_tables() -> SETOF text
,它会返回所有不符合“以 app_
为前缀”命名规范的表。
1. 环境准备
首先,确保你已经安装了 Rust 工具链和 pgrx
的开发环境。pgrx
的 GitHub 仓库 (https://github.com/pgcentralfoundation/pgrx) 提供了详细的安装指南。
核心步骤如下:
# 1. 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. 安装 pgrx 的 Cargo 插件
cargo install --locked cargo-pgrx
# 3. 初始化 pgrx,它会自动下载并编译所需的 PostgreSQL 版本
# 这一步会花费较长时间
cargo pgrx init
2. 创建新扩展项目
使用 cargo-pgrx
创建一个新的扩展项目。我们将它命名为 custom_linter
。
cargo pgrx new custom_linter
cd custom_linter
pgrx
会为我们生成一个完整的项目骨架,其中 src/lib.rs
是我们的主战场。
3. 编写核心 linting 逻辑
打开 src/lib.rs
,我们将在这里编写规则的核心逻辑。其本质是编写一个 Rust 函数,通过 pgrx
提供的 SPI (Server Programming Interface) 功能来执行一条 SQL 查询,以检查 PostgreSQL 的系统目录。
// src/lib.rs
use pgrx::prelude::*;
// 引入 pgrx 的魔法,这是每个 pgrx 扩展所必需的
pg_module_magic!();
#[pg_extern]
fn find_non_compliant_tables() -> impl std::iter::Iterator<Item = String> {
// 定义我们期望的表名前缀
let expected_prefix = "app_";
let mut non_compliant_tables = Vec::new();
// 使用 Spi::connect 开启一个事务上下文,用于执行后续的 SQL 查询
// 这是与数据库交互的安全方式
let result = Spi::connect(|client| {
// 查询 pg_class 系统目录,找出所有普通表(relkind = 'r')
// 并且这些表不属于系统 schema(如 pg_catalog, information_schema)
let query = "
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON c.relnamespace = n.oid
WHERE c.relkind = 'r'
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND n.nspname NOT LIKE 'pg_toast%'
AND NOT c.relname LIKE $1;
";
// 使用带有参数的安全查询方式,防止 SQL 注入
// 参数 $1 将被格式化为 'app_%'
let mut cursor = client.select(query, None, Some(vec![
(PgBuiltInOids::TEXTOID.oid(), format!("{}%", expected_prefix).into_datum()),
]));
// 遍历查询结果
while let Some(row) = cursor.next() {
// .get_datum_by_name(col_name) 会返回 Result<Option<T>, Error>
// 我们通过 .unwrap().unwrap() 来获取值,这在确定列存在且不为 NULL 时是安全的
let table_name: String = row.get_datum_by_name("relname").unwrap().unwrap();
non_compliant_tables.push(table_name);
}
Ok(Some(()))
});
if result.is_err() {
// 如果 SPI 查询失败,可以记录日志或提前返回
// 为简化起见,我们这里仅返回一个空列表
return Vec::new().into_iter();
}
// 将结果向量转换为迭代器返回
// pgrx 会自动处理 `SETOF <type>` 的返回形式
non_compliant_tables.into_iter()
}
代码解析:
#[pg_extern]
:这个宏是pgrx
的核心,它告诉pgrx
将这个 Rust 函数find_non_compliant_tables
暴露成一个同名的 SQL 函数。-> impl std::iter::Iterator<Item = String>
:这个返回类型告诉pgrx
,该函数将返回一个行的集合(SETOF
),每一行包含一个String
类型的值,这在 SQL层面对应为text
。Spi::connect(...)
:这是pgrx
提供的安全访问 PostgreSQL 内部 SPI 的接口。所有数据库查询都应在此闭包内完成。它能确保正确的事务和内存上下文管理。client.select(...)
:执行 SQL 查询。我们使用了参数化查询,将expected_prefix
安全地传递给 SQL,有效避免了 SQL 注入风险。row.get_datum_by_name(...)
:从结果行中按列名安全地提取数据,并自动将其转换为指定的 Rust 类型(这里是String
)。
4. 编译、安装和使用
现在我们的规则代码已经完成,让我们来部署它。
-
编译和安装:在项目根目录下运行
cargo pgrx install
。这个命令会编译你的 Rust 代码,并自动将生成的扩展文件安装到pgrx
管理的 PostgreSQL 实例中。cargo pgrx install
-
启动测试数据库并连接:使用
cargo pgrx run
启动一个临时的 PostgreSQL 实例,并自动打开一个psql
终端连接上去。cargo pgrx run
-
创建扩展并测试:在
psql
提示符中,我们首先创建一些测试表(包括合规和不合规的),然后创建我们的扩展,并调用我们新定义的函数。-- 创建一些测试数据 CREATE TABLE app_users (id serial primary key); -- 合规 CREATE TABLE app_products (id serial primary key); -- 合规 CREATE TABLE orders (id serial primary key); -- 不合规 CREATE TABLE customer_details (info text); -- 不合规 -- 启用我们的自定义 linter 扩展 CREATE EXTENSION custom_linter; -- 调用 linting 函数,查找不合规的表 SELECT * FROM find_non_compliant_tables();
执行结果应该如下所示:
find_non_compliant_tables --------------------------- orders customer_details (2 rows)
这个结果准确地列出了所有未使用 app_
前缀的表,证明我们的自定义 linter 规则已经成功运行!
结论与展望
通过 pgrx
,我们仅用几十行 Rust 代码就成功地创建了一个功能完备的、针对特定业务规则的 PostgreSQL linting 扩展。这种方法不仅性能高、内存安全,而且开发体验远胜于传统的 C 语言扩展开发。
虽然我们只实现了一个简单的命名规范检查,但你可以基于此模式扩展出更复杂的规则,例如:
- 强制存在特定列:检查所有表中是否都包含
created_at
和updated_at
字段。 - 索引检查:确保所有外键列都已建立索引。
- 权限审计:检查
public
schema 下是否存在不应有的对象或权限。
将这些自定义规则集成到你的 CI/CD 流程中,可以在每次数据库迁移后自动运行检查,从而实现真正的“数据库即代码”和持续的质量保障,将潜在问题扼杀在开发阶段。