SQLx 源码深度解析

前言

作者 杨艺韬 · 3,173 字 · 发布于 · 更新于

五行代码与一座冰山

你写了一个 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(20)),翻到 sqlx-core/src/pool/inner.rs 发现空闲连接被 ArrayQueue 和 AsyncSemaphore 联合守卫,fair 模式(默认开启)下等待者按先来后到排队,后来的获取者不能插到前面;你想在一个 Transaction 的作用域快结束时根据业务结果决定 commit 还是 rollback,发现 Transaction<'c, DB> 的 Drop 实现只会发起 rollback(Postgres / MySQL 驱动只是把 ROLLBACK 写进缓冲,等这条连接下一次被使用时才真正发出),永远不会替你 commit——必须显式调用 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 或 SQLite、让驱动 describe 这条语句(在 Postgres 上就是协议里的 Describe 消息)、拿到列类型、然后把这些信息注入编译器的类型推导链。这条从"SQL 字符串字面量"到"带类型的 Future<Output = Result<User, Error>>"的路径,设计细节值得每一个想做过程宏的 Rust 作者仔细读一遍。

这个专栏不是什么

这个专栏不是 sqlx 使用手册。 sqlx::query! 怎么写、PgPoolOptions 有哪些方法、FromRow 的 #[sqlx(rename = "...")] 怎么用——这些官方文档和 examples 已经覆盖得很好。本专栏的每一章都在回答"为什么这样设计"和"底层怎么实现",而不是"怎么用"。

这个专栏不是 Tokio / async 入门。 本专栏假设你已经读过《Tokio 源码深度解析》,或者至少熟悉 Future、Waker、tokio::spawn 这些概念。第 13-14 章 Pool 会直接解释 sqlx-core/src/sync.rs 那层 AsyncSemaphore 包装——启用 runtime-tokio 时它内部就是 tokio::sync::Semaphore(源码里一句 debug_assert!(fair, "Tokio only has fair permits")),async-std 下才换成 futures-intrusive 的实现——不会从零讲 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.6 到 0.7 破坏性最大的变更之一)。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 类型、按 SQLX_OFFLINE / DATABASE_URL(也会读 CARGO_MANIFEST_DIR 下的 .env)决定是连在线数据库还是读 .sqlx/ 里的离线 JSON。读通之后,你会建立起"过程宏如何与真实世界打交道"的直觉。

这个专栏是一份 trait 家族设计的教材。 Database trait 定义了 11 个关联类型,其中 4 个是泛型关联类型(GAT),把 ValueRef<'r>、Arguments<'q>、Statement<'q> 这些带生命周期的子类型绑在同一个 trait 下(见 sqlx-core/src/database.rs:72)。Executor trait(sqlx-core/src/executor.rs:33)的读/执行方法只留两个必须实现的原语——fetch_many(同文件 :88)和 fetch_optional(:132),另有 prepare_with / describe 两个无默认实现的 meta 方法;execute / execute_many / fetch / fetch_all 靠 default method 归约到前者,fetch_one(:115)归约到后者。Encode / Decode / Type 三件套共同构成双向类型映射,写入和读取两面各带自己的生命周期(Encode<'q, DB>、Decode<'r, DB>)。这些 trait 的分裂和组合是 Rust 类型系统用于"数据形态各异的驱动"的经典案例。

读者画像

前置知识

必须掌握:

最好掌握:

完全不需要:

本专栏的阅读路径

本专栏共 23 章(含前言),按六条主线展开。章节之间的依赖关系如下:

graph TD
    P[00 前言]
    C1[01 为什么需要 SQLx]
    subgraph 核心抽象
      C2[02 Workspace]
      C3[03 Database trait]
      C4[04 Executor trait]
    end
    subgraph 类型映射
      C5[05 Encode/Decode/Type]
      C6[06 Arguments]
      C7[07 Row/Column]
      C8[08 FromRow]
    end
    subgraph 查询API
      C9[09 Query/QueryAs]
      C10[10 QueryBuilder]
      C11[11 query! 宏]
    end
    subgraph 连接与事务
      C12[12 Connection]
      C13[13 Pool API]
      C14[14 Pool 内部]
      C15[15 Transaction]
    end
    subgraph 驱动实现
      C16[16 Postgres]
      C17[17 MySQL]
      C18[18 SQLite]
      C19[19 Any]
    end
    subgraph 工具与工程
      C20[20 迁移]
      C21[21 日志追踪]
      C22[22 生产实战]
    end
    P --> C1
    C1 --> C2 --> C3 --> C4
    C4 --> C5 --> C6 --> C7 --> C8
    C8 --> C9 --> C10 --> C11
    C4 --> C12 --> C13 --> C14 --> C15
    C15 --> C16 --> C17 --> C18 --> C19
    C11 --> C20 --> C21 --> C22

三条典型的阅读路径:

  1. 顺序阅读(推荐):从前言到第 22 章一路读下来。第 3 章 Database trait 和第 4 章 Executor trait 是整个专栏的枢纽——如果没读它们就跳到 Pool 或驱动,会产生"为什么这里要这么写"的疑惑。

  2. 实战路径:第 1 → 3 → 4 → 9 → 11 → 13 → 14 → 15 → 22。这条路径先建立核心心智模型(数据库抽象 → 执行器 → 查询 API → query! 宏 → 连接池 → 事务),然后直接跳到生产实战。中间跳过的章节可以按需回看。

  3. 驱动作者路径:第 1 → 3 → 5 → 6 → 7 → 8 → 12 → 16(以 Postgres 为模板)。读完这条路径,你就拥有了给任意数据库写 sqlx 驱动所需的全部抽象理解。

每一章的结构保持一致:

关于工具

本专栏在引用源码时会频繁用到以下命令:

# 获取与本专栏一致的源码
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.6

sqlx 源码里大量使用声明宏来批量生成样板代码——比如 sqlx_core::declare_driver_with_optional_migrate!(DRIVER = Postgres) 为每个驱动 crate 声明 Any 层的 AnyDriver 常量、impl_type_checking! 为一个驱动一次性登记全部 SQL 类型到 Rust 类型的映射表;还有 sqlx-macros-core/src/query/input.rs 里的 impl Parse for QueryMacroInput 来消化 query!("SELECT ... WHERE id = $1", user_id) 这种语法。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 是仍在积极演进的项目——上游已经打出了 v0.9.0 标签(2026-05-21),带来了新的破坏性变更(例如 Arguments trait 去掉了生命周期参数、query*() 系列函数改为接收 impl SqlSafeStr)。但本专栏强调的是设计决策和工程思路,这些在未来的版本里大概率依然成立。

除 sqlx 自身以外,本专栏还会引用几棵对照树——凡是路径里带版本号的引用都属于这一类,行号以下表为准:

对照仓库 版本 出现在 为什么要它
Diesel 2.3.9(tag v2.3.9) 第 1、3、7、8 章 编译期 DSL 路线的代表,和 sqlx 的"不包装 SQL"正面对照
tokio-postgres 0.7.9(tag tokio-postgres-v0.7.9) 第 1、7、15、16 章 裸协议异步路线的代表,错误分类法尤其值得对照
tokio 1.40.0 第 1、18 章 第 1 章全链路里的 TCP accept / write;第 18 章对照 spawn_blocking 线程池的默认值(SQLite 驱动恰恰没用它,而是每条连接一个专用 worker 线程)
hyper 1.9.0 第 1 章 画"一次 HTTP 请求从 accept 到写回"的全链路时,数据库只是其中一段

好了,序言结束。翻到第 1 章——为什么需要 SQLx——我们开始这场读码之旅。