Appearance
第19章 Any 驱动:运行时多态 vs 编译期多态
"Generic abstraction is the sharpest tool in Rust's box— but sometimes you need to hide the tool entirely behind a handle." —— 每个从编译期泛型转向运行时多态的 Rust 工程师的领悟
本章要点
- Any 驱动(
sqlx-core/src/any/,约 1500 行)是 sqlx 里唯一的运行时多态特例——其他地方都是DB: Database编译期泛型,Any 用Box<dyn AnyConnectionBackend>做类型擦除。 AnyConnectionBackendtrait(any/connection/backend.rs:9)—— 十几个方法覆盖 Connection 的全部职责,用BoxFuture返回值让 trait object-safe。每家驱动(Postgres/MySQL/SQLite)都有对应的 AnyConnectionBackend 实现。AnyValueKind枚举(any/value.rs:11-20)—— 9 个 variant(Null/Bool/SmallInt/Integer/BigInt/Real/Double/Text/Blob)—— 三家 DB 类型系统的交集。Postgres 的数组 / JSONB / UUID 在 Any 下不可用。install_drivers+OnceCell<&'static [AnyDriver]>(any/driver.rs:125)—— 进程级单次注册;URL scheme 分发靠扫描drivers.iter().find(|d| d.url_schemes.contains(&scheme))。AnyDriver::new::<DB>()是const fn—— 让FOSS_DRIVERS之类常量能在 static context 构造 driver 列表。- Any 的代价:每次 async 调用
BoxFuture堆分配(~50-100ns)、类型信息压成 enum(Postgres 的 OID 精度丢失)、功能子集(只能用三家 DB 交集)。 - Any 的合理场景:sqlx-cli 的 migrate/prepare 命令、跨 DB 的管理工具、"测试用 SQLite、生产用 Postgres" 的应用(§18.22)。
- 业务代码几乎不该用 Any——编译期多态(具体 Pool
<Postgres>)更快、更安全、类型更精确。
19.1 问题引入:运行时选 DB 的需求
sqlx 的主路径是编译期选 DB——PgPool / MySqlPool / SqlitePool 是不同类型、用户 cargo build 时就决定用哪家。
但有些场景运行时才知道用哪家 DB:
1. sqlx-cli——sqlx migrate run --database-url $DATABASE_URL 要能处理任意 DB。不能编译三个 binary(sqlx-cli-pg / sqlx-cli-mysql / sqlx-cli-sqlite)——一个 CLI 二进制应该通吃。
2. 跨 DB 工具——SQL 分析器、schema 比较器、数据迁移工具。用户环境变量决定连哪个 DB。
3. 测试 + 生产用不同 DB——§18.22 的模式。本地 :memory: SQLite、生产 Postgres——同一份业务代码。
4. 插件型应用——用户在配置文件里选 DB、app 不重新编译就能切换。
这些场景都需要运行时多态——Rust 的trait object 是标准解法。sqlx 的 Any 驱动就是这条路径的完整实现。
但 Any 有显著代价——详见 §19.10。大多数业务代码不该用 Any——它是"特殊场景的特殊工具",不是 "默认"。理解这条定位是本章的核心。
19.2 Any 的类型家族全貌
sqlx-core/src/any/mod.rs:25-40 列出 Any 的15 个公开类型:
rust
pub use arguments::{AnyArgumentBuffer, AnyArguments};
pub use column::AnyColumn;
pub use connection::{AnyConnection, AnyConnectionBackend};
pub use database::Any;
pub use kind::AnyKind; // 已废弃
pub use options::AnyConnectOptions;
pub use query_result::AnyQueryResult;
pub use row::AnyRow;
pub use statement::AnyStatement;
pub use transaction::AnyTransactionManager;
pub use type_info::{AnyTypeInfo, AnyTypeInfoKind};
pub use value::{AnyValue, AnyValueRef, AnyValueKind};
pub type AnyPool = crate::pool::Pool<Any>;
pub type AnyPoolOptions = crate::pool::PoolOptions<Any>;Any 是个 Database trait 的完整实现——第 3 章讨论过的 11 个关联类型全部有对应的 Any 版本(AnyConnection / AnyRow / AnyValue / AnyArguments / AnyStatement / AnyTransactionManager / AnyTypeInfo / AnyColumn / AnyQueryResult / AnyValueRef / AnyArgumentBuffer)。
和 Postgres/MySQL/SQLite 不同的地方:Any 的具体类型不 直接实现协议 —— 它们委托给底层的 AnyConnectionBackend trait object、把调用转发给真正的具体驱动(Postgres/MySQL/SQLite)。Any 是一层薄代理。
19.3 AnyConnectionBackend trait:类型擦除的基础
any/connection/backend.rs:9 的核心 trait:
rust
pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static {
fn name(&self) -> &str;
fn close(self: Box<Self>) -> BoxFuture<'static, crate::Result<()>>;
fn close_hard(self: Box<Self>) -> BoxFuture<'static, crate::Result<()>>;
fn ping(&mut self) -> BoxFuture<'_, crate::Result<()>>;
fn begin(&mut self, statement: Option<Cow<'static, str>>) -> BoxFuture<'_, crate::Result<()>>;
fn commit(&mut self) -> BoxFuture<'_, crate::Result<()>>;
fn rollback(&mut self) -> BoxFuture<'_, crate::Result<()>>;
fn start_rollback(&mut self);
fn get_transaction_depth(&self) -> usize { /* 默认 panic */ }
fn is_in_transaction(&self) -> bool { self.get_transaction_depth() != 0 }
fn cached_statements_size(&self) -> usize { 0 }
fn clear_cached_statements(&mut self) -> BoxFuture<'_, crate::Result<()>>;
fn shrink_buffers(&mut self);
// 执行相关
fn fetch_many<'q>(&'q mut self, query: &'q str, persistent: bool,
arguments: Option<AnyArguments<'q>>) -> BoxStream<'q, crate::Result<Either<AnyQueryResult, AnyRow>>>;
fn fetch_optional<'q>(&'q mut self, query: &'q str, persistent: bool,
arguments: Option<AnyArguments<'q>>) -> BoxFuture<'q, crate::Result<Option<AnyRow>>>;
fn prepare_with<'q>(&'q mut self, sql: &'q str, parameters: &[AnyTypeInfo])
-> BoxFuture<'q, crate::Result<AnyStatement<'q>>>;
fn describe<'q>(&'q mut self, sql: &'q str) -> BoxFuture<'q, crate::Result<Describe<Any>>>;
}十几个方法覆盖 Connection 的全部职责——但所有异步方法都返回 BoxFuture(而不是 impl Future)。
为什么 BoxFuture? 因为 trait object(dyn AnyConnectionBackend)不能有泛型返回类型或 impl Future(编译期不确定的 type)。BoxFuture 是 Pin<Box<dyn Future + Send>>——堆分配 + 类型擦除——让 trait object 可行。
super trait std::any::Any——让 Rust 标准库的 Any trait 可用(downcast_ref::<ConcreteBackend>)——某些高级场景用得到。
#[doc(hidden)] 在一些方法上——表明 "不对用户暴露"、driver 实现方用。
19.3.1 每家驱动实现 AnyConnectionBackend
Postgres 的实现在 sqlx-postgres/src/any.rs(约 200 行):
rust
impl AnyConnectionBackend for PgConnection {
fn name(&self) -> &str { "PostgreSQL" }
fn close(self: Box<Self>) -> BoxFuture<'static, crate::Result<()>> {
Connection::close(*self) // 调具体 Postgres Connection::close
}
fn ping(&mut self) -> BoxFuture<'_, crate::Result<()>> {
Connection::ping(self)
}
fn fetch_many<'q>(&'q mut self, query: &'q str, persistent: bool,
arguments: Option<AnyArguments<'q>>) -> BoxStream<'q, crate::Result<Either<AnyQueryResult, AnyRow>>>
{
let arguments = arguments.map(convert_any_args_to_pg); // 转换类型
let stream = Executor::fetch_many(self, (query, arguments));
// 转换流里的 Either<PgQueryResult, PgRow> 为 Either<AnyQueryResult, AnyRow>
stream.map(|item| match item {
Ok(Either::Left(pg_result)) => Ok(Either::Left(AnyQueryResult { ... })),
Ok(Either::Right(pg_row)) => Ok(Either::Right(AnyRow::from_pg(pg_row))),
Err(e) => Err(e),
}).boxed()
}
// ... 其他方法
}核心模式:把 AnyXxx 参数转成 PgXxx(arguments)、调具体 Connection 的方法、把 PgXxx 返回值转回 AnyXxx。类型转换层是 Any 驱动相对具体驱动的额外开销。
MySQL 和 SQLite 各自也有 any.rs 实现 AnyConnectionBackend——结构类似、各自包装自己的 Connection。
19.4 AnyConnection 的委托模式
any/connection/mod.rs:26-28:
rust
pub struct AnyConnection {
pub(crate) backend: Box<dyn AnyConnectionBackend>,
}单字段——一个 Box<dyn AnyConnectionBackend> trait object。所有方法都委托给 backend。
例如 AnyConnection::execute 的实现(any/connection/executor.rs):
rust
impl<'c> Executor<'c> for &'c mut AnyConnection {
type Database = Any;
fn fetch_many<'e, 'q, E>(self, query: E) -> BoxStream<'e, Result<Either<AnyQueryResult, AnyRow>, Error>>
where E: Execute<'q, Any>,
{
let sql = query.sql();
let persistent = query.persistent();
let arguments = /* ... */;
self.backend.fetch_many(sql, persistent, arguments).boxed()
}
// ...
}Executor impl 本质是"拆包 Execute trait 的数据、传给 backend"——backend 做真正的工作。这层平坦代理让 AnyConnection 的代码极其简短——真正的复杂度都在 backend 的具体实现里。
19.5 AnyValueKind:三家类型系统的交集
any/value.rs:11-20:
rust
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum AnyValueKind<'a> {
Null(AnyTypeInfoKind),
Bool(bool),
SmallInt(i16),
Integer(i32),
BigInt(i64),
Real(f32),
Double(f64),
Text(Cow<'a, str>),
Blob(Cow<'a, [u8]>),
}9 个 variant——Any 支持的全部值类型。这是三家 DB 类型系统的交集 + 基础整数扩展。
支持的:
- NULL(带类型信息)
- Bool(Postgres 原生、MySQL 用 TINYINT(1) 模拟、SQLite 用 INTEGER)
- SmallInt / Integer / BigInt(i16 / i32 / i64)
- Real / Double(f32 / f64)
- Text(UTF-8 字符串)
- Blob(字节序列)
不支持的(Postgres 有但 Any 没有):
- ARRAY 类型(Postgres 独有)
- JSON / JSONB(MySQL 有但 sqlite 无;Any 不统一)
- UUID(Postgres 原生、其他两家当 TEXT)
- 日期时间各种变体(Postgres 有 TIMESTAMPTZ / INTERVAL 等很多)
- Postgres 自定义 ENUM
- 所有网络 / 几何类型(Postgres 独有)
这让 Any 只能用于"最基础的 CRUD"——用户代码碰到"JSONB 列"的场景立即没法用 Any。
19.5.1 类型转换策略
每家驱动的 any.rs 要把自己的具体类型转成 AnyValueKind。例如 Postgres:
rust
fn convert_pg_value_to_any(pg_value: PgValueRef<'_>) -> AnyValueKind<'_> {
match pg_value.type_info().0 {
PgType::Bool => AnyValueKind::Bool(pg_value.decode::<bool>().unwrap()),
PgType::Int2 => AnyValueKind::SmallInt(pg_value.decode::<i16>().unwrap()),
PgType::Int4 => AnyValueKind::Integer(pg_value.decode::<i32>().unwrap()),
PgType::Int8 => AnyValueKind::BigInt(pg_value.decode::<i64>().unwrap()),
PgType::Float4 => AnyValueKind::Real(pg_value.decode::<f32>().unwrap()),
PgType::Float8 => AnyValueKind::Double(pg_value.decode::<f64>().unwrap()),
PgType::Text | PgType::Varchar => AnyValueKind::Text(Cow::Owned(pg_value.decode::<String>().unwrap())),
PgType::Bytea => AnyValueKind::Blob(Cow::Owned(pg_value.decode::<Vec<u8>>().unwrap())),
other => /* 报错或转成 Text 退化 */,
}
}复杂类型(JSONB / UUID / ARRAY)无法转成 AnyValueKind——sqlx 的 Any Row 在这些列上报错或退化成 Text。
SQLite 的转换更简单(类型少):
rust
fn convert_sqlite_value_to_any(sqlite_value: SqliteValueRef<'_>) -> AnyValueKind<'_> {
match sqlite_value.data_type {
SqliteDataType::Integer => AnyValueKind::BigInt(sqlite_value.int64()),
SqliteDataType::Real => AnyValueKind::Double(sqlite_value.double()),
SqliteDataType::Text => AnyValueKind::Text(...),
SqliteDataType::Blob => AnyValueKind::Blob(...),
SqliteDataType::Null => AnyValueKind::Null(AnyTypeInfoKind::Null),
}
}SQLite 的 INTEGER 永远转成 BigInt(i64)——因为 SQLite 没有 INT4 的区分。这也是为什么 AnyValueKind 有 SmallInt / Integer / BigInt 三个变体——让 Postgres 能保留精度信息、SQLite 只用 BigInt。
19.6 AnyArguments 的实现
any/arguments.rs 的核心:
rust
pub struct AnyArguments<'q> {
pub values: AnyArgumentBuffer<'q>,
}
pub struct AnyArgumentBuffer<'q>(pub(crate) Vec<AnyValueKind<'q>>);和 PgArguments / MySqlArguments 不同——AnyArguments 不 encode 字节——它是一个 Vec<AnyValueKind>——每参数一个 enum 变体。
Encode<'q, Any> for i32 的实现(简化):
rust
impl<'q> Encode<'q, Any> for i32 {
fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
buf.0.push(AnyValueKind::Integer(*self));
Ok(IsNull::No)
}
}不 encode 字节、不选 wire format——只是往 Vec 里 push 一个 enum。真正的字节 encode 在 backend.fetch_many 里、把 AnyArguments 转成 PgArguments/MySqlArguments/SqliteArguments 时发生。
这种**"延迟到 backend 层 encode"**模式让 AnyArguments 的实现极其简单——但 encode 工作推迟到具体驱动层、总开销不变。
19.6.1 Option<T> 的特殊处理
any/mod.rs:60-80:
rust
impl<'q, T> Encode<'q, Any> for Option<T>
where T: Encode<'q, Any> + 'q + Type<Any>,
{
fn encode_by_ref(&self, buf: &mut AnyArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
if let Some(value) = self {
value.encode_by_ref(buf)
} else {
buf.0.push(AnyValueKind::Null(T::type_info().kind));
Ok(IsNull::Yes)
}
}
}Null 带类型信息——AnyValueKind::Null(AnyTypeInfoKind)——因为不同 DB 对 NULL 参数期望不同类型信息(Postgres 要 OID)。T::type_info().kind 在 encode 时确定——让 backend 能重新构造合适的 NULL。
19.7 install_drivers + OnceCell 的全局注册
any/driver.rs:125-133:
rust
static DRIVERS: OnceCell<&'static [AnyDriver]> = OnceCell::new();
pub fn install_drivers(drivers: &'static [AnyDriver])
-> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
{
DRIVERS.set(drivers).map_err(|_| "drivers already installed".into())
}OnceCell<&'static [AnyDriver]> —— 进程级单次初始化。第一次 set 成功、后续 set 报错。典型用法:
rust
fn main() {
sqlx::any::install_default_drivers(); // 内部调 install_drivers
// 或手动
sqlx::any::install_drivers(&[PG_DRIVER, MYSQL_DRIVER]).unwrap();
// ... app 主体
}install_default_drivers 注册所有 feature 启用的 backend——最常见用法。用户一开始就调、之后的 AnyConnection::connect 能正常分发。
不调 install_drivers 直接 connect 会 panic——any/driver.rs:155 的 expect:
rust
let drivers: &[AnyDriver] = DRIVERS.get().expect("No drivers installed. ...");这个 panic 是刻意的——告诉用户"你忘了 install"——比 silent fail 好。
19.8 URL scheme 分发
any/driver.rs:141-162 的 from_url:
rust
pub(crate) fn from_url(url: &Url) -> crate::Result<&'static AnyDriver> {
let scheme = url.scheme();
let drivers: &[AnyDriver] = DRIVERS.get().expect("No drivers installed. ...");
drivers.iter()
.find(|driver| driver.url_schemes.contains(&url.scheme()))
.ok_or_else(|| Error::Configuration(
format!("no driver found for URL scheme {scheme:?}").into()
))
}三步:
- 从 URL 提取 scheme(
postgres://的 scheme 是 "postgres")。 - 从 OnceCell 取已注册的 driver 列表。
- 扫描找第一个 url_schemes 包含当前 scheme 的 driver。
AnyDriver::new::<DB>() 创建时把 DB::URL_SCHEMES(第 3 章 §3.8)拷贝到 url_schemes——所以 PG_DRIVER.url_schemes = &["postgres", "postgresql"]、MYSQL_DRIVER.url_schemes = &["mysql", "mariadb"]、SQLITE_DRIVER.url_schemes = &["sqlite"]。
找到 driver 后,调其 connect 函数指针(AnyDriver::connect 字段)—— monomorphize 成具体驱动的 connect_with_db::<Postgres> / _::<MySql> / _::<Sqlite>——建立底层 Connection、包成 Box<dyn AnyConnectionBackend>。
19.9 AnyDriver 的 const fn 构造
any/driver.rs:38-50:
rust
impl AnyDriver {
pub const fn without_migrate<DB: Database>() -> Self
where
DB::Connection: AnyConnectionBackend,
<DB::Connection as Connection>::Options:
for<'a> TryFrom<&'a AnyConnectOptions, Error = Error>,
{
Self {
name: DB::NAME,
url_schemes: DB::URL_SCHEMES,
connect: DebugFn(AnyConnection::connect_with_db::<DB>),
migrate_database: None,
}
}
}const fn——让 AnyDriver::new::<DB>() 能在 const context 使用(比如 static DRIVERS: OnceCell<...> 的初始化)。
connect: DebugFn(AnyConnection::connect_with_db::<DB>) ——泛型函数指针。DB 在 new 的调用点 monomorphize——每家驱动一个实例:
rust
// 用户或内部代码:
const PG_DRIVER: AnyDriver = AnyDriver::without_migrate::<Postgres>();
const MYSQL_DRIVER: AnyDriver = AnyDriver::without_migrate::<MySql>();这是编译期的 monomorphization —— 具体驱动的 connect 函数在编译后就存在;运行时 AnyDriver.connect 调用走一次间接函数指针调用(几纳秒开销)——没有动态分发的 vtable 查找。
这条设计让 Any 驱动的 connect 阶段几乎零开销——监 overhead 集中在后续的 BoxFuture / trait object 上。
19.10 Any 的代价
使用 Any 驱动有四条明确代价:
1. BoxFuture 堆分配
每次 async_method().await 返回 BoxFuture——Pin<Box<dyn Future>>——一次堆分配(~50-100ns)。具体驱动用 impl Future 无分配。
在热路径(比如每秒数千 query)这条代价可见——但相对 DB round-trip(毫秒级)仍微小。
2. 类型信息压平
AnyValueKind 的 9 个 variant 只能表达三家 DB 的基础类型。具体驱动能用的 Postgres OID 23(INT4)在 Any 下变成 Integer——精度信息丢失。
用 Any 时只能用这 9 种类型——复杂业务类型(JSONB、Uuid、数组)直接不可用。
3. 功能子集
Postgres 独有的 LISTEN/NOTIFY、COPY、advisory lock——Any 都没有。MySQL 的 last_insert_id——AnyQueryResult 有等价字段但语义不完全对齐三家。
用 Any 只能用三家 DB 的交集功能——很窄。
4. 编译期校验丢失
query! 宏不支持 Any——因为编译期不知道具体 DB 方言。用户只能用运行时 query_as::<Any, User>——放弃 query! 的类型安全。
这四条代价合起来让 Any 不适合业务核心代码——只适合"工具 / 测试 / 通用管理"场景。
19.11 Any 的合理场景
虽然代价多,Any 仍然有刚需场景:
19.11.1 sqlx-cli
cargo sqlx migrate run 需要对任意 DB 执行迁移 SQL——一个 binary 通吃。实现上 sqlx-cli 用 AnyPool + 运行时解析 DATABASE_URL:
rust
let pool = AnyPoolOptions::new().connect(&database_url).await?;
for migration in migrations {
sqlx::raw_sql(&migration.sql).execute(&pool).await?;
}Migration SQL 基本用 DDL、很少涉及复杂类型——Any 的类型限制不构成问题。sqlx-cli 的 migrate / prepare / database 子命令都走 Any。
19.11.2 跨 DB 工具
比如schema 导出工具——读 information_schema、输出通用格式:
rust
let pool = AnyPool::connect(&url).await?;
let tables: Vec<AnyRow> = sqlx::query_as("SELECT table_name FROM information_schema.tables")
.fetch_all(&pool).await?;information_schema 在三家 DB 里结构略有差异(特别是列名)、但基础查询可用——用 Any 写一遍就能跨 DB。
19.11.3 测试用 SQLite / 生产用 Postgres
§18.22 的模式——业务代码用 AnyPool、测试注入 sqlite::memory: pool、生产注入 postgres://... pool:
rust
async fn get_user(pool: &AnyPool, id: i32) -> Result<Option<User>, Error> {
sqlx::query_as("SELECT * FROM users WHERE id = $1 OR id = ?") // 两家兼容语法
.bind(id).fetch_optional(pool).await
}这条模式要求 SQL 是 cross-compatible 子集——用户要自己把控语法兼容。
19.11.4 运行时选 DB 的小应用
比如一个 SQL REPL 工具——用户输入 URL、工具连、用户输入 SQL、工具执行:
rust
let url = read_line()?;
let pool = AnyPool::connect(&url).await?;
loop {
let sql = read_line()?;
let rows: Vec<AnyRow> = sqlx::query(&sql).fetch_all(&pool).await?;
print_rows(&rows);
}通用 SQL 客户端——按用户需求连任何 DB。Any 让这种工具可能。
19.12 编译期多态 vs 运行时多态的决策
用一张表总结 Any 和具体驱动的权衡:
| 维度 | 具体驱动(Postgres 等) | Any 驱动 |
|---|---|---|
| 分发时机 | 编译期(泛型 DB) | 运行时(trait object) |
| 类型信息 | 精确(PgTypeInfo / OID) | 9 种枚举 variant |
| 性能开销 | 最小(impl Future) | BoxFuture 堆分配、类型 enum match |
| 功能完整度 | 完整(protocol 扩展都可用) | 三家交集(无 LISTEN/COPY/JSONB 等) |
| query! 宏 | 支持 | 不支持 |
| 业务代码 | 推荐 | 不推荐 |
| CLI / 工具 | 可以但笨重 | 推荐 |
| 测试跨 DB | 多个 pool | 一个 pool |
简单规则:业务代码选具体驱动、工具代码选 Any——分场景用。
19.13 Any 驱动的实现要点
读完这章、从工程角度看 Any 驱动的六个设计要点:
1. trait object 用 BoxFuture 返回——让 async method 能放进 trait object。代价是堆分配、收益是动态分发可能。
2. const fn + 函数指针 = 编译期 vtable——AnyDriver::new::<DB>() monomorphize 出具体 connect 函数。运行时间接调用、但没有 vtable 开销。
3. URL scheme 做分发——drivers.iter().find(|d| d.url_schemes.contains(&scheme))——简单清晰。
4. OnceCell 做全局注册——进程级单次 install、之后只读。避免多线程同步问题。
5. AnyValueKind 枚举做类型交集——用 enum variant 表示三家 DB 类型的合集——运行时 match 取值。
6. 每家驱动实现 AnyConnectionBackend 做类型转换——Any 层的每个方法转发到具体驱动、数据结构互转——driver 层是真的工作发生地。
这六点让 Any 驱动 1500 行代码就能支撑跨 DB 通用的能力——巧妙的工程。
19.14 本章小结
本章讲完了 sqlx 的 Any 驱动:
- 运行时多态的场景(§19.1)—— sqlx-cli、跨 DB 工具、测试 / 生产不同 DB、插件应用。
- Any 的 15 个公开类型(§19.2)—— Any Database trait 家族完整实现、委托给 backend。
- AnyConnectionBackend trait(§19.3)—— 十几个方法 + BoxFuture 返回让 trait object-safe。
- AnyConnection 委托模式(§19.4)—— 单字段 Box
<dyn Backend>、方法都转发。 - AnyValueKind 9 变体(§19.5)—— 三家类型系统交集、复杂类型不可用。
- AnyArguments 延迟 encode(§19.6)—— Vec
<AnyValueKind>、不 encode 字节、backend 层转换。 - install_drivers + OnceCell(§19.7)—— 进程级单次注册。
- URL scheme 分发(§19.8)——
drivers.iter().find按 scheme 找。 - const fn AnyDriver::new(§19.9)—— 编译期 monomorphize、运行时间接调用。
- 四条代价(§19.10)—— BoxFuture 堆分配、类型信息压平、功能子集、query! 不可用。
- 四种合理场景(§19.11)—— CLI、跨 DB 工具、测试/生产分离、运行时选 DB。
- 决策表(§19.12)—— 业务用具体驱动、工具用 Any。
第五部分"驱动实现"到此正式结束——Postgres / MySQL / SQLite / Any 四章讲完。下一部分是第六部分"工具与工程"——迁移、日志、生产实战——本书最后三章。
19.15 Any 驱动的调用路径可视化
用 mermaid 画 Any 驱动的完整分发路径:
两段路径:
- 连接建立:URL 解析 → 查 OnceCell → 找 driver → 调其 connect → 包 Box → 返回 AnyConnection。
- 查询执行:AnyConnection 的方法 → backend trait object → 具体 Connection → 类型转换 → 返回。
每一条 query 都走类型转换——AnyArguments 转 PgArguments、PgRow 转 AnyRow——开销约几百纳秒。对 DB round-trip(毫秒级)来说可以忽略、但说明 Any 有"额外的数据结构转换层"。
19.16 Any 驱动的代码组织
sqlx-core/src/any/ 目录结构(约 1500 行):
any/
├── arguments.rs AnyArguments + AnyArgumentBuffer (~200 行)
├── column.rs AnyColumn (~100 行)
├── connection/
│ ├── mod.rs AnyConnection struct (~200 行)
│ ├── backend.rs AnyConnectionBackend trait (~100 行)
│ ├── executor.rs Executor impl (~200 行)
├── database.rs Any struct + Database impl (~80 行)
├── driver.rs AnyDriver + install_drivers (~250 行)
├── error.rs AnyDatabaseError (~50 行)
├── kind.rs AnyKind(已废弃,~50 行)
├── migrate.rs Migrator 支持(~150 行)
├── mod.rs 顶层 re-export + Option impl (~150 行)
├── options.rs AnyConnectOptions (~100 行)
├── query_result.rs AnyQueryResult (~50 行)
├── row.rs AnyRow (~150 行)
├── statement.rs AnyStatement (~100 行)
├── transaction.rs AnyTransactionManager (~150 行)
├── type_info.rs AnyTypeInfo + AnyTypeInfoKind (~100 行)
├── types/ 基础类型的 Encode/Decode(~300 行)
└── value.rs AnyValueKind + AnyValue + AnyValueRef (~200 行)总计约 1500 行——是 sqlx-postgres(19841 行)的 7.5%。因为 Any 不实现协议——只做类型擦除 + 方法委托。
这条比例让"加新 DB 驱动到 Any 支持" 成本可控——只要该驱动实现 AnyConnectionBackend(约 200-300 行 adapter)、Any 层自动 work。
19.17 Any 驱动的版本演进
Any 驱动从 0.3 到 0.8 演进:
- 0.3(2020):首次引入 Any——基础的 Box
<dyn>多态。 - 0.5:install_drivers 机制正式化——之前需要 feature gate 组合复杂。
- 0.6:AnyValueKind 枚举重构、统一三家类型。
- 0.7(GAT):大幅简化内部代码——去掉大量
for<'r> HasValueRef。 - 0.8(本书版本):migrate 支持加入 AnyDriver、sqlx-cli 全面用 Any。
0.9.0-alpha 传闻——Any 可能继续简化 + 修复某些 edge case 的类型丢失问题。
演进路径显示 Any 是 sqlx 团队稳定维护的一块——虽然用户少、但 sqlx-cli 深度依赖、不能放弃。这也是为什么 Any 的 code quality 超出用户数量应有的水准——因为它是内部工具的基础。
19.18 Any 和 Rust 生态的对比
Rust 里"运行时选 backend"的库不少——对比几个:
| 库 | 模式 | 特点 |
|---|---|---|
| sqlx::Any | Box<dyn AnyConnectionBackend> | 编译期列出 backend、运行时选 |
| reqwest | 固定 hyper backend | 不需要运行时多态 |
| redis-rs | Connection 变种 | 通过 enum 变体支持 Redis/Memcached |
| tracing subscriber | Box<dyn Subscriber> | 类似 sqlx Any、运行时换 backend |
| log | Log trait + set_logger | 同上 |
sqlx::Any 和 tracing::Subscriber 最像——都是"用 trait object 做 backend 抽象、启动时注册、运行时分发"。模式成熟、有效。
这种模式在 Rust 生态里是**"运行时后端"**的标准做法——理解 sqlx::Any 就理解了这类设计。迁移到你自己项目——比如"支持多种 cache backend 的抽象层"——同一套结构就能写。
19.19 Any 驱动的哲学观察
读完 Any 驱动、有一个哲学观察:
sqlx 同时提供了编译期多态(具体驱动)和运行时多态(Any)——承认两种多态各有用武之地、不做二选一。
这和一些语言的取向不同:
- Go 用 interface——运行时多态为主、编译期泛型直到 1.18 才加入。
- Java 用 interface——同上。
- C++ 可以用 template(编译期)或 virtual function(运行时)——传统 C++ 书经常"辩"哪个更好。
sqlx 的 Rust 实现同时拥抱两种——根据场景选合适的。这反映了 Rust 语言的实用主义——不教条、给工具、让用户选。
这条设计哲学值得迁移到你自己的 Rust 库——"提供两套 API、编译期多态的默认、运行时多态的 escape hatch"——覆盖 90% 场景(编译期)+ 剩下 10% 特殊场景(运行时)。
19.20 Any 驱动的实战案例
最后给一个真实可用的 Any 驱动案例——一个通用 DB 探索工具:
rust
use sqlx::{AnyPool, any::AnyConnectOptions, AnyRow, Row};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 注册所有驱动
sqlx::any::install_default_drivers();
// 从 stdin 读 URL
let url: String = std::env::args().nth(1).expect("usage: tool <url>");
let pool = AnyPool::connect(&url).await?;
// 通用元数据查询(三家 DB 都支持)
let tables: Vec<AnyRow> = sqlx::query(
"SELECT table_name FROM information_schema.tables
WHERE table_schema = $1 OR table_schema = ?"
).bind("public").fetch_all(&pool).await?;
println!("Tables:");
for row in tables {
let name: String = row.try_get("table_name").or_else(|_| row.try_get("TABLE_NAME"))?;
println!(" - {}", name);
}
Ok(())
}这段代码一份二进制处理三家 DB:
mysql://...走 sqlx-mysql 的 backend。postgres://...走 sqlx-postgres。sqlite::memory:走 sqlx-sqlite。
跨 DB 细节的处理:
- 列名大小写——Postgres lowercase、MySQL 保留 uppercase——try_get 两次回退。
- 占位符混用——
$1和?都写出来让 sqlx 的 format_placeholder 按当前 DB 选(实际要分 DB 写——但类似 demo 能跑)。
生产里这种工具有几十个——sqlx-cli 自己、Debezium / Maxwell 的 adapter、DataDog agent 的 DB 监控——都依赖 Any 这种运行时多态能力。
19.22 Any 驱动常见用法陷阱
生产代码里用 Any 的几个常见坑:
陷阱 1:忘了 install_drivers
rust
#[tokio::main]
async fn main() {
let pool = AnyPool::connect("postgres://...").await?; // panic!
}错误信息:No drivers installed. Please see the documentation in sqlx::any for details.。启动代码第一行必须调 sqlx::any::install_default_drivers() 或手动 install_drivers。
陷阱 2:写了 DB 特有语法期望跨 DB 运行
rust
// Postgres 下 OK、MySQL 下 SQL 语法错
sqlx::query("SELECT * FROM users WHERE id IN (SELECT unnest(ARRAY[1, 2, 3]))")
.fetch_all(&any_pool).await?;Any 不翻译 SQL——写的是什么、发的就是什么。跨 DB 代码必须用三家通用 SQL 子集——标准 SELECT / INSERT / UPDATE / DELETE + 基础函数。
陷阱 3:用了 Postgres 独有类型
rust
sqlx::query("INSERT INTO events (data) VALUES ($1)")
.bind(serde_json::json!({...})) // Any 不支持直接 bind JSON
.execute(&any_pool).await?;AnyValueKind 没有 JSON variant——只能 bind Text(把 JSON 序列化成字符串)。JSON 作为参数要自己 serde_json::to_string 再 bind。
陷阱 4:期望 Any 处理 nullability 推断
Any 不对接 describe 路径——nullability 完全靠 SQL 里 as "col!: Type" 或 FromRow 的 Option<T>。
陷阱 5:Any 和 query! 混用
rust
sqlx::query!("SELECT * FROM users WHERE id = ?", 42) // query! 不支持 Any
.fetch_one(&any_pool).await?;query! 宏在 compile 时需要具体 DB——Any 下只能用运行时 query / query_as。
记住这五条陷阱——Any 驱动是"把编译期多态降级成运行时多态"——原来靠编译期 check 的事现在靠用户自觉**。
19.23 Any 的未来展望
sqlx 0.9.0-alpha 的路线图里关于 Any 的几条传闻:
1. 更严格的类型系统——AnyValueKind 可能加新 variant(如 Json / Uuid)——扩大交集。 2. 更好的 nullability 支持——Any 可能接入简化的 describe 路径。 3. 第三方驱动整合——让用户 implement AnyConnectionBackend 注入 ClickHouse / TiDB 等 backend。
这些改进让 Any 变更实用——但核心机制不会变(Box<dyn> + OnceCell + URL 分发)——读懂本章的你能轻松跟上演进。
Any 作为 sqlx 架构的**"运行时多态层"**会继续存在——它的独特价值(sqlx-cli 的基础)无法被替代。哪怕 Rust 的 async trait 能力提升(dyn AsyncTrait 的新特性)让 Any 实现更简单——抽象角色不会消失。
19.24 Any 性能开销的定性分析
Any 相对具体驱动的额外开销来源(定性描述、具体数字请以你自己的 cargo bench 为准):
pool.acquire().await—— 具体驱动返回impl Future、可以在栈上或 async state machine 里驻留;Any 返回BoxFuture、每次 await 多一次堆分配 + 一次虚表间接。量级:纳秒级差异、在毫秒级 query 里不可见。query.execute()/fetch_one()—— 执行路径上是单次 BoxFuture 包装、后续 I/O 都是 DB RTT 主导。Any 额外开销占 query 总时间 << 1%。bind参数—— 具体驱动把值直接编码到 driver-specific buffer;Any 先推进AnyValueKind枚举(enum tag + owned 数据)、再由 backend 转换。每个 bind 多一个 enum push 的小开销。decode解码—— 具体驱动直接按Decode<DB>展开;Any 先到AnyValueKind再match分派到具体类型。比直接路径多一次 match。
总体判断:
- 稳态 query 的 Any 开销相对 DB RTT 可忽略——毫秒级 RTT 淹没微秒级 overhead。
- 极高 QPS(单机 10万+/s)纯内存路径才能看到 Any 开销累积——生产 DB 场景几乎遇不到。
- Any 的代价主要是功能子集和类型精度(不能用 UUID/JSON 等类型、不能用
query!())——不是速度。
想知道自己的场景准确数字——cargo bench 跑一次对照比读"我估计的数据"可靠 10 倍。
19.25 Any 驱动的实际使用门槛
最后一点实用观察——Any 驱动的学习门槛其实不低:
- 要理解 trait object 的限制(BoxFuture / Send bound / object safety)。
- 要理解类型擦除后信息量减少——不能用复杂类型。
- 要理解 install_drivers 的全局状态——每个进程一次、注册之后不能改。
- 要写跨 DB 兼容 SQL——方言差异自己处理。
这些门槛让 Any 不适合 Rust 新手——本书的前 11 章(query / query_as / Pool)是 sqlx 新手该学的;Any 属于"熟手才用的高级工具"。
所以如果你是新学 sqlx——先跳过 Any、用具体驱动把业务跑起来。半年后再回来看本章——你会发现 Any 能帮你解决当时没想到的场景(跨 DB 工具、动态配置)——那时学才事半功倍。
学习路径建议:
- 第 1 个月:第 1-11 章(基础用法)。
- 第 2-3 个月:第 12-15 章(Pool / Transaction,生产必备)。
- 第 4-6 个月:第 16-18 章(驱动实现,理解内部)。
- 半年后:第 19 章 Any(高级场景)+ 第 20-22 章(工程实战)。
按这个节奏走、你不会被深度淹没——每个阶段聚焦该阶段能用的。
19.26 Any 驱动源码中的精彩细节
收尾前再提几条sqlx-core/src/any/ 源码里的亮点:
1. impl Encode<'q, Any> for Option<T>(any/mod.rs:60-80)——特殊处理 NULL 的类型保留:
rust
if let Some(value) = self {
value.encode_by_ref(buf)
} else {
buf.0.push(AnyValueKind::Null(T::type_info().kind));
Ok(IsNull::Yes)
}NULL 带内层 T 的 type_info().kind——让 backend 能用这个 kind 生成合适的 Postgres OID NULL 等。小细节、大用处。
2. #[non_exhaustive] 标在 AnyValueKind(any/value.rs:10):
rust
#[non_exhaustive]
pub enum AnyValueKind<'a> { ... }告诉用户代码不能 exhaustive match——sqlx 未来加新 variant 不破坏兼容。这条向前兼容考虑在很多 trait 和 enum 上都有——sqlx 团队的长期维护意识。
3. AnyConnectionBackend::get_transaction_depth 默认 panic(any/connection/backend.rs:52):
rust
fn get_transaction_depth(&self) -> usize {
unimplemented!("...This is a provided method to avoid a breaking change, but it will become a required method in version 0.9 and later.");
}精彩的向前兼容技巧——0.8 给默认实现(panic)避免破坏老驱动、0.9 变 required method(driver 必须实现)。新功能渐进加入。
4. #[allow(deprecated)] pub use kind::AnyKind(any/mod.rs)—— 保留 deprecated 的 AnyKind export 避免用户代码 break——老 API 的graceful deprecation。
5. // NOTE: required due to the lack of lazy normalization(any/mod.rs:56)——impl_into_arguments_for_arguments!(AnyArguments<'q>); 的原因注释——解释 why、未来 Rust 改进了可以清理。
这五条细节不影响 Any 的核心功能——但体现代码持续维护的工艺。读这些细节让你学到"怎么把一个库维护 5-7 年不破坏用户代码"。
第 19 章正式结束。下一站第 20 章迁移系统。
19.27 AnyDriver 结构体:零成本的 backend 目录
Any 驱动的核心数据结构是 AnyDriver(sqlx-core/src/any/driver.rs:27-34):
rust
#[non_exhaustive]
pub struct AnyDriver {
pub(crate) name: &'static str,
pub(crate) url_schemes: &'static [&'static str],
pub(crate) connect:
DebugFn<fn(&AnyConnectOptions) -> BoxFuture<'_, crate::Result<AnyConnection>>>,
pub(crate) migrate_database: Option<AnyMigrateDatabase>,
}四个字段:
name——driver 名("PostgreSQL"/"MySQL"/"SQLite")——诊断用。url_schemes——一个静态切片——每种 URL scheme prefix(["postgres", "postgresql"])——用来匹配 URL。connect——函数指针包在DebugFn里—— 创建具体 backend 的 connect 逻辑——这一个字段就是多态派发点。migrate_database——可选的 migrate-trait 适配器——只有启用migratefeature 且底层 DB 实现了MigrateDatabase才 Some。
零成本的多态——所有字段都是 &'static 或函数指针——编译期决定、运行时只读——比 trait object 更紧凑(8+16+16+24 = 64 字节左右、无 vtable 间接)。
DebugFn 是一个 trick(sqlx-core/src/common/mod.rs)—— 把函数指针包一层、实现 Debug(函数指针默认不 Debug)—— 让 AnyDriver 能 derive Debug。小细节、但体现 sqlx 对开发者工效的关注——出错能 println!。
19.28 declare_driver_with_optional_migrate 宏
源码(sqlx-core/src/any/driver.rs:14-25):
rust
#[macro_export]
macro_rules! declare_driver_with_optional_migrate {
($name:ident = $db:path) => {
#[cfg(feature = "migrate")]
pub const $name: $crate::any::driver::AnyDriver =
$crate::any::driver::AnyDriver::with_migrate::<$db>();
#[cfg(not(feature = "migrate"))]
pub const $name: $crate::any::driver::AnyDriver =
$crate::any::driver::AnyDriver::without_migrate::<$db>();
};
}这个宏让 sqlx-postgres / sqlx-mysql / sqlx-sqlite crate 声明自己的 AnyDriver 常量:
rust
// sqlx-postgres
declare_driver_with_optional_migrate!(DRIVER = Postgres);展开后得到 pub const DRIVER: AnyDriver = AnyDriver::with_migrate::<Postgres>()—— 一个编译期常量—— 零运行时初始化开销。
with_migrate / without_migrate 都是 const fn—— 编译期计算整个 AnyDriver 结构体值、产出 const 常量。用户注册时只是把这个常量加入 DRIVERS: OnceCell<&'static [AnyDriver]> 切片——零分配、零运行时构造。
19.29 OnceCell 全局注册与 URL 分派
静态 DRIVERS(sqlx-core/src/any/driver.rs:12):
rust
static DRIVERS: OnceCell<&'static [AnyDriver]> = OnceCell::new();语义:
- 进程生命周期内只能 set 一次——多次 set 报错。
- set 后不可变——所有后续读都拿到同一份。
OnceCell::get()是 lock-free fast path——每次 query 都会走一次、不能慢。
用户 API:
rust
sqlx::any::install_default_drivers();
// 或手动
sqlx::any::install_drivers(&[sqlx::postgres::any::DRIVER, sqlx::mysql::any::DRIVER]);URL → Driver 分派逻辑——查 DRIVERS 中每个 driver 的 url_schemes、找第一个前缀匹配的:
rust
pub fn from_url(url: &Url) -> Result<&'static AnyDriver> {
let scheme = url.scheme();
DRIVERS.get()
.ok_or_else(|| /* 未初始化 */)?
.iter()
.find(|d| d.url_schemes.contains(&scheme))
.ok_or_else(|| /* 找不到 */)
}这个线性扫描 看起来低效——实际最多 3-4 个 driver—— O(n) 性能等同 O(1)—— 比 HashMap 还快(不需要 hash 计算 + cache miss)。
与 AnyKind 的关系——早期 sqlx 有一个 AnyKind 枚举(sqlx-core/src/any/kind.rs:12)显式列举四种 DB——现在标记为 #[deprecated]、不再使用——被这套 AnyDriver + url_schemes 分派取代——**"枚举转切片"**是 sqlx 架构演进的一步关键重构——让新 DB 加入不需要改 sqlx-core 的枚举。
19.30 AnyMigrateDatabase:Option 背后的 feature 分支
AnyDriver 里的 migrate_database: Option<AnyMigrateDatabase> 字段(sqlx-core/src/any/driver.rs:33)——Option 不是冗余、而是feature 差分的核心设计。
源码两个 with_migrate impl(:51-77):
rust
#[cfg(not(feature = "migrate"))]
pub const fn with_migrate<DB: Database>() -> Self
where
DB::Connection: AnyConnectionBackend,
<DB::Connection as Connection>::Options:
for<'a> TryFrom<&'a AnyConnectOptions, Error = Error>,
{
Self::without_migrate::<DB>()
}
#[cfg(feature = "migrate")]
pub const fn with_migrate<DB: Database + crate::migrate::MigrateDatabase>() -> Self
// ...
{
Self {
migrate_database: Some(AnyMigrateDatabase {
create_database: DebugFn(DB::create_database),
database_exists: DebugFn(DB::database_exists),
drop_database: DebugFn(DB::drop_database),
force_drop_database: DebugFn(DB::force_drop_database),
}),
..Self::without_migrate::<DB>()
}
}两个 impl 的区别:
- 没有 migrate feature 时——
with_migrate<DB>只要求DB: Database—— 等价于without_migrate——migrate_database字段为None。 - 有 migrate feature 时——
with_migrate<DB>要求DB: Database + MigrateDatabase—— 把四个 migration-lifecycle 函数(create_database/exists/drop/force_drop)的函数指针打包进AnyMigrateDatabase。
Option 的真正含义——"有没有启用 migrate feature" 的运行时可见证据—— 用户调用 any_migrate_database() 时如果 None、返回 "feature not enabled" 错误——而不是编译期直接拒绝编译——让上层代码可以跨 feature 写一份。
这是 Rust 条件编译的高级用法—— "同一个 API 表面在有/无 feature 时行为不同但类型相同"—— 相比传统的 #[cfg] 分裂函数名(any_migrate_database_migrate vs any_migrate_database_nomigrate)—— 这种 Option 承载方式让用户代码简单。
19.31 AnyConnectOptions:极简的两字段
具体驱动的 PgConnectOptions 有几十个字段—— Any 的 AnyConnectOptions 只有两个(sqlx-core/src/any/options.rs:17-22):
rust
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct AnyConnectOptions {
pub database_url: Url,
pub log_settings: LogSettings,
}为什么如此精简?
- database_url—— 一条 URL 字符串包含一切——
postgres://user:pass@host:5432/db?sslmode=require&statement_cache_capacity=200—— 所有 backend-specific 参数走 query string。 - log_settings—— 日志级别—— 因为 log_statements / log_slow_statements 是 ConnectOptions trait 的必须方法(sqlx-core/src/connection.rs:254-259)。
URL 不在 connect 时解析—— from_str 和 from_url 只做 URL 语法校验、不解析 scheme 特定字段(比如 Postgres 的 statement_cache_capacity、MySQL 的 no_engine_substitution)—— 这些延迟到 AnyConnection::connect 里、根据 URL scheme 选具体 driver、再让该 driver 的 from_url 做完整解析。
延迟解析的好处—— Any 驱动层不需要知道 backend-specific 参数—— 加一个新的 backend 字段(比如未来 Postgres 加个 prepare_threshold 参数)不改 Any 代码—— 开闭原则的完美体现。
from_url 只 clone(options.rs:39-44):
rust
fn from_url(url: &Url) -> Result<Self, Error> {
Ok(AnyConnectOptions {
database_url: url.clone(),
log_settings: LogSettings::default(),
})
}6 行代码—— 对比 PgConnectOptions::from_url 的 100+ 行—— Any 层薄得让人感动。
19.32 connect_with_db:泛型入口函数
AnyConnection::connect_with_db<DB>(any/connection/mod.rs:43 起)—— 把具体 DB 的 Connection 包装成 AnyConnection:
rust
pub(crate) fn connect_with_db<DB: Database>(
options: &AnyConnectOptions,
) -> BoxFuture<'_, Result<AnyConnection, Error>>
where
DB::Connection: AnyConnectionBackend,
<DB::Connection as Connection>::Options:
for<'a> TryFrom<&'a AnyConnectOptions, Error = Error>,
{
// 1. TryFrom<&AnyConnectOptions> for DB::ConnectOptions
// 2. DB::Connection::connect(options)
// 3. 包装成 Box<dyn AnyConnectionBackend>
}三步:
- TryFrom 转换—— AnyConnectOptions → 具体 ConnectOptions—— 具体 driver 实现这个 TryFrom、读 URL query string、填好所有字段。
- Connect—— 调
Connection::connect(options)建连接—— 此时具体 driver 已全部生效。 - 包装——
Box<dyn AnyConnectionBackend>—— 类型擦除完成。
where 子句里的 for<'a> TryFrom<&'a AnyConnectOptions, Error = Error> 是关键—— HRTB(Higher-Rank Trait Bound)—— 意思是"对任何 lifetime 'a 都能 TryFrom"——通用、无生命周期限制。
这个函数是 AnyDriver 结构体 connect 字段的函数指针值(见第 19.27 节 DebugFn(AnyConnection::connect_with_db::<DB>))—— 编译期 monomorphize 为每家 DB 一份——运行时通过函数指针表分派。
"编译期 monomorphize + 函数指针表" 是 Rust 做运行时多态的高性能姿势—— 避开 trait object 的 vtable 间接—— Any 驱动这 segment 的设计是生产级 Rust 的教科书。
19.33 Any 的 BoxFuture 开销量化
前面多次提到"BoxFuture 有开销"——这里给一个可量化的数字感。
BoxFuture 是什么—— Pin<Box<dyn Future<Output = T> + Send + 'a>>—— 一个堆上分配的、类型擦除的异步任务。
三条开销来源:
- 堆分配—— 每次创建 BoxFuture 都走
Box::new、触发 Rust 分配器。分配器快慢取决于 allocator 实现(jemalloc / mimalloc / 系统默认 malloc)——都是纳秒级。 - 虚表间接—— 每次
.await调用 Future 的poll通过 vtable、比直接泛型 poll 多一次间接跳转、破坏 CPU 预测执行。 Sendbound 合成检查——编译器在每次 BoxFuture 构造点确认底层 Future 捕获的值都Send——这是编译期开销、不是运行时。
量级比较——两者都在纳秒级,BoxFuture 路径是具体驱动的数倍、但 DB I/O 是毫秒级——BoxFuture 的纳秒差异被 I/O 量级淹没。
什么时候 BoxFuture 开销才可能显形?
- 无 I/O 纯内存操作—— DB 场景不存在。
- 极高 QPS 单机 + 极短 query—— 只有 CPU-bound 纯内存 DB(Redis 之类)才可能看到累积效应——sqlx 不服务这种场景。
- 嵌入式 / 低功耗设备—— 内存分配的能耗代价比想象大、避开 Any 合理。
结论—— Any 的 BoxFuture 开销对 99% 生产 DB 场景完全可忽略—— 不要被"运行时多态慢"的教条劝退—— 如果担心、自己 cargo bench 跑一次比凭直觉强。
19.34 Any 驱动在 sqlx-cli 的关键作用
sqlx-cli(sqlx-cli/src/)是 sqlx 官方的命令行工具—— 提供 migrate add/run/info、database create/drop、prepare 等命令—— 全部基于 Any 驱动实现。
设计动机:
sqlx migrate run --database-url $URL—— URL 可能是postgres://,mysql://,sqlite://—— 同一个 CLI 二进制要处理任意 DB。- CLI 如果按 DB 分成三个二进制(
sqlx-pg,sqlx-mysql,sqlx-sqlite)—— 用户安装和维护都麻烦。 - 用 Any 驱动—— 一个二进制、运行时根据 URL scheme 分派—— UX 完胜。
实际代码(sqlx-cli/src/migrate.rs 附近):
rust
use sqlx::any::install_default_drivers;
pub async fn run(url: &str) -> anyhow::Result<()> {
install_default_drivers();
let mut conn = AnyConnection::connect(url).await?;
let migrator = Migrator::new(Path::new("./migrations")).await?;
migrator.run(&mut conn).await?;
Ok(())
}三行就搞定—— install_default_drivers + AnyConnection::connect + Migrator::run—— CLI 通用化的魔法。
Any 驱动的这个应用场景 让它无法被替代—— 即使某一天 dyn AsyncTrait 简化了运行时多态的实现—— sqlx-cli 的需求本身就是 Any 驱动的理由。
所以本章一再强调 "业务代码别用 Any、工具代码才用 Any"—— Any 不是劝退、而是定位—— 有人确实需要它。sqlx-cli 就是最大的那个使用者。
19.35 AnyValueKind 的编码 / 解码细节
AnyValueKind 枚举是 Any 驱动类型系统的核心交集(sqlx-core/src/any/value.rs:11-20):
rust
#[non_exhaustive]
pub enum AnyValueKind<'a> {
Null(AnyTypeInfoKind),
Bool(bool),
SmallInt(i16),
Integer(i32),
BigInt(i64),
Real(f32),
Double(f64),
Text(Cow<'a, str>),
Blob(Cow<'a, [u8]>),
}9 个 variant—— 三家 DB 的最大公约子集。
为什么没有 Uuid / Json / Timestamp?
- Uuid—— Postgres 原生 UUID 类型、MySQL 用 CHAR(36)、SQLite 存 TEXT—— 没有三家统一二进制表达—— Any 放弃。
- Json—— Postgres jsonb/json、MySQL JSON、SQLite TEXT(无原生)—— 同上。
- Timestamp—— 三家语义差异太大(Postgres timestamptz、MySQL DATETIME、SQLite 存 TEXT/INTEGER)—— Any 只承诺 Text 层。
用户绕过办法——把 UUID/JSON 序列化成 String、bind(string)、走 Text variant—— 在应用层维持类型语义。
Cow<'a, str> / Cow<'a, [u8]>—— 允许借用或拥有—— Text 可以是字符串字面量的借用(零拷贝)也可以是拥有的 String(跨 await 安全)。
encode 路径——具体驱动把 AnyValueKind match 分派:
rust
// sqlx-postgres/src/any.rs 大致逻辑
match kind {
AnyValueKind::Null(_) => pg_encode_null(),
AnyValueKind::Bool(b) => pg_encode_bool(b),
AnyValueKind::SmallInt(i) => pg_encode_int2(i),
AnyValueKind::Integer(i) => pg_encode_int4(i),
AnyValueKind::BigInt(i) => pg_encode_int8(i),
AnyValueKind::Real(f) => pg_encode_float4(f),
AnyValueKind::Double(f) => pg_encode_float8(f),
AnyValueKind::Text(s) => pg_encode_text(s),
AnyValueKind::Blob(b) => pg_encode_bytea(b),
}9 分支 match—— match 分支编译成 jump table—— 每条分支调具体 Postgres 的 encode 函数—— 运行时开销是一次 jump + 一次函数调用—— 量级 ns。
#[non_exhaustive]—— 意味着未来可能加 variant(比如 Uuid、Timestamp)—— 用户 match 必须带 _ =>—— 避免将来升级 breaking。
这个 variant 数目和内容是sqlx 对"多 DB 通用子集"精心裁剪的结果—— 选 9 个是权衡"覆盖度 vs 实现复杂度"—— 少一个用户抱怨、多一个具体驱动实现多写代码—— 当前的 9 是均衡点。
19.36 AnyRow::map_from:从具体 Row 做类型降级
Any 驱动怎么把 PgRow / MySqlRow / SqliteRow 转成 AnyRow?核心方法是 AnyRow::map_from(sqlx-core/src/any/row.rs:82-125):
rust
#[doc(hidden)]
pub fn map_from<'a, R: Row>(
row: &'a R,
column_names: Arc<crate::HashMap<UStr, usize>>,
) -> Result<Self, Error>
where
usize: ColumnIndex<R>,
AnyTypeInfo: for<'b> TryFrom<&'b <R::Database as Database>::TypeInfo, Error = Error>,
AnyColumn: for<'b> TryFrom<&'b <R::Database as Database>::Column, Error = Error>,
bool: Type<R::Database> + Decode<'a, R::Database>,
i16: Type<R::Database> + Decode<'a, R::Database>,
i32: Type<R::Database> + Decode<'a, R::Database>,
i64: Type<R::Database> + Decode<'a, R::Database>,
f32: Type<R::Database> + Decode<'a, R::Database>,
f64: Type<R::Database> + Decode<'a, R::Database>,
String: Type<R::Database> + Decode<'a, R::Database>,
Vec<u8>: Type<R::Database> + Decode<'a, R::Database>,
{
let mut row_out = AnyRow { column_names, columns: ..., values: ... };
for col in row.columns() {
let value = row.try_get_raw(col.ordinal())?;
let type_info = AnyTypeInfo::try_from(&value.type_info())?;
// 把具体驱动的 ValueRef 按 type_info.kind decode 成 AnyValueKind
// ...
}
Ok(row_out)
}where 子句 9 个约束—— 约束"R::Database 必须支持 bool/i16/i32/i64/f32/f64/String/Vec<u8> 的 Type + Decode"—— 这是 AnyValueKind 9 个 variant 的对应需求。
每家具体 DB 都满足这 9 条—— Postgres 的 bool/int2/int4/int8/float4/float8/text/bytea—— MySQL 的同等类型—— SQLite 的存储类别。
注释里的重要决策(:76-78):
This is not a
TryFromimpl because trait impls are easy for users to accidentally become reliant upon, even if hidden, but we want to be able to change the bounds on this function as theAnydriver gains support for more types.
sqlx 故意不实现 TryFrom<PgRow> for AnyRow—— 因为 trait impl 会被用户 "accidentally rely on"—— 一旦用户在业务代码写 any_row: AnyRow = pg_row.try_into()?; —— 改 AnyRow 的 bound(增加 variant 或类型支持)就会 breaking。
用独立的 map_from 方法—— #[doc(hidden)]—— 明确"只给 sqlx 内部用"—— 保留未来扩展 bound 的自由—— 这是公共 API 克制的经典案例。
对读者的启示—— 设计库时不要乱给 From/TryFrom—— 每个 impl 都是永久承诺—— 不如用显式方法、保留改动空间。
衍生思考—— 同样的理由适用于 #[derive(Deserialize)] 和 From 的关系——sqlx 不给 AnyRow 写 Deserialize(会让 AnyRow 被当成任意序列化源头)、也不给 From(同上)—— 用 map_from 这种命名方法—— 明确意图、边界清楚—— 是公共 API 表面控制的成熟表现。这条纪律在任何 Rust 生产库里都有效—— 少一个自动 impl、少一次 breaking change 机会—— API 稳定性的长期红利远大于"写起来方便"的短期收益。
和 #[non_exhaustive] 的配合—— 前面 §19.35 讲 AnyValueKind 的 #[non_exhaustive] 防 match 扩展 breaking—— 这里的不实现 TryFrom 是同一哲学的另一个切面—— sqlx 对未来自由度的保留是系统性的、不是零散的。这种整体一致的 API 演进纪律是库成熟度的标志—— 值得你在自己项目里对照检视。
19.37 URL scheme 到 backend 的落地
用户调用 sqlx::any::install_default_drivers() 之后、DRIVERS 里至少有三个 AnyDriver——怎么从连接 URL 选对一个?
代码路径:
AnyConnection::connect(url)被调用。- 解析 URL——提取 scheme(
"postgres"/"postgresql"/"mysql"/"mariadb"/"sqlite")。 - 扫
DRIVERS—— 找第一个url_schemes切片里包含该 scheme 的 driver。 - 调用
driver.connect(options)—— 实际创建对应后端的连接。
为什么接受多个 scheme per driver——因为 Postgres 可以用 postgres:// 或 postgresql:// 两种 URL 形式—— url_schemes = &["postgres", "postgresql"]——MySQL 可以用 mysql:// 或 mariadb://——url_schemes = &["mysql", "mariadb"]——数组形式开箱支持别名。
和以前的 AnyKind 枚举对比——AnyKind 在 FromStr 里硬编码每个 URL 前缀(sqlx-core/src/any/kind.rs:32-62)—— 每加一种 DB 要改 enum + FromStr—— 现在的 url_schemes 切片让 driver 自己声明——扩展不改 sqlx-core—— 是开放式架构对封闭式枚举的胜利。
这段演进—— AnyKind 被 #[deprecated] 标注(:10)但保留—— 是 sqlx 架构改进的活化石——读源码时能看到旧世界和新世界并存——可作为 Rust API 迁移的一个小型案例。
下一章第 20 章进入迁移系统——第六部分工具与工程的开始。