SQLx 源码深度解析

前言

作者 杨艺韬 · 2,753 字

前言

五行代码与一座冰山

你写了一个 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 类型的字段加自定义 EncodeDecode,发现派生宏的属性和 sqlx::Type 的 trait 约束缠绕在一起;你想让 CI 在没有 DATABASE_URL 的情况下也能编译,发现 .sqlx/ 目录里那一堆 JSON 是 cargo sqlx prepare 生成的离线缓存;你想搞清楚 Pool::acquire().await? 为什么在高并发下偶尔要花上几百毫秒(而你明明设了 max_connections = 50),翻到 sqlx-core/src/pool/inner.rs 发现空闲连接被 ArrayQueueAsyncSemaphore 联合守卫,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::fetchExecutor::execute 的一次调用。PoolPoolConnection 的转换不是"拿一个连接对象"——它是 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 源码深度解析》,或者至少熟悉 FutureWakertokio::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 ExecutorPool 的 fetch 实现、一起 git log 那个把 &mut TransactionExecutor 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 消息、解析 ParameterDescriptionRowDescription、把 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 类型系统用于"数据形态各异的驱动"的经典案例。

读者画像

前置知识

必须掌握:

最好掌握:

完全不需要:

本书的阅读路径

本书共 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 源码里大量使用 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,未来的正式版可能会在 ExecutorPool 的 API 上带来较大变化。但本书强调的是设计决策工程思路,这些在未来的版本里大概率依然成立。

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