
从 Eugene 移植 PostgreSQL 迁移安全规则到 postgres-language-server 的完整实践指南【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp导读本文基于 agentic/port_eugene_rules.md 展开系统讲解如何把 Eugene 这一 PostgreSQL 迁移 linter 的规则逐条移植到 postgres-language-server 的pgls_analyser分析器。你将掌握移植前的规则理解流程、用just new-lintrule脚手架生成规则骨架、基于pgls_query完整 PostgreSQL protobuf AST 实现规则逻辑、通过expect_lint/expect_no_diagnostics编写规格测试并验收快照以及如何处理 schema 归一化、跨语句文件上下文和事务状态跟踪等常见坑位。背景为什么要移植 Eugene 规则Eugene 是一个专注于检测 PostgreSQL 迁移中危险操作的 lint 工具其规则覆盖了诸如向已有表添加 serial 列导致全表重写并发创建索引在事务中长时间持有 ACCESS EXCLUSIVE 锁等高风险场景。postgres-language-server 的目标是把这些安全检查内建到语言服务器环境中——让开发者在 IDE 里写迁移脚本时pgls_analyser就能实时给出与 Eugene 等价的诊断而不必单独跑一遍外部工具。移植过程中需要同时参照 Eugene 仓库中的三处关键位置规则实现eugene/eugene/src/lints/rules.rs每个规则的函数实现例如added_serial_column提示元数据eugene/eugene/src/hint_data.rs规则的 ID如E11以及 name、condition、effect、workaround 等文档文本示例 SQLeugene/eugene/examples/ID/目录下的bad.sql应触发规则的无效用例与good.sql不应触发的有效用例这些位置分别对应了移植后三个产物的来源规则逻辑、诊断消息与文档内容、测试用例。移植的完整流程第一步理解规则在动手写任何 Rust 代码之前先彻底读懂 Eugene 侧的规则实现在eugene/eugene/src/lints/rules.rs中找到对应规则函数如added_serial_column理解它匹配的 AST 模式以及是否有特殊逻辑例如需要检查之前的语句阅读hint_data.rs中该规则元数据ID、名称、触发条件、影响与规避建议这部分将直接变成移植后规则的文档内容查看eugene/eugene/examples/ID/下的bad.sql与good.sql作为测试用例的素材来源。第二步用脚手架创建规则pgls_analyser的代码生成管线见 crates/pgls_analyser/CONTRIBUTING.md要求新建规则时同步更新多个文件。项目用 just 中的new-lintrulerecipe# 创建规则severity 可选取值 error默认/ warn / info just new-lintrule safety ruleName severity # 示例移植 Eugene 的 E11添加 serial 列 just new-lintrule safety addSerialColumn error该命令会生成crates/pgls_analyser/src/lint/safety/rule_name.rs规则实现文件crates/pgls_analyser/tests/specs/safety/ruleName/basic.sql初始测试文件更新配置文件与诊断类别注册crates/pgls_analyser/src/registry.rs等由 xtask/codegen/src/generate_analyser.rs 生成注意 severity 的取值是info、warn、error而不是warning——这是移植 Squawk 规则时踩过的坑同样适用于 Eugene 移植记录于 agentic/port_squawk_rules.md 的 LEARNINGS。第三步实现规则规则文件的核心结构如下以文档中的模板为骨架与仓库中 add_serial_column.rs 的实际情况保持一致use crate::{LinterDiagnostic, LinterRule, LinterRuleContext}; use pgls_analyse::{RuleSource, declare_lint_rule}; use pgls_console::markup; use pgls_diagnostics::Severity; declare_lint_rule! { /// 单行简要描述会显示在规则列表中。 /// /// 详细说明规则检测什么、为什么有问题以及 PostgreSQL 的行为与性能/安全影响。 /// /// ## Examples /// /// ### Invalid /// /// sql,expect_diagnostic /// -- 应触发规则的 SQL /// ALTER TABLE users ADD COLUMN id serial; /// /// /// ### Valid /// /// sql /// -- 不应触发规则的 SQL /// CREATE TABLE users (id serial PRIMARY KEY); /// /// pub RuleName { version: next, name: ruleName, severity: Severity::Error, // 或 Warning recommended: true, // 或 false sources: [RuleSource::Eugene(ID)], // 例如 E11 } } impl LinterRule for RuleName { type Options (); fn run(ctx: LinterRuleContextSelf) - VecLinterDiagnostic { let mut diagnostics Vec::new(); // 按语句类型做模式匹配 if let pgls_query::NodeEnum::AlterTableStmt(stmt) ctx.stmt() { // 规则逻辑 if /* condition */ { diagnostics.push( LinterDiagnostic::new( rule_category!(), None, markup! { 错误消息可用 Emphasis格式化/Emphasis强调。 }, ) .detail(None, 关于问题的补充上下文。) .note(建议的修复方式或规避办法。), ); } } diagnostics } }对照仓库中真实移植的AddSerialColumn规则crates/pgls_analyser/src/lint/safety/add_serial_column.rs可以看到两个关键实现细节SERIAL 类型判断通过get_type_name把TypeName.names中的多个String节点拼接成pg_catalog.serial之类的完整类型串再用is_serial_type匹配serial/bigserial/smallserial及其serial2/serial4/serial8别名和pg_catalog.前缀变体GENERATED ... STORED 判断遍历col_def.constraints检查ConstrType::ConstrGenerated且generated_when aALWAYS。诊断消息遵循项目对规则的三大支柱要求见 CONTRIBUTING.md先说明错误本身message再解释为什么触发.detail最后告诉用户应该怎么做.note或 code action。常用实现模式访问之前的语句用于multipleAlterTable这类需要跨语句状态的规则let file_ctx ctx.file_context(); let previous file_ctx.previous_stmts();Schema 归一化空 schema 视为publiclet schema_normalized if schema.is_empty() { public } else { schema.as_str() };检查特定 ALTER TABLE 子命令for cmd in stmt.cmds { if let Some(pgls_query::NodeEnum::AlterTableCmd(cmd)) cmd.node { if cmd.subtype() pgls_query::protobuf::AlterTableType::AtAddColumn { // 处理 ADD COLUMN } } }提取列类型if let Some(pgls_query::NodeEnum::ColumnDef(col_def)) cmd.def.as_ref().and_then(|d| d.node.as_ref()) { if let Some(type_name) col_def.type_name { let type_str get_type_name(type_name); } } fn get_type_name(type_name: pgls_query::protobuf::TypeName) - String { type_name .names .iter() .filter_map(|n| { if let Some(pgls_query::NodeEnum::String(s)) n.node { Some(s.sval.as_str()) } else { None } }) .collect::Vec_() .join(.) }multipleAlterTable移植自 Eugene W12见 multiple_alter_table.rs是文件上下文规则的代表作它遍历file_ctx.previous_stmts()对每个之前的语句检查是否为作用于同一 schema 同一表的AlterTableStmt命中则提示用户把多个 ALTER TABLE 合并为逗号分隔的单条语句避免 Postgres 多次扫描/重写表。第四步编写全面的测试测试目录位于crates/pgls_analyser/tests/specs/safety/ruleName/每个测试文件首行必须带有期望标注解析逻辑见 rules_tests.rs 中的Expectation::from_file应触发规则的用例-- expect_lint/safety/ruleName注意expect_lint期待恰好一个诊断详见下文坑位 5不应触发规则的用例-- expect_no_diagnostics文档推荐的测试结构示例tests/specs/safety/addSerialColumn/ ├── basic.sql # 基础触发用例 ├── bigserial.sql # bigserial 类型变体 ├── generated_stored.sql # GENERATED ... STORED 变体 ├── valid_regular_column.sql # 有效普通列 └── valid_create_table.sql # 有效CREATE TABLE 上下文运行测试并验收快照cargo insta test -p pgls_analyser --acceptpgls_test_macros::gen_tests!会扫描tests/specs/**/*.sql为每个文件生成独立测试rules_tests.rs每个用例同时产出.sql.snap快照与期望断言的双重校验。第五步验证与生成代码# 检查编译 cargo check # 生成 lint 代码与文档同时执行 analyser/configuration/bindings/schema-types/splinter/pglinter/docs 等 codegen见 justfile just gen-lint # 运行全部规则测试 cargo test -p pgls_analyser --test rules_tests # 最终验证含代码生成、格式化、clippy 与 git diff 检查 just ready第六步手动测试规则创建一个测试 SQL 文件-- test.sql ALTER TABLE users ADD COLUMN id serial;然后用 CLI 运行检查cargo run -p pgls_cli -- check /path/to/test.sql常见坑位与解决方案1. Schema 匹配不一致问题不同语句对同一表的 schema 标注可能不同public.users与users。解决统一归一化——空 schema 字符串一律视为public再比较multiple_alter_table.rs中即有此实现。2. AST 导航差异问题Eugene 使用简化 ASTStatementSummary而本仓库使用 libpg_query 的完整 PostgreSQL protobuf ASTpgls_querycrate。解决用if let模式匹配 辅助函数导航完整 AST以已有 pgt 规则为参考需要查类型/枚举时查看pgls_query::protobuf模块完整绑定在 crates/pgls_query/src/protobuf.rs。3. 文件上下文规则问题像multipleAlterTable这样需要跨语句跟踪状态的规则。解决用ctx.file_context()获取AnalysedFileContext通过previous_stmts()拿到当前语句之前的所有语句let file_ctx ctx.file_context(); let previous_stmts file_ctx.previous_stmts();AnalysedFileContext的完整定义linter_context.rs提供了stmts文件全部语句、previous_stmts()、stmt_count()、next()与transaction_state()等接口其pos字段记录当前正在分析的语句下标previous_stmts()返回self.stmts[0..self.pos]。4. 事务状态跟踪问题部分 Eugene 规则依赖事务状态如RUNNING_STATEMENT_WHILE_HOLDING_ACCESS_EXCLUSIVE检查是否持有 ACCESS EXCLUSIVE 锁。解决这一能力在仓库中已经落地。AnalysedFileContext内嵌了TransactionStatecrates/pgls_analyser/src/linter_context.rs跟踪lock_timeout_set/statement_timeout_set/idle_in_transaction_timeout_set对应SET lock_timeout等 GUC 设置VariableSetKind::VarResetAll会清除全部标记created_objects事务内创建的对象schema 归一化为publicholding_access_exclusive与access_exclusive_tables通过is_dangerous_lock_stmt/access_exclusive_table_for_alter判断危险锁not_valid_constraintsNOT VALID 约束登记供require_separate_constraint_validation等规则使用transaction_depth事务嵌套深度update_from_stmt在每处理一条语句时更新状态BEGIN/SAVEPOINT增加深度COMMIT/ROLLBACK减少深度并调用reset_transaction_state清空累积状态以避免跨事务误报ALTER TABLE的AtValidateConstraint子命令因只取 SHARE UPDATE EXCLUSIVE 锁而被排除在 ACCESS EXCLUSIVE 判定之外。基于该机制E4 对应的runningStatementWhileHoldingAccessExclusive规则已移植完成running_statement_while_holding_access_exclusive.rs其实现非常简洁tx_state.is_holding_access_exclusive()为真即产生警告。因此文档中事务跟踪需要先扩展AnalysedFileContext的旧建议已过时当前只需直接使用现有 API。5. 测试期望数量不符问题expect_lint期待恰好一个诊断但规则可能产生多个。解决要么拆分成多个测试文件每个只触发一次要么调整测试让其只触发一次也可仿照addSerialColumn文档中多条expect_diagnostic代码块的做法文档侧每条 invalid 片段必须且只能产生一个诊断见 CONTRIBUTING.md。规则映射考量规则重叠部分 Eugene 规则可能与 pgt 已有规则重叠移植前需要评审Eugene 规则可能的 PGT 重叠处理建议SET_COLUMN_TYPE_TO_JSONpreferJsonb两者都评审可能增强现有规则CREATE_INDEX_NONCONCURRENTLYrequireConcurrentIndexCreation两者都评审CHANGE_COLUMN_TYPEchangingColumnType两者都评审ADD_NEW_UNIQUE_CONSTRAINT_WITHOUT_USING_INDEXdisallowUniqueConstraint两者都评审重叠时的处理步骤对比两份实现若 Eugene 的实现更全面考虑更新现有规则若覆盖点不同则两条都保留记录差异。事务感知规则E4RUNNING_STATEMENT_WHILE_HOLDING_ACCESS_EXCLUSIVE与 E9LOCKTIMEOUT_WARNING需要跨多条语句跟踪事务状态。推荐路径先实现简单的单语句规则再基于TransactionState设计跨语句跟踪在逐语句处理过程中更新状态。如前所述TransactionState已就位E4 也已移植lockTimeoutWarning规则同样存在于 crates/pgls_analyser/src/lint/safety/lock_timeout_warning.rs。Eugene 简化 AST 与 PostgreSQL 完整 AST 的对比Eugene 的简化 ASTEugene 使用StatementSummary枚举做简化表示enum StatementSummary { AlterTable { schema: String, name: String, actions: VecAlterTableAction }, CreateIndex { schema: String, idxname: String, concurrently: bool, target: String }, // ... } enum AlterTableAction { AddColumn { column: String, type_name: String, stored_generated: bool, ... }, SetType { column: String, type_name: String }, // ... }PostgreSQL 完整 ASTpgls_query本项目使用 libpg_query 生成的完整 protobuf ASTpgls_query::NodeEnum::AlterTableStmt(stmt) - stmt.cmds: VecNode - NodeEnum::AlterTableCmd(cmd) - cmd.subtype: AlterTableType - cmd.def: OptionNode - NodeEnum::ColumnDef(col_def)翻译策略先看 Eugene 的简化逻辑映射到对应的 PostgreSQL AST 节点参考已有 pgt 规则在pgls_query::protobuf中查找可用的类型与枚举。由于两边都基于 libpg_query 解析 SQLSquawk 移植文档 agentic/port_squawk_rules.md 也确认了这一点AST 节点具有特性对等feature parity只是绑定形态不同——本仓库的完整绑定在pgls_query::protobuf模块。测试基础设施从源码看断言如何工作rules_tests.rscrates/pgls_analyser/tests/rules_tests.rs揭示了测试的全链路pgls_test_macros::gen_tests!遍历tests/specs/**/*.sql生成测试函数每个测试按目录结构解析出(group, rule, fname)三元组构造RuleFilter::Rule(group, rule)只启用目标规则避免其他规则干扰诊断计数用pgls_statement_splitter::split拆分语句逐条解析 AST 后交给Analyser同时做两件事把诊断写入快照insta以及按文件头注释解析Expectationexpect_no_diagnostics或expect_category计数做严格断言。这意味着每个 SQL 用例都受到快照内容 诊断数量的双重校验expect_系列标注必须与get_category_name()返回的类别名一致否则测试直接 panic。移植完成度核对清单移植新规则时逐项确认对应文档末的 Template Checklist并结合仓库当前状态规则实现在src/lint/safety/rule.rs文档含示例invalid 与 valid 用例invalid 用expect_diagnostic属性且只产生一个诊断sources: [RuleSource::Eugene(ID)]来源标注declare_lint_rule!宏的sources字段参见 CONTRIBUTING.md 中RuleSource::Squawk(ban-drop-column)的用法至少 3-5 个测试文件invalid 与 valid 混合用cargo insta test --accept验收快照cargo test -p pgls_analyser --test rules_tests全部通过cargo check编译干净just gen-lint完成代码生成用示例 SQL 做 CLI 手动测试在本文档中登记已完成规则下一步工作优先级优先移植高风险安全规则E1、E2、E6、E7、E9评审重叠检查 E3、E5、E6、E7 是否与现有规则重叠crates/pgls_analyser/src/lint/safety/ 目前已有 52 个规则文件事务跟踪基于已就位的TransactionState设计/完善 E4、E9 的事务状态跟踪E4 已完成E9 的lockTimeoutWarning亦已存在文档在移植后的规则中更新 Eugene 来源标注测试确保所有移植规则都有全面测试覆盖仓库现状可作为移植进度的参照crates/pgls_analyser/src/lint/safety/下已包含addSerialColumnE11、multipleAlterTableW12、runningStatementWhileHoldingAccessExclusiveE4、lockTimeoutWarningE9等来自 Eugene 的规则且TransactionState已为事务感知类规则提供了成熟的基础设施同类移植工作Squawk 规则的完成清单记录于 agentic/port_squawk_rules.md。更多规则编写规范可参考 crates/pgls_analyser/CONTRIBUTING.md 与 ARCHITECTURE.md。【免费下载链接】postgres_lspA Language Server for Postgres项目地址: https://gitcode.com/GitHub_Trending/po/postgres_lsp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考