源码地图
主线位置:不在主线上。本页说明三条主线经过的代码分在哪几个 crate、哪些目录,crate 之间怎么依赖,feature 怎么把它们拼起来,以及
rt/怎么决定用哪个异步运行时。
sqlx 仓库在 v0.9.0 是一个 Cargo 工作区,members 里有 9 个 crate 和 15 个示例项目。和本卷有关的 crate 之间是这样依赖的:
flowchart TB user["你的 crate"] --> facade["sqlx 门面"] facade --> core["sqlx-core"] facade -->|"feature postgres"| pg["sqlx-postgres"] facade -->|"feature mysql"| my["sqlx-mysql"] facade -->|"feature sqlite"| lite["sqlx-sqlite"] facade -->|"feature macros"| mac["sqlx-macros"] mac --> mcore["sqlx-macros-core"] mcore --> core mcore --> pg pg --> core my --> core lite --> core cli["sqlx-cli"] --> facade
各个 crate
| crate | 管什么 | src/ 行数 |
|---|---|---|
sqlx | 门面:按 feature 重新导出各 crate 的类型;query! 系列 macro_rules!;Any 驱动的 install_default_drivers | 1 388 |
sqlx-core | 与数据库无关的部分:Database、Executor、Encode / Decode 等 trait,query*() 构造函数,连接池,事务,迁移,语句缓存,rt/ 运行时抽象,net/ socket 与 TLS,any/ | 17 081 |
sqlx-postgres | Postgres 驱动:协议消息的编解码、连接与认证、扩展查询、类型映射、LISTEN / COPY | 20 375 |
sqlx-mysql | MySQL / MariaDB 驱动(本卷不讲) | 8 470 |
sqlx-sqlite | SQLite 驱动,调 libsqlite3(本卷只在对比时提) | 10 642 |
sqlx-macros | 过程宏 crate(proc-macro = true),7 个入口,每个都只转调 sqlx-macros-core | 101 |
sqlx-macros-core | 宏的真正实现:query! 的流水线、#[derive(FromRow)] 等派生、#[sqlx::test]、migrate! | 3 990 |
sqlx-cli | 命令行工具 sqlx / cargo sqlx:建库、迁移、prepare | 2 122 |
sqlx-test | 仓库自己测试用的辅助 crate,publish = false | — |
行数是 find <crate>/src -name '*.rs' | xargs cat | wc -l 的结果。各子 crate 的 Cargo.toml 描述都写着「Not intended to be used directly」(sqlx-postgres 写的是「Not for direct use; see the sqlx crate for details.」),使用者只依赖 sqlx。
sqlx:门面
src/lib.rs 前半段全是 pub use sqlx_core::...。驱动按 feature 拼进来,Postgres 这一块是:
#[cfg(feature = "postgres")]
#[cfg_attr(docsrs, doc(cfg(feature = "postgres")))]
#[doc(inline)]
pub use sqlx_postgres::{
self as postgres, PgConnection, PgExecutor, PgPool, PgTransaction, Postgres,
};
(src/lib.rs 第 54 行)。所以 sqlx::PgPool、sqlx::postgres::PgPoolOptions 其实是 sqlx_postgres 里的类型;sqlx::query、sqlx::Pool 来自 sqlx_core。不开 postgres feature,这些名字在 sqlx 里就不存在。
query!、query_as! 这些宏定义在门面的 src/macros/mod.rs,是 macro_rules!,展开成 $crate::sqlx_macros::expand_query!(...)。门面里 pub extern crate sqlx_macros;,用户自己的 crate 就不需要直接依赖 sqlx-macros。
sqlx-core:与数据库无关的部分
lib.rs 开头是 #![forbid(unsafe_code)]。主要目录和文件:
| 路径 | 行数 | 内容 | 讲它的页 |
|---|---|---|---|
database.rs、connection.rs | — | Database(一组关联类型)、Connection | Database 与关联类型 |
executor.rs、acquire.rs | — | Executor、Execute、Acquire | Executor 与 Acquire |
query*.rs、raw_sql.rs、sql_str.rs、query_builder.rs | — | query() 一族、SqlSafeStr、QueryBuilder | Query、QueryAs 与 SqlSafeStr |
pool/ | 2 498 | Pool、PoolInner、PoolConnection、PoolOptions | 04 连接池 |
any/ | 1 840 | Any 驱动 | Database 与关联类型 |
net/ | 1 243 | TCP / Unix socket、TLS(rustls 或 native-tls) | 建立连接 |
config/ | 1 176 | sqlx.toml 的结构 | 离线模式与 sqlx.toml |
migrate/ | 1 043 | Migrator、Migrate trait | 迁移 |
rt/ | 353 | spawn、timeout、sleep、yield_now 在各运行时上的实现 | 本页下文 |
common/statement_cache.rs | — | 语句缓存(LRU) | 一次查询的全过程 |
sqlx-postgres:Postgres 驱动
| 路径 | 行数 | 内容 | 讲它的页 |
|---|---|---|---|
types/ | 8 826 | 各种 Rust 类型和 Postgres 类型的 Encode / Decode | Postgres 的类型 |
connection/ | 2 483 | 建立连接与认证(establish.rs、sasl.rs)、executor.rs(run、prepare)、describe.rs | 建立连接、扩展查询协议、describe |
message/ | 2 314 | 每种前端 / 后端消息一个文件 | 协议消息 |
options/ | 1 511 | PgConnectOptions、URL 与环境变量解析 | 建立连接 |
listener.rs、copy.rs | — | LISTEN / NOTIFY、COPY | LISTEN 与 COPY |
驱动的 [dependencies] 里没有 libpq,也没有任何 -sys crate;和协议有关的依赖是 hmac、sha2、hkdf、md-5、rand、stringprep、base64,用来做 SCRAM-SHA-256 和 MD5 认证。sqlx-postgres/src/ 里一个 unsafe 块也没有(grep -rn unsafe 只命中一行文档注释)。
sqlx-macros 和 sqlx-macros-core
sqlx-macros/src/lib.rs 一共 101 行,7 个过程宏入口:函数式的 expand_query、migrate,派生的 Encode、Decode、Type、FromRow,属性宏 test。每个都是「解析输入 → 调 sqlx_macros_core 里对应的函数 → 把错误变成 compile_error!」。过程宏 crate 只能导出过程宏,不能导出普通函数和类型,把实现放进一个普通库 crate,才能被别的代码复用和单独测试;sqlx-macros-core 的文档也说明它面向「building a custom SQLx driver」的人,API 不遵守语义化版本。
sqlx-macros-core 里的驱动表是写死的 FOSS_DRIVERS:按 feature 放进 MySql、Postgres、Sqlite 三个 QueryDriver。它直接依赖三个驱动 crate(开 offline、migrate),因为编译时要真的连库 describe;连库用的运行时由 block_on 提供,tokio 下是一个进程内共享的 current-thread 运行时,注释说「We need a single, persistent Tokio runtime since we're caching connections」。
sqlx-cli
sqlx-cli 只依赖门面 sqlx(runtime-tokio、migrate、any,再按自己的 feature 打开数据库和 TLS),并不依赖 sqlx-macros-core。cargo sqlx prepare 生成 .sqlx/query-*.json 的办法是让编译器跑一遍宏:run_prepare_step 删掉旧的 JSON,然后执行 cargo check,带上环境变量 SQLX_OFFLINE=false、SQLX_OFFLINE_DIR=<缓存目录>、SQLX_TMP=<target>/sqlx-tmp,以及 DATABASE_URL。写文件的是你的 crate 编译时被调起的 query!。细节见离线模式与 sqlx.toml。
feature 体系
门面 Cargo.toml 的 features 表是几组正交的开关,每个 feature 再用 crate?/feature 的写法转发给需要它的子 crate:
| 组 | feature | 说明 |
|---|---|---|
| 默认 | any、macros、migrate、json | default = ["any", "macros", "migrate", "json"] |
| 运行时 | runtime-tokio、runtime-async-std、runtime-smol、runtime-async-global-executor | 至少开一个,否则用到运行时的地方会 panic |
| TLS | tls-native-tls、tls-rustls-ring-webpki、tls-rustls-ring-native-roots、tls-rustls-aws-lc-rs | tls-rustls 是 tls-rustls-ring 的别名,后者又是 tls-rustls-ring-webpki 的别名,都为兼容旧名字保留;不开就没有 TLS |
| 数据库 | postgres、mysql、mysql-rsa、sqlite、sqlite-unbundled | sqlite 等于 sqlite-bundled 加 sqlite-deserialize、sqlite-load-extension、sqlite-unlock-notify |
| 类型 | json、chrono、time、uuid、bigdecimal、rust_decimal、ipnet、ipnetwork、mac_address、bit-vec、bstr | 打开对应第三方类型的 Encode / Decode |
| 功能 | macros、derive、migrate、sqlx-toml | macros 隐含 derive,还打开 sqlx-core 和各驱动的 offline(让 Describe 能序列化成 .sqlx 里的 JSON);sqlx-toml 不在默认集里 |
运行时和 TLS 曾经合在一个组合 feature 里(runtime-tokio-rustls 这种),0.8.x 里这些名字还作为旧写法保留;0.9.0 的 #3821 把它们删掉了(CHANGELOG:「Deleted deprecated combination runtime+TLS features (e.g. runtime-tokio-native-tls)」),现在必须分开写。本卷的实验 crate 用的是 runtime-tokio + tls-rustls-ring-webpki + postgres。
sqlx-toml 为什么不默认打开,CHANGELOG 在 #3383 下面解释了:「Default features of library crates can be hard to completely turn off because of feature unification, so it's better to keep the default feature set as limited as possible.」
rt/:运行时在运行时选
sqlx 需要运行时做四件事:spawn(连接归还、池的维护任务)、timeout(acquire_timeout)、sleep、yield_now,以及打开 socket。rt/mod.rs 里每个函数都是同一个形状,以 spawn 为例:
pub fn spawn<F>(fut: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
#[cfg(feature = "_rt-tokio")]
if let Ok(handle) = tokio::runtime::Handle::try_current() {
return JoinHandle::Tokio(handle.spawn(fut));
}
cfg_if! {
if #[cfg(feature = "_rt-async-global-executor")] {
JoinHandle::AsyncTask(Some(async_global_executor::spawn(fut)))
} else if #[cfg(feature = "_rt-smol")] {
JoinHandle::AsyncTask(Some(smol::spawn(fut)))
} else if #[cfg(feature = "_rt-async-std")] {
JoinHandle::AsyncStd(async_std::task::spawn(fut))
} else {
missing_rt(fut)
}
}
}
选择分两层。编译期,feature 决定哪些分支存在;运行期,只要编进了 tokio、并且当前线程处在一个 tokio 运行时里(rt_tokio::available 就是 Handle::try_current().is_ok()),就用 tokio;否则落到 async-io 一系(async-global-executor、smol、async-std 按这个顺序取第一个编进来的)。几个运行时同时开也能编译,用哪个取决于调用时所在的上下文。打开 TCP 连接的 connect_tcp 是同样的结构:tokio 可用就 tokio::net::TcpStream::connect,否则走 async-io。两条路都走不通时调 missing_rt,它是一个 const fn,返回 !:编进了 tokio 时 panic「this functionality requires a Tokio context」,一个运行时都没开时 panic「one of the runtime features of SQLx must be enabled」。
tokio 下 spawn 和 timeout 的内部见 tokio 卷的 spawn 全路径和 Sleep 的一生;Handle::try_current 读的是线程本地的运行时上下文,见线程本地上下文。
0.8.6 的 rt/mod.rs 里只有两条路:tokio,或者 async-std,spawn 的后半段是 #[cfg(feature = "_rt-async-std")] 和 #[cfg(not(feature = "_rt-async-std"))] missing_rt(fut)。0.9.0 合并了 PR #3791(「Smol+async global executor 1.80 dev」),加上 runtime-smol 和 runtime-async-global-executor,CHANGELOG 写明用途:「Adds runtime-smol and runtime-async-global-executor features to replace usages of the deprecated async-std crate.」PR 说明里还写了它的代价:「This is a breaking change -- async-global-executor has MSRV 1.80」。于是有了上面那段 cfg_if! 链和 JoinHandle::AsyncTask(smol 和 async-global-executor 共用 async-task 的任务句柄)。runtime-async-std 仍然保留,排在链的最后。
Any 驱动
any 在默认 feature 里。AnyPool / AnyConnection 按 URL 的 scheme 在运行时选驱动,代价是所有类型都要经过一层转换,能用的类型也少。驱动表不是编译时写死的:程序要先调门面的 install_default_drivers,它用一个 Once 把按 feature 编进来的几个驱动的 any::DRIVER 交给 sqlx_core::any::install_drivers,存进一个只能设置一次的全局变量。Any 和 Database 关联类型的关系见 Database 与关联类型。本卷其余部分只讲 Postgres 驱动本身,不经 Any。
runtime-tokio,却不在 tokio 运行时里调 sqlx,会发生什么?平台 M2 Max、macOS、rustc 1.99.0,实验 crate 的 sqlx feature 同上(只有 runtime-tokio 一个运行时)。不用 #[tokio::main],改用 futures crate 的单线程执行器驱动连接:
use sqlx::Connection;
// No #[tokio::main]: drive sqlx's future with the futures crate's executor.
fn main() {
let url = std::env::var("DATABASE_URL").unwrap();
let r = futures::executor::block_on(sqlx::PgConnection::connect(&url));
println!("{:?}", r.map(|_| "connected"));
}
会打印 Ok("connected")、打印一个 Err(...),还是 panic?如果 panic,panic 在 sqlx 的哪个文件里?
动脑筋答案
thread 'main' (3060768) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/net/socket/mod.rs:200:13:
this functionality requires a Tokio context
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic,不是 Err。PgConnection::connect 第一件事是打开 TCP 连接,走到 connect_tcp:rt_tokio::available() 为假(futures::executor::block_on 不是 tokio 运行时),又没有编进 async-io 一系的运行时,于是 cfg_if! 落到 missing_rt,panic 位置 net/socket/mod.rs:200 就是 connect_tcp 里那一行 crate::rt::missing_rt((host, port, with_socket))。因为编进了 tokio,消息是「requires a Tokio context」而不是「one of the runtime features」。missing_rt 的返回类型是 !,Error 枚举里也没有「没有运行时」这一项,所以这里不会变成 Err。
小结
- 使用者只依赖门面
sqlx;它按 feature 重新导出sqlx-core、各驱动和宏,sqlx::PgPool实际是sqlx_postgres里的类型。 sqlx-core(1.7 万行)放 trait、连接池、迁移、运行时抽象,forbid(unsafe_code);sqlx-postgres(2 万行,类型映射占 8.8k)纯 Rust 实现协议,不依赖 libpq。sqlx-macros是 101 行的薄壳,实现都在sqlx-macros-core,它直接依赖驱动,编译时用一个共享的 current-thread 运行时连库;sqlx-cli不用它,prepare是带着环境变量跑一遍cargo check。- feature 分运行时、TLS、数据库、类型、功能几组;0.9 起运行时和 TLS 必须分开写,
sqlx-toml不默认打开。 rt/在运行时选:编进了 tokio 且处在 tokio 上下文里就用 tokio,否则用 async-io 一系,都不行就 panic。