从 0 到 1 搭好 sqlc 质量门禁:用 vet 规则拦住坏 SQL

发布时间:2026/10/31 1:58:31
从 0 到 1 搭好 sqlc 质量门禁:用 vet 规则拦住坏 SQL

从 0 到 1 搭好 sqlc 质量门禁:用 vet 规则拦住坏 SQL

【免费下载链接】sqlcGenerate type-safe code from SQL项目地址: https://gitcode.com/gh_mirrors/sq/sqlc

上周一个"查最近 7 天订单"的接口上线后开始变慢,定位半天才发现:新加的查询没走索引,全表扫了 2000 万行。问题不在代码,而在 SQL——而你的代码评审流程恰好没有人专门看 SQL。sqlc 的vet子命令就是为解决这件事设计的:它把质量规则写进sqlc.yaml,每次跑sqlc vet时逐条校验你的查询,不合格的查询直接报错、退出码非 0。这篇文章带你配好一套 sqlc 质量门禁,让这类问题在进主干之前就被拦住。

30 秒看懂 sqlc vet

sqlc vet会读取 sqlc 配置中定义的规则,把每条规则当成一个判断表达式,逐条套在包的每条查询上;表达式结果为true就报一条错误。规则用 CEL 表达式书写,所以既能查文本模式,也能查执行计划。

它能检查什么,先看这张表:

检查项作用是否需要连数据库
查询文本匹配拦截DELETE、裸SELECT *这类反模式否
查询元信息限制参数个数、禁用某类cmd(如:exec)否
sqlc/db-prepare内置规则让数据库真正PREPARE每条查询,列不存在、类型不匹配当场暴露是
postgresql.explain/mysql.explain拿真实EXPLAIN执行计划,判断成本、顺序扫描、是否走主键是(PostgreSQL / MySQL)

不连库就能跑的规则适合所有项目;连库后的检查才谈得上"质量门禁"的完整形态。

从零搭一套质量检查

整套配置就放在sqlc.yaml里,参考 vet 文档。下面按动作展开。

连上数据库。在sql包下加database.uri,并启用内置规则:

version: '2' sql: - schema: "schema.sql" queries: "query.sql" engine: "postgresql" database: uri: "postgresql://postgres:postgres@localhost:5432/postgres" rules: - sqlc/db-prepare

跑sqlc vet时,sqlc 会连接这个库并对每条查询执行PREPARE。注意:sqlc 不会自动建表,库里的 schema 必须和schema.sql一致,先用你现有的迁移工具把表建好;或者配置 managed databases 让 sqlc 自动创建带正确 schema 的临时库。

写规则。规则定义在全局的rules:列表,每个规则三个字段:name(唯一标识)、message(命中时报给开发者的话)、rule(CEL 表达式)。然后在你希望生效的包下用rules:按名字启用——定义了但没启用的规则不会跑:

rules: - name: no-full-scan message: "列表查询请限制返回行数" rule: "query.cmd == 'many' && !query.sql.contains('LIMIT')" - name: cost-guard message: "查询成本超阈值,请检查执行计划" rule: "postgresql.explain.plan.total_cost > 300.0"

校验。在本地跑sqlc vet,每条命中的查询会打印成文件名: 查询名: 规则名: message的形式,只要有命中,退出码就是 1。想给个别查询豁免,在 SQL 注释里加一行即可:

/* name: BackfillFix :exec */ /* @sqlc-vet-disable cost-guard */ UPDATE orders SET status = 'paid' WHERE id = ?;

@sqlc-vet-disable不带参数则跳过该查询的所有规则。

规则表达式怎么写

规则表达式能拿到三类变量:config(引擎、schema 路径等)、query(SQL 文本、名字、cmd、参数列表)、postgresql.explain/mysql.explain(执行计划,仅连库且引擎匹配时可用)。拆三个高频写法:

查文本与元信息(不连库)。query变量对应结构很简单:

字段含义
query.sql查询 SQL 全文,支持.contains()等字符串操作
query.name/* name: ... */注解里的名字
query.cmdmany/one/exec等
query.params.size()参数个数

组合起来就能表达"多行查询必须带 LIMIT":

rule: "query.cmd == 'many' && !query.sql.contains('LIMIT')"

查执行计划成本(连 PostgreSQL)。sqlc 会对查询跑EXPLAIN (VERBOSE, COSTS, BUFFERS, FORMAT JSON)并解析成结构体,plan.total_cost是最常用的成本估计值:

rule: "postgresql.explain.plan.total_cost > 300.0"

查顺序扫描(连 PostgreSQL)。plan.node_type是执行计划的节点类型,判断顶层是否为全表扫描:

rule: "postgresql.explain.plan.node_type == 'Seq Scan'"

MySQL 侧结构不同,成本在query_block.cost_info里,注意先判存在再取值:

rule: "has(mysql.explain.query_block.cost_info) && double(mysql.explain.query_block.cost_info.query_cost) > 2.0"

字段拿不准时,别猜:设SQLCDEBUG=dumpexplain=1再跑sqlc vet,sqlc 会把每条查询的原始EXPLAINJSON 打印出来,照着 JSON 写表达式就行。完整字段定义见 vet 参考 和仓库protos/vet/vet.proto。

接入 CI/CD 让检查自动跑

本地跑得通还不够,门禁必须挂到流水线上。最小命令序列是sqlc vet(规则校验)加sqlc diff(确认生成代码没被手改、没漏跑generate),两个都失败即退出码 1,天然适合当 CI 卡点:

name: sqlc on: [push] jobs: vet: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest - run: sqlc vet env: # 规则里用 ${VET_TEST_DB_URI} 这类占位符,这里注入 CI 的库地址 VET_TEST_DB_URI: "postgresql://postgres:postgres@localhost:5432/postgres" - run: sqlc diff

用uri: "${ENV_VAR}"把数据库地址交给环境变量,本地和 CI 共用一份配置。vet的退出码直接决定这一步红不绿,不需要额外脚本。

这些坑我替你踩过了

现象原因对策
sqlc/db-prepare报错,但本地查询明明能跑sqlc 不执行迁移,库里的 schema 落后于schema.sql跑 vet 前先执行迁移,或改用 managed databases 自动建临时库
explain 类规则报database connection required用了postgresql.explain但包里没配database该包补database.uri;只想查文本的话删掉 explain 规则
某条规则误伤历史查询,团队要求豁免规则一刀切,老代码一时改不完查询上加@sqlc-vet-disable 规则名精确豁免,别用无参版跳掉全部检查
阈值调多少没底拍脑袋设阈值,要么全红要么全绿先SQLCDEBUG=dumpexplain=1看现有查询的真实成本分布,取一个明显高于现状的起点,再逐步收紧
规则里直接引用 explain 字段,部分查询报错不同查询的EXPLAINJSON 结构不同,字段可能不存在引用前加has(...)判空,参考 MySQL 那条写法的has() &&组合
团队规则越加越乱,没人敢删规则没有 owner 和文档每条规则的message写清"为什么禁 + 怎么改",每季度按误报率清一次

规则宁少勿滥:一条总是红的规则会让开发者绕开vet,整个门禁形同虚设。

写在最后

先不连库,把文本类规则跑红、跑绿、跑进 CI;确认稳定后再连库加sqlc/db-prepare和 explain 规则,阈值从宽松起步逐步收紧。质量门禁不是一次会议搭出来的,而是随查询一起长出来的。更多细节看 官方文档。

【免费下载链接】sqlcGenerate type-safe code from SQL项目地址: https://gitcode.com/gh_mirrors/sq/sqlc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考