Appearance
前言
五行代码与一座冰山
你写了一个 Rust 后端服务。查询用户信息的代码是这样的:
rust
let pool = PgPoolOptions::new()
.max_connections(20)
.connect(&database_url).await?;
let user: User = sqlx::query_as!(
User,
"SELECT id, name, email FROM users WHERE id = $1",
user_id
).fetch_one(&pool).await?;cargo check 通过了。你改了 SQL 里的 email 拼成 emial——编译器立刻在 query_as! 这一行报错:"column 'emial' does not exist"。你把 user_id 的 Rust 类型从 i32 改成 String——编译器又报错:"expected i32, found String"。整个过程里你根本没有跑过这条 SQL,但 Rust 编译器硬是替你把 SQL 的语法、列名、类型都核了一遍。
直到某一天,你想给一个 enum 类型的字段加自定义 Encode 和 Decode,发现派生宏的属性和 sqlx::Type 的 trait 约束缠绕在一起;你想让 CI 在没有 DATABASE_URL 的情况下也能编译,发现 .sqlx/ 目录里那一堆 JSON 是 cargo sqlx prepare 生成的离线缓存;你想搞清楚 Pool::acquire().await? 为什么在高并发下偶尔要花上几百毫秒(而你明明设了 max_connections = 50),翻到 sqlx-core/src/pool/inner.rs 发现空闲连接被 ArrayQueue 和 AsyncSemaphore 联合守卫,fair 模式下排队会阻塞前面的获取者;你想在一个 Transaction 的作用域快结束时根据业务结果决定 commit 还是 rollback,发现 Transaction<'c> 的 Drop 实现默认会尝试 rollback 但不能保证——必须显式调用 tx.commit().await?。
五行代码底下藏着一整座冰山。
为什么读源码
Rust 数据库生态有一个鲜明的特点:生态没有 ORM 的话语权,驱动才是。
sqlx 不是 ORM。它不帮你做 User.save()、不帮你做 has_many 关联、不帮你做查询 DSL。它只做一件事:把一条 SQL 和一次连接池调度,变成一次类型安全的 .await。在这个极简目标下,sqlx 把工业界这一层做到了极致:编译期校验、异步优先、零拷贝解码、运行时多态 Any、离线缓存、迁移系统、连接池、事务保护、tracing 集成——每一项都经过了最坏情况的打磨。
这意味着两件事。
第一件,你必须理解底层才能用好 sqlx。你在 sqlx::query! 里写的每一条 SQL、每一次 .bind()、每一个 #[derive(FromRow)],最终都会变成 Executor::fetch 或 Executor::execute 的一次调用。Pool 到 PoolConnection 的转换不是"拿一个连接对象"——它是 AsyncSemaphore::acquire 后拿到 permit、再从 ArrayQueue 弹出 Idle<DB>、再包成 PoolConnection 并设置 Drop 回调归还的一条流水线。理解这些,你才能在看到 PoolTimedOut 错误时准确判断是"连接池满了"还是"连接建立超时",而不是靠盲目调大 max_connections。
第二件,sqlx 是 Rust "编译期连接外部世界"的范本。query! 是 Rust 生态里最早也是最成功的把真实数据库 schema 带进类型系统的过程宏之一。它在 cargo check 阶段通过 DATABASE_URL 连回 Postgres 或 MySQL、发一个 DESCRIBE 消息、拿到列类型、然后把这些信息注入编译器的类型推导链。这条从"SQL 字符串字面量"到"带类型的 Future<Output = Result<User, Error>>"的路径,设计细节值得每一个想做过程宏的 Rust 作者仔细读一遍。
这本书不是什么
这本书不是 sqlx 使用手册。 sqlx::query! 怎么写、PgPoolOptions 有哪些方法、FromRow 的 #[sqlx(rename = "...")] 怎么用——这些官方文档和 examples 已经覆盖得很好。本书的每一章都在回答"为什么这样设计"和"底层怎么实现",而不是"怎么用"。
这本书不是 Tokio / async 入门。 本书假设你已经读过《Tokio 源码深度解析》,或者至少熟悉 Future、Waker、tokio::spawn 这些概念。第 13 章 Pool 会直接解释 AsyncSemaphore 与 Tokio 的 Semaphore 在 fair 语义上的差异,不会从零讲 Poll::Pending 是什么。
这本书不是 SQL 教程。 本书不会教你什么是 JOIN、什么是索引、什么是事务隔离级别。如果你对这些概念还感到陌生,建议先补齐 SQL 基础再回来。
这本书不是数据库内核书。 本书关心的是 sqlx 客户端的实现:协议编码、消息收发、预处理、连接池。数据库服务端的存储引擎、查询优化器、WAL——这些是 PostgreSQL / MySQL / SQLite 自己的内部,不是 sqlx 的关注点。
这本书是什么
这本书是 sqlx 0.8.6 的一份"读码日记"。 每一章都像是你和我对着屏幕,一起读 sqlx-core/src/pool/inner.rs 的 619 行、一起 grep impl Executor 看 Pool 的 fetch 实现、一起 git log 那个把 &mut Transaction 的 Executor impl 删掉的 commit(那正是 0.7 到 0.8 破坏性最大的变更之一)。2026 年 4 月锁定的 sqlx 0.8.6(tag v0.8.6、commit bab1b02)是我们共同的地图——每一处源码引用都会标注 文件:行号,你可以随时在自己的 clone 里对照验证。
这本书是一份"编译期触达外部"的教材。 sqlx::query! 是 sqlx-macros 里约 100 行宏入口加上 sqlx-macros-core 里 3500+ 行实现,整个流程涉及:连接数据库、发 Describe 消息、解析 ParameterDescription 和 RowDescription、把 PostgreSQL 的 OID 映射到 Rust 类型、按 CARGO_MANIFEST_DIR 的路径规则决定是用离线 JSON 还是连在线数据库。读通之后,你会建立起"过程宏如何与真实世界打交道"的直觉。
这本书是一份 trait 家族设计的教材。 Database trait 通过泛型关联类型(GAT)定义了 11 个关联类型,把 Row<'r>、ValueRef<'r>、Arguments<'q> 这些带生命周期的子类型绑在同一个 trait 下(见 sqlx-core/src/database.rs:72)。Executor trait 通过 default method 把 fetch / fetch_all / fetch_one 都建立在一个 fetch_many 抽象上(见 sqlx-core/src/executor.rs:33)。Encode / Decode / Type 三件套共同构成双向类型映射,每一面都要实现、每一面都有生命周期。这些 trait 的分裂和组合是 Rust 类型系统用于"数据形态各异的驱动"的经典案例。
读者画像
- Rust 后端开发者:天天写
sqlx::query!,想理解派生宏属性到底做了什么、为什么&mut Transaction不再自动是Executor、PoolTimedOut错误怎么诊断。 - 数据库驱动作者:准备给 TiDB / ClickHouse / DuckDB 写 sqlx 驱动,想从
sqlx-core的 trait 家族理解最小实现集合。 - 性能工程师:在生产遇到连接池排队、PREPARE 缓存失效、慢查询诊断,需要从 sqlx 源码找根因。
- Rust 元编程爱好者:对"过程宏怎么连数据库"感兴趣——
sqlx::query!是经典案例。 - 《Tokio》《Axum》读者:想把异步后端栈的最后一环(持久层)源码心智补齐。
前置知识
必须掌握:
- trait 与泛型:
impl Trait for Type、关联类型、where 子句、生命周期约束。sqlx 的核心抽象全部建立在 trait 家族上。 - 泛型关联类型(GAT):
type Row<'r>这样的写法。Databasetrait 重度依赖 GAT。 Future与Pin:知道async fn返回值是一个状态机。sqlx 的Executor::fetch返回BoxStream,Pool::acquire返回impl Future。async/await语法:会写async fn,理解.await在运行时的含义。- Tokio 基础:知道
Semaphore、RwLock、tokio::spawn。第 13-14 章 Pool 会直接讨论这些原语。
最好掌握:
- SQL 基础:
SELECT/INSERT/UPDATE、事务、索引。 - PostgreSQL 或 MySQL 的线路协议(wire protocol)概念:知道有
Parse/Bind/Execute这样的消息。第 16-17 章会详细拆。 - 过程宏基础:知道
proc_macro::TokenStream、syn、quote。第 11 章query!宏会用到。
完全不需要:
- 能手写过程宏。第 11 章会讲
sqlx-macros的实现,但你不需要自己写过 proc macro。 - Diesel 使用经验。本书不会以 Diesel 为基准做使用对比,只在设计决策点(比如"为什么不做 DSL")偶尔对照。
- 数据库内核经验。sqlx 是客户端,存储引擎与查询优化是服务端的事。
本书的阅读路径
本书共 23 章(含前言),按六条主线展开。章节之间的依赖关系如下:
两条典型的阅读路径:
顺序阅读(推荐):从前言到第 22 章一路读下来。第 3 章 Database trait 和第 4 章 Executor trait 是整本书的枢纽——如果没读它们就跳到 Pool 或驱动,会产生"为什么这里要这么写"的疑惑。
实战路径:第 1 → 3 → 4 → 9 → 11 → 13 → 14 → 15 → 22。这条路径先建立核心心智模型(数据库抽象 → 执行器 → 查询 API → query! 宏 → 连接池 → 事务),然后直接跳到生产实战。中间跳过的章节可以按需回看。
驱动作者路径:第 1 → 3 → 5 → 6 → 7 → 8 → 12 → 16(以 Postgres 为模板)。读完这条路径,你就拥有了给任意数据库写 sqlx 驱动所需的全部抽象理解。
每一章的结构保持一致:
- 一个真实场景:从一行业务代码或一个编译错误切入。
- 问题背景:为什么这个问题不是显然的。
- 候选方案对比:其他库(Diesel、tokio-postgres、deadpool)的做法、每种的 trade-off。
- sqlx 的选择:直接对着源码读决定。
- 工程落地:怎么把这一章的理解用到你的日常工作里。
关于工具
本书在引用源码时会频繁用到以下命令:
bash
# 获取与本书一致的源码
git clone https://github.com/launchbadge/sqlx.git
cd sqlx && git checkout v0.8.6
# 展开宏(query! 展开后的 Future 类型才看得清)
cargo install cargo-expand
cargo expand --bin my_app
# ripgrep 用来快速定位符号
cargo install ripgrep
rg "impl Executor" sqlx-core/src/
# cargo sqlx CLI(准备 offline 缓存、迁移管理)
cargo install sqlx-cli --version 0.8.6sqlx 源码里大量使用 crate::declare_driver_with_optional_migrate! 之类的宏来为一个数据库 crate 一次性声明所有 Database 关联类型,还有 sqlx-macros-core/src/query/input.rs 里的自定义解析器来消化 query!("SELECT ...", $1) 这种语法。cargo expand 是读懂这些代码的必备工具——第 11 章 query! 宏会直接对着展开后的代码讲解。
关于版本
本书所引用的源码,全部锁定于 2026 年 4 月 24 日的版本快照(sqlx 0.8.6 / sqlx-core 0.8.6 / sqlx-macros 0.8.6 / sqlx-postgres 0.8.6 / sqlx-mysql 0.8.6 / sqlx-sqlite 0.8.6,tag v0.8.6、commit bab1b02)。sqlx 是仍在积极演进的项目——2025 年下半年发布了 0.9.0-alpha.1,未来的正式版可能会在 Executor 和 Pool 的 API 上带来较大变化。但本书强调的是设计决策和工程思路,这些在未来的版本里大概率依然成立。
好了,序言结束。翻到第 1 章——为什么需要 SQLx——我们开始这场读码之旅。