Rust in Everything
基于 sqlx 0.9.0

源码地图

主线位置:不在主线上。本页说明三条主线经过的代码分在哪几个 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_drivers1 388
sqlx-core与数据库无关的部分:Database、Executor、Encode / Decode 等 trait,query*() 构造函数,连接池,事务,迁移,语句缓存,rt/ 运行时抽象,net/ socket 与 TLS,any/17 081
sqlx-postgresPostgres 驱动:协议消息的编解码、连接与认证、扩展查询、类型映射、LISTEN / COPY20 375
sqlx-mysqlMySQL / MariaDB 驱动(本卷不讲)8 470
sqlx-sqliteSQLite 驱动,调 libsqlite3(本卷只在对比时提)10 642
sqlx-macros过程宏 crate(proc-macro = true),7 个入口,每个都只转调 sqlx-macros-core101
sqlx-macros-core宏的真正实现:query! 的流水线、#[derive(FromRow)] 等派生、#[sqlx::test]、migrate!3 990
sqlx-cli命令行工具 sqlx / cargo sqlx:建库、迁移、prepare2 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(一组关联类型)、ConnectionDatabase 与关联类型
executor.rs、acquire.rs—Executor、Execute、AcquireExecutor 与 Acquire
query*.rs、raw_sql.rs、sql_str.rs、query_builder.rs—query() 一族、SqlSafeStr、QueryBuilderQuery、QueryAs 与 SqlSafeStr
pool/2 498Pool、PoolInner、PoolConnection、PoolOptions04 连接池
any/1 840Any 驱动Database 与关联类型
net/1 243TCP / Unix socket、TLS(rustls 或 native-tls)建立连接
config/1 176sqlx.toml 的结构离线模式与 sqlx.toml
migrate/1 043Migrator、Migrate trait迁移
rt/353spawn、timeout、sleep、yield_now 在各运行时上的实现本页下文
common/statement_cache.rs—语句缓存(LRU)一次查询的全过程

sqlx-postgres:Postgres 驱动

路径行数内容讲它的页
types/8 826各种 Rust 类型和 Postgres 类型的 Encode / DecodePostgres 的类型
connection/2 483建立连接与认证(establish.rs、sasl.rs)、executor.rs(run、prepare)、describe.rs建立连接、扩展查询协议、describe
message/2 314每种前端 / 后端消息一个文件协议消息
options/1 511PgConnectOptions、URL 与环境变量解析建立连接
listener.rs、copy.rs—LISTEN / NOTIFY、COPYLISTEN 与 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、jsondefault = ["any", "macros", "migrate", "json"]
运行时runtime-tokio、runtime-async-std、runtime-smol、runtime-async-global-executor至少开一个,否则用到运行时的地方会 panic
TLStls-native-tls、tls-rustls-ring-webpki、tls-rustls-ring-native-roots、tls-rustls-aws-lc-rstls-rustls 是 tls-rustls-ring 的别名,后者又是 tls-rustls-ring-webpki 的别名,都为兼容旧名字保留;不开就没有 TLS
数据库postgres、mysql、mysql-rsa、sqlite、sqlite-unbundledsqlite 等于 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-tomlmacros 隐含 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 读的是线程本地的运行时上下文,见线程本地上下文。

📝NOTE
冷知识:0.9 多了两个运行时,是为了替换已弃用的 async-std

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。

❗IMPORTANT
动脑筋:开了 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。

💬 关联讨论

发起讨论
加载中...