Rust in Everything
基于 sqlx 0.9.0

sqlx 源码解析

全卷只对着一个版本写:

crate版本tagcommit日期
sqlx(及 sqlx-core、sqlx-postgres、sqlx-macros、sqlx-macros-core、sqlx-cli)0.9.0v0.9.075bc0482026-05-21(CHANGELOG 记的发布日期是 2026-05-06)

0.9.0 发布时仓库从 launchbadge/sqlx 转到了 transact-rs/sqlx,本卷的源码链接都指向后者的 tag。工作区里的 crate 同版本发布、互相用 =0.9.0 锁住;各 crate 管什么、谁依赖谁,见源码地图。sqlx 支持 Postgres、MySQL / MariaDB 和 SQLite,本卷只深入 Postgres 驱动 sqlx-postgres,另外两个只在对比时提到。

sqlx 的 README 第一句是「SQLx is an async, pure Rust SQL crate featuring compile-time checked queries without a DSL」(原文在 pure Rust 后面有一个脚注:SQLite 驱动调的是 C 库,是例外)。它不提供 ORM,也不提供拼查询的 DSL:查询就是一段 SQL 字符串,query! 宏在编译时连上开发数据库,让数据库自己检查这段 SQL,拿回参数和结果列的类型,生成对应的 Rust 代码。运行时那一侧,Postgres 的线协议用纯 Rust 实现,每条查询默认变成预备语句并按连接缓存,连接池也在库里。本站不直接调 sqlx,而是经 sea-orm 1.1.20 用 sqlx 0.8.6:sea-orm 生成的每一条 SQL,最后都走这一卷讲的那条路径。

全卷沿三条线展开:

  • 查询主线(一次 fetch_one):query_as("... where id = $1").bind(id).fetch_one(&pool)。bind 把值编码进 PgArguments;&Pool 借一条连接,交给 PgConnection::run;语句缓存没命中就发 Parse + Describe + Sync 拿回参数和列的类型,然后 Bind + Execute + Close + Sync 一次发出;每条 DataRow 包成 PgRow,FromRow 和 Decode 把它变成 Rust 值。
  • 连接池主线(一次 acquire 和归还):在 acquire_timeout 内拿一个信号量许可,弹一条空闲连接并 ping,或者新建一条;PoolConnection 被 drop 时 spawn 一个任务,再 ping 一次、把连接放回空闲队列、释放许可;后台任务按 idle_timeout 和 max_lifetime 回收连接。
  • 编译期检查主线(一次 query!):过程宏读 DATABASE_URL,编译时连库(没有就读 .sqlx 里 cargo sqlx prepare 生成的 JSON),对 SQL 做 describe,用 pg_attribute 和 EXPLAIN 推断每列可不可能是 NULL,生成参数类型检查和一个匿名的 Record 结构体。

三条线每一步对应的源码和页面,见怎么读这一卷。

flowchart TB
  intro[00 导读] --> overview[01 全景]
  overview --> core[02 核心抽象]
  core --> pg["03 Postgres 驱动"]
  core --> pool[04 连接池]
  pg --> tx["05 事务与迁移"]
  pool --> tx
  pg --> macros[06 编译期宏]
  tx --> design[07 设计取舍]
  macros --> design
  design --> pit[08 常见坑]
  pit --> site[09 在本站中]
  site --> hands[10 动手]

实验环境

关于行为的说法(发了哪几条协议消息、语句缓存命中没有、池子里有几条连接、报什么错、rustc 说了什么)都配一个小程序和它的真实输出,平台是 M2 Max、macOS、rustc 1.99.0、Postgres 16.14(本机容器)。看协议消息用一个自己写的 tokio TCP 代理,把客户端和服务端之间的每条消息按类型打出来,代码在一次查询的全过程。sqlx 跑在 tokio 上,讲到 socket、超时、spawn、信号量时链到 tokio 卷,Json<T> 和 .sqlx 文件链到 serde 卷。

各部分

  1. 00 导读:锁定的版本和仓库的搬迁,三条主线每一步落在哪个函数、阅读路线、怎么看语句日志和协议消息;工作区各 crate 的分工、feature 体系、rt/ 怎么在运行时选 tokio。
  2. 01 全景:sqlx 和 tokio-postgres、diesel、sea-orm 在设计上的分界;用抓包把一次 query_as + bind + fetch_one 完整走一遍,第二次执行不再发 Parse,只有 &str 和 raw_sql 走简单查询。
  3. 02 核心抽象:本卷重点。Database 与关联类型,Executor 与 Acquire,Query、QueryAs 与 0.9 的 SqlSafeStr,Type / Encode / Decode,Row 与 FromRow,错误。
  4. 03 Postgres 驱动:建立连接与 SCRAM 认证,协议消息的编解码,扩展查询协议与语句缓存,Postgres 的类型,describe,LISTEN 与 COPY。
  5. 04 连接池:PoolOptions 的各个参数和默认值,acquire 的循环,归还与后台维护。
  6. 05 事务与迁移:Transaction 与保存点、Drop 时排队的回滚;migrate! 与 Migrator。
  7. 06 编译期宏:query! 的流水线,生成的代码,离线模式与 sqlx.toml,#[sqlx::test]。
  8. 07 设计取舍:把各章的取舍汇总到一起,每条链回有证据的页面。
  9. 08 常见坑:用报错原文做标题,每个都先跑出最小复现。
  10. 09 在本站中:本站经 sea-orm 1.1.20 怎么用 sqlx 0.8.6,池参数和 sqlx 默认值的对比;0.8.6 到 0.9.0 的破坏性改动是否会影响本站。
  11. 10 动手:不依赖 sqlx,从零写一个迷你 Postgres 客户端:启动与认证、简单查询、带一个参数的扩展查询、一个用信号量的最小连接池。

适合谁读

  • 用 sqlx(或者经 sea-orm 间接用它)写过代码,想知道 fetch_one(&pool) 背后发生了什么、池为什么会超时、query! 为什么要连库的读者:从导读开始,按阅读路线里「只想会用」那条读 01、02 的 Executor 和 Query、04、07。
  • 想读懂一个数据库驱动怎么实现的读者:按顺序读。02 是全卷的重点,三条主线都从那里的 trait 出发;03 往下一直走到协议的字节。
  • 需要的前置知识是 async/await 和 Postgres 的基本概念(预备语句、事务、类型 OID);协议本身在 03 里从头讲。

💬 关联讨论

发起讨论
加载中...