sqlx Code Review
Review Workflow
- 1. Check Cargo.toml — Note sqlx features (
runtime-tokio, tls-rustls/tls-native-tls, postgres/mysql/sqlite, uuid, chrono, json, migrate) and Rust edition (2024 changes RPIT lifetime capture and removes need for async-trait) - Check query patterns — Compile-time checked (
query!, query_as!) vs runtime (query, query_as) - Check pool configuration — Connection limits, timeouts, idle settings
- Check migrations — File naming, reversibility, data migration safety
- Check type mappings — Rust types align with SQL column types
Output Format
Report findings as:
CODEBLOCK0
Quick Reference
| Issue Type | Reference |
|---|
| Query macros, bind parameters, result mapping | references/queries.md |
| Migrations, pool config, transaction patterns |
references/migrations.md |
Review Checklist
Query Patterns
- - [ ] Compile-time checked queries (
query!, query_as!) used where possible - [ ]
sqlx.toml or DATABASE_URL configured for offline compile-time checking - [ ] No string interpolation in queries (SQL injection risk) — use bind parameters (
$1, $2) - [ ]
query_as! maps to named structs, not anonymous records, for public APIs - [ ]
.fetch_one(), .fetch_optional(), .fetch_all() chosen appropriately - [ ]
.fetch() (streaming) used for large result sets
Connection Pool
- - [ ]
PgPool shared via Arc or framework state (not created per-request) - [ ] Pool size configured for the deployment (not left at defaults in production)
- [ ] Connection acquisition timeout set
- [ ] Idle connection cleanup configured
- [ ] Edition 2024: Pool initialization uses
std::sync::LazyLock (not once_cell::sync::Lazy or lazy_static!) for static pool singletons
Transactions
- - [ ]
pool.begin() used for multi-statement operations - [ ] Transaction committed explicitly (not relying on implicit rollback on drop)
- [ ] Errors within transactions trigger rollback before propagation
- [ ] Nested transactions use savepoints (
tx.begin()) if needed
Type Mapping
- - [ ]
sqlx::Type derives match database column types - [ ] Enum representations consistent between Rust, serde, and SQL
- [ ]
Uuid, DateTime<Utc>, Decimal types used (not strings for structured data) - [ ]
Option<T> used for nullable columns - [ ]
serde_json::Value used for JSONB columns - [ ] No enum variants or struct fields named
gen — reserved keyword in edition 2024 (use r#gen with #[sqlx(rename = "gen")] or choose a different name)
Edition 2024 Compatibility
- - [ ] Functions returning
-> impl Stream or -> impl Future account for RPIT lifetime capture changes (all in-scope lifetimes captured by default; use + use<'a> for precise control) - [ ] Custom
FromRow or Type trait impls use native async fn in traits where applicable (no #[async_trait] needed, stable since Rust 1.75) - [ ] Prefer
#[expect(unused)] over #[allow(unused)] for compile-time query fields only used in some code paths (self-cleaning lint suppression, stable since 1.81) - [ ] Static pool initialization uses
std::sync::LazyLock (not once_cell or lazy_static!)
Migrations
- - [ ] Migration files follow naming convention (
YYYYMMDDHHMMSS_description.sql) - [ ] Destructive migrations (DROP, ALTER DROP COLUMN) are reversible or have data backup plan
- [ ] No data-dependent schema changes in same migration as data changes
- [ ]
sqlx::migrate!() called at application startup
Severity Calibration
Critical
- - String interpolation in SQL queries (SQL injection)
- Missing transaction for multi-statement writes (partial writes on error)
- Connection pool created per-request (connection exhaustion)
- Missing bind parameter escaping
Major
- - Runtime queries (
query()) where compile-time (query!()) could verify correctness - Missing transaction rollback on error paths
- Enum type mismatch between Rust and database
- Unbounded
.fetch_all() on potentially large tables - Field or variant named
gen without r#gen escape (edition 2024 compile failure)
Minor
- - Pool defaults used in production without tuning
- Missing
.fetch_optional() (using .fetch_one() then handling error for "not found") - Overly broad
SELECT * when only specific columns needed - Missing indexes for queried columns (flag only if query pattern is clearly slow)
- Edition 2024:
once_cell::sync::Lazy or lazy_static! used where std::sync::LazyLock works - Using
#[allow(unused)] instead of #[expect(unused)] for query fields (prefer self-cleaning lint suppression)
Informational
- - Suggestions to use
query_as! for type-safe result mapping - Suggestions to add database-level constraints alongside Rust validation
- Migration organization improvements
Valid Patterns (Do NOT Flag)
- - Runtime
query() for dynamic queries — Compile-time checking doesn't work with dynamic SQL sqlx::FromRow derive — Valid alternative to query_as! for reusable row typesTEXT columns for enum storage — Valid with sqlx::Type derive, simpler than custom SQL types.execute() ignoring row count — Acceptable for idempotent operations (upserts, deletes)- Shared DB with other languages — e.g., Elixir owns migrations, Rust reads. This is a valid architecture.
r#gen with #[sqlx(rename = "gen")] — Correct edition 2024 workaround for gen columns in database types+ use<'a> on query helper return types — Precise RPIT lifetime capture (edition 2024)std::sync::LazyLock for static pool initialization — Replaces once_cell/lazy_static (stable since Rust 1.80)- Native
async fn in custom FromRow/Type trait impls — async-trait crate no longer needed (stable since Rust 1.75)
Before Submitting Findings
Load and follow beagle-rust:review-verification-protocol before reporting any issue.
sqlx 代码审查
审查工作流程
- 1. 检查 Cargo.toml — 注意 sqlx 特性(runtime-tokio、tls-rustls/tls-native-tls、postgres/mysql/sqlite、uuid、chrono、json、migrate)和 Rust 版本(2024 版更改了 RPIT 生命周期捕获,不再需要 async-trait)
- 检查查询模式 — 编译时检查(query!、queryas!)与运行时检查(query、queryas)
- 检查连接池配置 — 连接限制、超时设置、空闲设置
- 检查迁移 — 文件命名、可逆性、数据迁移安全性
- 检查类型映射 — Rust 类型与 SQL 列类型对齐
输出格式
按以下格式报告发现:
text
[文件:行号] 问题标题
严重级别:严重 | 主要 | 次要 | 参考信息
问题描述及其重要性说明。
快速参考
references/migrations.md |
审查清单
查询模式
- - [ ] 尽可能使用编译时检查的查询(query!、queryas!)
- [ ] 配置了 sqlx.toml 或 DATABASEURL 用于离线编译时检查
- [ ] 查询中没有字符串插值(SQL 注入风险)—— 使用绑定参数($1、$2)
- [ ] 对于公共 API,queryas! 映射到命名结构体,而非匿名记录
- [ ] 适当选择 .fetchone()、.fetchoptional()、.fetchall()
- [ ] 对于大型结果集使用 .fetch()(流式处理)
连接池
- - [ ] 通过 Arc 或框架状态共享 PgPool(非每次请求创建)
- [ ] 根据部署环境配置连接池大小(生产环境不保留默认值)
- [ ] 设置了连接获取超时
- [ ] 配置了空闲连接清理
- [ ] 2024 版:静态连接池单例使用 std::sync::LazyLock 初始化(非 oncecell::sync::Lazy 或 lazystatic!)
事务
- - [ ] 多语句操作使用 pool.begin()
- [ ] 显式提交事务(不依赖 drop 时的隐式回滚)
- [ ] 事务内的错误在传播前触发回滚
- [ ] 需要时使用保存点(tx.begin())处理嵌套事务
类型映射
- - [ ] sqlx::Type 派生与数据库列类型匹配
- [ ] Rust、serde 和 SQL 之间的枚举表示一致
- [ ] 使用 Uuid、DateTime、Decimal 类型(非字符串表示结构化数据)
- [ ] 可空列使用 Option
- [ ] JSONB 列使用 serde_json::Value
- [ ] 没有名为 gen 的枚举变体或结构体字段 —— 2024 版中的保留关键字(使用 r#gen 配合 #[sqlx(rename = gen)] 或选择其他名称)
2024 版兼容性
- - [ ] 返回 -> impl Stream 或 -> impl Future 的函数考虑 RPIT 生命周期捕获变化(默认捕获所有作用域内的生命周期;使用 + use 进行精确控制)
- [ ] 自定义 FromRow 或 Type trait 实现中,适用时使用 trait 中的原生 async fn(无需 #[asynctrait],自 Rust 1.75 起稳定)
- [ ] 对于仅在某些代码路径中使用的编译时查询字段,优先使用 #[expect(unused)] 而非 #[allow(unused)](自清理 lint 抑制,自 1.81 起稳定)
- [ ] 静态连接池初始化使用 std::sync::LazyLock(非 oncecell 或 lazy_static!)
迁移
- - [ ] 迁移文件遵循命名约定(YYYYMMDDHHMMSS_description.sql)
- [ ] 破坏性迁移(DROP、ALTER DROP COLUMN)可逆或有数据备份计划
- [ ] 数据变更与依赖数据的模式变更不在同一迁移中
- [ ] 在应用程序启动时调用 sqlx::migrate!()
严重级别校准
严重
- - SQL 查询中的字符串插值(SQL 注入)
- 多语句写入缺少事务(错误时部分写入)
- 每次请求创建连接池(连接耗尽)
- 缺少绑定参数转义
主要
- - 在编译时查询(query!())可验证正确性的情况下使用运行时查询(query())
- 错误路径上缺少事务回滚
- Rust 与数据库之间的枚举类型不匹配
- 对可能很大的表使用无限制的 .fetch_all()
- 名为 gen 的字段或变体未使用 r#gen 转义(2024 版编译失败)
次要
- - 生产环境使用未经调整的连接池默认值
- 缺少 .fetchoptional()(使用 .fetchone() 然后处理未找到错误)
- 只需特定列时使用过于宽泛的 SELECT *
- 查询列缺少索引(仅在查询模式明显缓慢时标记)
- 2024 版:在 std::sync::LazyLock 可用的情况下使用 oncecell::sync::Lazy 或 lazystatic!
- 对查询字段使用 #[allow(unused)] 而非 #[expect(unused)](优先使用自清理 lint 抑制)
参考信息
- - 建议使用 query_as! 进行类型安全的结果映射
- 建议在 Rust 验证之外添加数据库级别约束
- 迁移组织改进
有效模式(请勿标记)
- - 动态查询的运行时 query() —— 编译时检查不适用于动态 SQL
- sqlx::FromRow 派生 —— 对于可重用行类型是 queryas! 的有效替代
- 用于枚举存储的 TEXT 列 —— 配合 sqlx::Type 派生有效,比自定义 SQL 类型更简单
- .execute() 忽略行计数 —— 对于幂等操作(upserts、deletes)可接受
- 与其他语言共享数据库 —— 例如,Elixir 拥有迁移,Rust 只读。这是有效的架构。
- r#gen 配合 #[sqlx(rename = gen)] —— 数据库类型中 gen 列的正确 2024 版解决方案
- 查询辅助返回类型上的 + use —— 精确的 RPIT 生命周期捕获(2024 版)
- 用于静态连接池初始化的 std::sync::LazyLock —— 替代 oncecell/lazy_static(自 Rust 1.80 起稳定)
- 自定义 FromRow/Type trait 实现中的原生 async fn —— 不再需要 async-trait crate(自 Rust 1.75 起稳定)
提交发现前
在报告任何问题之前,加载并遵循 beagle-rust:review-verification-protocol。