前言:当内建规则不够用时
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) 提供了详细的安装指南。
核心步骤如下:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install --locked cargo-pgrx
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 的系统目录。
use pgrx::prelude::*;
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();
let result = Spi::connect(|client| {
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;
";
let mut cursor = client.select(query, None, Some(vec![
(PgBuiltInOids::TEXTOID.oid(), format!("{}%", expected_prefix).into_datum()),
]));
while let Some(row) = cursor.next() {
let table_name: String = row.get_datum_by_name("relname").unwrap().unwrap();
non_compliant_tables.push(table_name);
}
Ok(Some(()))
});
if result.is_err() {
return Vec::new().into_iter();
}
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);
CREATE EXTENSION custom_linter;
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 流程中,可以在每次数据库迁移后自动运行检查,从而实现真正的“数据库即代码”和持续的质量保障,将潜在问题扼杀在开发阶段。