202510
database-systems

使用 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()
}

代码解析:

  1. #[pg_extern]:这个宏是 pgrx 的核心,它告诉 pgrx 将这个 Rust 函数 find_non_compliant_tables 暴露成一个同名的 SQL 函数。
  2. -> impl std::iter::Iterator<Item = String>:这个返回类型告诉 pgrx,该函数将返回一个行的集合(SETOF),每一行包含一个 String 类型的值,这在 SQL层面对应为 text
  3. Spi::connect(...):这是 pgrx 提供的安全访问 PostgreSQL 内部 SPI 的接口。所有数据库查询都应在此闭包内完成。它能确保正确的事务和内存上下文管理。
  4. client.select(...):执行 SQL 查询。我们使用了参数化查询,将 expected_prefix 安全地传递给 SQL,有效避免了 SQL 注入风险。
  5. row.get_datum_by_name(...):从结果行中按列名安全地提取数据,并自动将其转换为指定的 Rust 类型(这里是 String)。

4. 编译、安装和使用

现在我们的规则代码已经完成,让我们来部署它。

  1. 编译和安装:在项目根目录下运行 cargo pgrx install。这个命令会编译你的 Rust 代码,并自动将生成的扩展文件安装到 pgrx 管理的 PostgreSQL 实例中。

    cargo pgrx install
    
  2. 启动测试数据库并连接:使用 cargo pgrx run 启动一个临时的 PostgreSQL 实例,并自动打开一个 psql 终端连接上去。

    cargo pgrx run
    
  3. 创建扩展并测试:在 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_atupdated_at 字段。
  • 索引检查:确保所有外键列都已建立索引。
  • 权限审计:检查 public schema 下是否存在不应有的对象或权限。

将这些自定义规则集成到你的 CI/CD 流程中,可以在每次数据库迁移后自动运行检查,从而实现真正的“数据库即代码”和持续的质量保障,将潜在问题扼杀在开发阶段。