从 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.cmd | many/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),仅供参考