SQLx 源码深度解析

第19章 Any 驱动:运行时多态 vs 编译期多态

作者 杨艺韬 · 11,743 字 · 发布于 · 更新于

sqlx 别处都是 DB: Database 的编译期泛型,Any 驱动是唯一的例外——用类型擦除换来按 URL scheme 运行时分发的能力,代价是只剩三家数据库类型系统的交集。

本章要点

  • Any 驱动(sqlx-core/src/any/,约 1800 行)是 sqlx 里唯一的运行时多态特例——其他地方都是 DB: Database 编译期泛型,Any 用 Box<dyn AnyConnectionBackend> 做类型擦除。
  • AnyConnectionBackend trait(sqlx-core/src/any/connection/backend.rs:9)—— 20 个方法覆盖 Connection、TransactionManager 与 Executor 的职责,用 BoxFuture / BoxStream 返回值让 trait 可以做成 trait object。每家驱动(Postgres/MySQL/SQLite)都有对应的 AnyConnectionBackend 实现。
  • AnyValueKind 枚举(sqlx-core/src/any/value.rs:11-21)—— 9 个 variant(Null/Bool/SmallInt/Integer/BigInt/Real/Double/Text/Blob)—— 三家 DB 类型系统的交集。Postgres 的数组 / JSONB / UUID 在 Any 下不可用。
  • install_drivers + OnceCell<&'static [AnyDriver]>(sqlx-core/src/any/driver.rs:125)—— 进程级单次注册;URL scheme 分发靠扫描 drivers.iter().find(|d| d.url_schemes.contains(&scheme))。
  • AnyDriver::with_migrate::<DB>() / without_migrate::<DB>() 是 const fn(sqlx-core/src/any/driver.rs:37、:62)—— 让 declare_driver_with_optional_migrate! 宏能在各驱动 crate 里声明 pub const DRIVER: AnyDriver。
  • Any 的代价:多一层 dyn 调用和参数 / 结果行的类型转换、类型信息压成 enum(原始类型身份丢失,映射表外的类型直接报错)、功能子集(只能用三家 DB 交集)。
  • Any 的合理场景:sqlx-cli 的 migrate/database 命令、跨 DB 的管理工具、"测试用 SQLite、生产用 Postgres" 的应用(§18.23)。
  • 业务代码几乎不该用 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.23 的模式。本地 :memory: SQLite、生产 Postgres——同一份业务代码。

4. 插件型应用——用户在配置文件里选 DB、app 不重新编译就能切换。

这些场景都需要运行时多态——Rust 的trait object 是标准解法。sqlx 的 Any 驱动就是这条路径的完整实现。

但 Any 有显著代价——详见 §19.10。大多数业务代码不该用 Any——它是"特殊场景的特殊工具",不是 "默认"。理解这条定位是本章的核心。

19.2 Any 的类型家族全貌

sqlx-core/src/any/mod.rs:28-52 列出 Any 的17 个公开 re-export(含 AnyConnectionBackend trait)+ 2 个类型别名(:55 另有一个 AnyExecutor trait 别名):

pub use arguments::{AnyArgumentBuffer, AnyArguments};
pub use column::AnyColumn;
pub use connection::AnyConnection;
pub use connection::AnyConnectionBackend;
pub use database::Any;
#[allow(deprecated)]
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};
#[doc(hidden)]
pub use value::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:类型擦除的基础

sqlx-core/src/any/connection/backend.rs:9 的核心 trait:

pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static {
    fn name(&self) -> &str;
    fn close(self: Box<Self>) -> BoxFuture<'static, crate::Result<()>>;
    #[doc(hidden)] 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 { /* 默认 unimplemented!() */ }
    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<()>> {
        Box::pin(async move { Ok(()) })
    }
    fn shrink_buffers(&mut self);
    #[doc(hidden)] fn flush(&mut self) -> BoxFuture<'_, crate::Result<()>>;
    #[doc(hidden)] fn should_flush(&self) -> bool;
    #[cfg(feature = "migrate")]
    fn as_migrate(&mut self) -> crate::Result<&mut (dyn crate::migrate::Migrate + Send + 'static)> { /* 默认报 Configuration 错 */ }

    // 执行相关
    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<'c, 'q: 'c>(&'c mut self, sql: &'q str, parameters: &[AnyTypeInfo])
        -> BoxFuture<'c, crate::Result<AnyStatement<'q>>>;
    fn describe<'q>(&'q mut self, sql: &'q str) -> BoxFuture<'q, crate::Result<Describe<Any>>>;
}

一共 20 个方法(其中 get_transaction_depth、is_in_transaction、cached_statements_size、clear_cached_statements、as_migrate 5 个带默认实现),覆盖 Connection、TransactionManager 和 Executor 的职责——异步方法一律返回 BoxFuture(fetch_many 返回 BoxStream),而不是 impl Future。

为什么 BoxFuture? 因为 trait object(dyn AnyConnectionBackend)不能有泛型方法或返回 impl Future(编译期不确定的 type)。BoxFuture 是 Pin<Box<dyn Future + Send>>——堆分配 + 类型擦除——让 trait object 可行。顺带一提,这并不是 Any 独有的开销:0.8.6 的 Connection 方法本来就返回 BoxFuture(sqlx-core/src/connection.rs:35-44),Executor 方法返回 BoxFuture 或 BoxStream(sqlx-core/src/executor.rs),具体驱动同样要装箱。

super trait std::any::Any——理论上允许把 trait object 向下转型回具体连接类型;但 0.8.6 全树没有对 backend 做 downcast 的代码,而且 AnyConnection::backend 字段是 pub(crate),用户代码也拿不到它。

#[doc(hidden)] 在一些方法上——表明 "不对用户暴露"、driver 实现方用。

19.3.1 每家驱动实现 AnyConnectionBackend

Postgres 的实现在 sqlx-postgres/src/any.rs(258 行):

impl AnyConnectionBackend for PgConnection {
    fn name(&self) -> &str { <Postgres as Database>::NAME }  // "PostgreSQL"

    fn close(self: Box<Self>) -> BoxFuture<'static, sqlx_core::Result<()>> {
        Connection::close(*self)  // 调具体 Postgres Connection::close
    }

    fn ping(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> {
        Connection::ping(self)
    }

    fn fetch_many<'q>(&'q mut self, query: &'q str, persistent: bool,
        arguments: Option<AnyArguments<'q>>) -> BoxStream<'q, sqlx_core::Result<Either<AnyQueryResult, AnyRow>>>
    {
        let persistent = persistent && arguments.is_some();
        // AnyArguments -> PgArguments
        let arguments = match arguments.as_ref().map(AnyArguments::convert_to).transpose() {
            Ok(arguments) => arguments,
            Err(error) => {
                return stream::once(future::ready(Err(sqlx_core::Error::Encode(error)))).boxed()
            }
        };

        Box::pin(
            self.run(query, arguments, persistent, None)
                .try_flatten_stream()
                .map(move |res: sqlx_core::Result<Either<PgQueryResult, PgRow>>| match res? {
                    Either::Left(result) => Ok(Either::Left(map_result(result))),
                    Either::Right(row) => Ok(Either::Right(AnyRow::try_from(&row)?)),
                }),
        )
    }
    // ... 其他方法
}

核心模式:把 AnyXxx 参数转成 PgXxx(arguments)、调具体 Connection 的方法、把 PgXxx 返回值转回 AnyXxx。类型转换层是 Any 驱动相对具体驱动的额外开销。

MySQL(sqlx-mysql/src/any.rs,227 行)和 SQLite(sqlx-sqlite/src/any.rs,237 行)各自也有 any.rs 实现 AnyConnectionBackend——结构类似、各自包装自己的 Connection。

19.4 AnyConnection 的委托模式

sqlx-core/src/any/connection/mod.rs:26-28:

pub struct AnyConnection {
    pub(crate) backend: Box<dyn AnyConnectionBackend>,
}

单字段——一个 Box<dyn AnyConnectionBackend> trait object。所有方法都委托给 backend。

例如 &mut AnyConnection 的 Executor 实现(sqlx-core/src/any/connection/executor.rs:11-28):

impl<'c> Executor<'c> for &'c mut AnyConnection {
    type Database = Any;

    fn fetch_many<'e, 'q: 'e, E>(
        self,
        mut query: E,
    ) -> BoxStream<'e, Result<Either<AnyQueryResult, AnyRow>, Error>>
    where
        'c: 'e,
        E: 'q + Execute<'q, Any>,
    {
        let arguments = match query.take_arguments().map_err(Error::Encode) {
            Ok(arguments) => arguments,
            Err(error) => return stream::once(future::ready(Err(error))).boxed(),
        };
        self.backend
            .fetch_many(query.sql(), query.persistent(), arguments)
    }
    // ...
}

Executor impl 本质是"拆包 Execute trait 的数据、传给 backend"——backend 做真正的工作。这层平坦代理让 AnyConnection 的代码极其简短——真正的复杂度都在 backend 的具体实现里。

19.5 AnyValueKind:三家类型系统的交集

sqlx-core/src/any/value.rs:11-21:

#[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 的 BOOL 列映射成 Bool——MySQL 的 TINYINT(1) 列不在映射表里、读出直接报错,SQLite 里 1 = 1 这类结果按 INTEGER 读成 BigInt、只能用整数类型取,声明为 BOOLEAN 的列则直接报错,均为实测或见 §19.5.1 的映射表)
  • 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 原生,MySQL / SQLite 没有原生 UUID 类型)
  • 日期时间各种变体(Postgres 有 TIMESTAMPTZ / INTERVAL 等很多;SQLite 声明为 DATETIME 的列在 Any 下同样报错,实测)
  • NUMERIC / DECIMAL(三家都不在映射表里)
  • Postgres 自定义 ENUM
  • 所有网络 / 几何类型(Postgres 独有)

这让 Any 只能用于"最基础的 CRUD"——用户代码碰到"JSONB 列"的场景立即没法用 Any。

19.5.1 类型转换策略

每家驱动的 any.rs 要把自己的具体类型信息转成 AnyTypeInfo(再据此得到 AnyValueKind)。例如 Postgres:

// sqlx-postgres/src/any.rs:191
impl<'a> TryFrom<&'a PgTypeInfo> for AnyTypeInfo {
    type Error = sqlx_core::Error;

    fn try_from(pg_type: &'a PgTypeInfo) -> Result<Self, Self::Error> {
        Ok(AnyTypeInfo {
            kind: match &pg_type.0 {
                PgType::Bool => AnyTypeInfoKind::Bool,
                PgType::Void => AnyTypeInfoKind::Null,
                PgType::Int2 => AnyTypeInfoKind::SmallInt,
                PgType::Int4 => AnyTypeInfoKind::Integer,
                PgType::Int8 => AnyTypeInfoKind::BigInt,
                PgType::Float4 => AnyTypeInfoKind::Real,
                PgType::Float8 => AnyTypeInfoKind::Double,
                PgType::Bytea => AnyTypeInfoKind::Blob,
                PgType::Text | PgType::Varchar => AnyTypeInfoKind::Text,
                PgType::DeclareWithName(UStr::Static("citext")) => AnyTypeInfoKind::Text,
                _ => {
                    return Err(sqlx_core::Error::AnyDriverError(
                        format!("Any driver does not support the Postgres type {pg_type:?}").into(),
                    ))
                }
            },
        })
    }
}

先把具体驱动的 TypeInfo 降级成 AnyTypeInfoKind、再由 AnyRow::map_from(§19.31)按这个 kind 把值 decode 成对应的 AnyValueKind。

复杂类型(JSONB / UUID / ARRAY)无法转成 AnyTypeInfo——上面 _ => 分支直接返回 Error::AnyDriverError,不退化成 Text。

SQLite 的转换更简单(类型少):

// sqlx-sqlite/src/any.rs:152
fn try_from(sqlite_type: &'a SqliteTypeInfo) -> Result<Self, Self::Error> {
    Ok(AnyTypeInfo {
        kind: match &sqlite_type.0 {
            DataType::Null => AnyTypeInfoKind::Null,
            DataType::Int4 => AnyTypeInfoKind::Integer,
            DataType::Integer => AnyTypeInfoKind::BigInt,
            DataType::Float => AnyTypeInfoKind::Double,
            DataType::Blob => AnyTypeInfoKind::Blob,
            DataType::Text => AnyTypeInfoKind::Text,
            _ => { /* Error::AnyDriverError */ }
        },
    })
}

SQLite 只区分 Int4(→ Integer)和 Integer(→ BigInt)、没有 SmallInt 一档;Real 也统一映到 Double。这也是为什么 AnyValueKind 要有 SmallInt / Integer / BigInt 三个变体——让 Postgres 能保留精度信息、SQLite 只用其中两档。

MySQL 的映射(sqlx-mysql/src/any.rs:159-186)认 SMALLINT / INT / BIGINT / FLOAT / DOUBLE、四种 BLOB 列类型和 CHAR / VARCHAR 一类;TINYINT、MEDIUMINT、DECIMAL、日期时间、JSON 都落进报错分支。还有一处坑:MySQL 在协议里把 TEXT 列报成 BLOB 一类的列类型,0.8.6 按列类型把它映射成 Blob,于是用 String 去取 TEXT 列会报类型不匹配(按源码判断,本机无 MySQL、未实跑);v0.9.0 的 #4255 改成按 str / [u8] 的兼容性判断,才把这类列归到 Text。

反方向(bind 参数)SQLite 走的是 map_arguments——直接映射、保留生命周期,源码注释(:208,写作 convert_into(),指的就是 convert_to())明确说了这是它的替代:

// sqlx-sqlite/src/any.rs:216
AnyValueKind::Null(_)     => SqliteArgumentValue::Null,
AnyValueKind::Bool(b)     => SqliteArgumentValue::Int(b as i32),
AnyValueKind::SmallInt(i) => SqliteArgumentValue::Int(i as i32),
AnyValueKind::Integer(i)  => SqliteArgumentValue::Int(i),
AnyValueKind::BigInt(i)   => SqliteArgumentValue::Int64(i),
AnyValueKind::Real(r)     => SqliteArgumentValue::Double(r as f64),
AnyValueKind::Double(d)   => SqliteArgumentValue::Double(d),
AnyValueKind::Text(t)     => SqliteArgumentValue::Text(t),
AnyValueKind::Blob(b)     => SqliteArgumentValue::Blob(b),
// AnyValueKind 是 #[non_exhaustive],末尾还有一个 _ => unreachable!("BUG: missing mapping ...")

19.6 AnyArguments 的实现

sqlx-core/src/any/arguments.rs 的核心:

pub struct AnyArguments<'q> {
    #[doc(hidden)]
    pub values: AnyArgumentBuffer<'q>,
}

pub struct AnyArgumentBuffer<'q>(#[doc(hidden)] pub Vec<AnyValueKind<'q>>);

和 PgArguments / MySqlArguments 不同——AnyArguments 不 encode 字节——它是一个 Vec<AnyValueKind>——每参数一个 enum 变体。

Encode<'q, Any> for i32 的实现(简化):

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::convert_to::<PgArguments>()(sqlx-core/src/any/arguments.rs:45)逐个 out.add(...) 转成 PgArguments/MySqlArguments 时发生(SQLite 走 map_arguments 直接映射)。

这种"延迟到 backend 层 encode"模式让 AnyArguments 的实现极其简单——但 encode 工作只是推迟到具体驱动层、并没有省掉,还多了一份中间表示和一次转换。

19.6.1 Option<T> 的特殊处理

sqlx-core/src/any/mod.rs:68-83:

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<crate::encode::IsNull, crate::error::BoxDynError> {
        if let Some(value) = self {
            value.encode_by_ref(buf)
        } else {
            buf.0.push(AnyValueKind::Null(T::type_info().kind));
            Ok(crate::encode::IsNull::Yes)
        }
    }
}

Null 带类型信息——AnyValueKind::Null(AnyTypeInfoKind)——因为不同 DB 对 NULL 参数期望不同类型信息(Postgres 要 OID)。T::type_info().kind 在 encode 时确定——让 backend 能重新构造合适的 NULL。

19.7 install_drivers + OnceCell 的全局注册

sqlx-core/src/any/driver.rs:12、:125-131:

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 报错。典型用法:

fn main() {
    sqlx::any::install_default_drivers();  // 内部调 install_drivers
    // 或者(二选一)手动挑驱动;两者都调的话,后一次会失败:
    // sqlx::any::install_drivers(&[sqlx::postgres::any::DRIVER, sqlx::mysql::any::DRIVER]).unwrap();
    // ... app 主体
}

install_default_drivers 注册所有 feature 启用的 backend——最常见用法。它内部用 std::sync::Once 包了一层(sqlx-0.8.6/src/any/mod.rs:35-49),重复调用无害;但如果之前已经手动 install_drivers 过,它会 panic(non-default drivers already installed,实测)。用户一开始就调、之后的 AnyConnection::connect 能正常分发。

不调 install_drivers 直接 connect 会 panic——sqlx-core/src/any/driver.rs:140-142 的 expect:

let drivers: &[AnyDriver] = DRIVERS.get().expect("No drivers installed. ...");

这个 panic 是刻意的——告诉用户"你忘了 install"——比 silent fail 好。

19.8 URL scheme 分发

sqlx-core/src/any/driver.rs:137-150 的 from_url:

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()
        ))
}

三步:

  1. 从 URL 提取 scheme(postgres:// 的 scheme 是 "postgres")。
  2. 从 OnceCell 取已注册的 driver 列表。
  3. 扫描找第一个 url_schemes 包含当前 scheme 的 driver。

AnyDriver::without_migrate::<DB>() 创建时把 DB::URL_SCHEMES(第 3 章 §3.8)拷进 url_schemes——所以 sqlx_postgres::any::DRIVER.url_schemes = &["postgres", "postgresql"](sqlx-postgres/src/database.rs:37)、sqlx_mysql::any::DRIVER.url_schemes = &["mysql", "mariadb"](sqlx-mysql/src/database.rs:35)、sqlx_sqlite::any::DRIVER.url_schemes = &["sqlite"](sqlx-sqlite/src/database.rs:36)。

找到 driver 后,调其 connect 函数指针(AnyDriver::connect 字段)—— monomorphize 成具体驱动的 connect_with_db::<Postgres> / _::<MySql> / _::<Sqlite>——建立底层 Connection、包成 Box<dyn AnyConnectionBackend>。

19.9 AnyDriver 的 const fn 构造

sqlx-core/src/any/driver.rs:36-49:

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::without_migrate::<DB>() 能在 const context 使用(各驱动 crate 用它声明 pub const DRIVER: AnyDriver,见 §19.24)。

connect: DebugFn(AnyConnection::connect_with_db::<DB>) ——泛型函数指针。DB 在构造点 monomorphize——每家驱动一个实例:

// sqlx-postgres/src/any.rs:24 / sqlx-mysql/src/any.rs:22 展开后:
pub const DRIVER: AnyDriver = AnyDriver::with_migrate::<Postgres>();
pub const DRIVER: AnyDriver = AnyDriver::with_migrate::<MySql>();

这是编译期的 monomorphization —— 具体驱动的 connect 函数在编译后就存在;运行时 AnyDriver.connect 调用走一次间接函数指针调用。

这条设计让 Any 驱动在 connect 阶段的分派本身几乎不花什么——额外开销集中在连上之后:每次调用经 dyn AnyConnectionBackend 的 vtable 转发,外加参数和结果行的类型转换。

19.10 Any 的代价

使用 Any 驱动有四条明确代价:

1. 动态分发 + 类型转换

BoxFuture 本身不算 Any 独有的代价——0.8.6 里具体驱动的 Connection / Executor 方法同样返回 BoxFuture / BoxStream。Any 多出来的是:每次调用经 dyn AnyConnectionBackend 的 vtable 转发一次;绑定参数先进 AnyArguments、再由 backend 转成具体驱动的 Arguments;结果行则由 AnyRow::map_from(§19.31)把每一列都解码成拥有所有权的值(字符串、字节都会复制一份)。

在热路径(比如每秒数千 query)这条代价可见——但相对 DB round-trip 仍微小。

2. 类型信息压平

AnyValueKind 的 9 个 variant 只能表达三家 DB 的基础类型。Postgres 的 TEXT、VARCHAR、citext 在 Any 下都成了 Text,原始类型身份丢失;映射表之外的类型(§19.5.1)直接报错。

用 Any 时只能用这 9 种类型——复杂业务类型(JSONB、Uuid、数组)直接不可用。

3. 功能子集

sqlx 为 Postgres 提供的 PgListener(LISTEN/NOTIFY)、COPY、PgAdvisoryLock 等专用 API 都绑定在 PgConnection / PgPool 这些具体类型上——Any 下都用不了。AnyQueryResult 虽然有 last_insert_id 字段,但只有 MySQL 会填(sqlx-mysql/src/any.rs:225),Postgres 和 SQLite 一律是 None(sqlx-sqlite/src/any.rs:232-237,SQLite 实测)。

用 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 用 AnyConnection(不是 pool)+ 运行时解析 DATABASE_URL(sqlx-cli/src/lib.rs:137-138、:153):

async fn connect(opts: &ConnectOpts) -> anyhow::Result<AnyConnection> {
    retry_connect_errors(opts, AnyConnection::connect).await
}
// retry_connect_errors 内部第一件事:
sqlx::any::install_default_drivers();

Migration SQL 基本用 DDL、很少涉及复杂类型——Any 的类型限制不构成问题。sqlx-cli 的 migrate 和 database 子命令都走 Any(database 走 Any: MigrateDatabase,见 sqlx-cli/src/database.rs);prepare 子命令只在设置了数据库 URL 时经上面的 connect 入口连一下、确认库可达就关掉(sqlx-cli/src/prepare.rs:362-365 的 check_backend),真正的查询描述是 shell 出 cargo 子进程、由 query! 宏各自连库完成。

19.11.2 跨 DB 工具

比如schema 导出工具——读 information_schema、输出通用格式:

let pool = AnyPool::connect(&url).await?;
// Postgres 的 table_name 是 sql_identifier 类型,不在 Any 的映射表里,要先转成 text
let tables: Vec<AnyRow> = sqlx::query("SELECT table_name::text AS table_name FROM information_schema.tables")
    .fetch_all(&pool).await?;

information_schema 只有 Postgres 和 MySQL 有(SQLite 没有,要查 sqlite_master),两家的列类型也不一样——Postgres 这里的 ::text 转换在 MySQL 上就不是合法语法。Any 省掉的是"连接和取值用哪套类型",SQL 本身仍要按 DB 分别写(§19.18 的例子就是这么做的)。

19.11.3 测试用 SQLite / 生产用 Postgres

§18.23 的模式——业务代码用 AnyPool、测试注入 sqlite::memory: pool、生产注入 postgres://... pool。测试那边的 pool 要用 AnyPoolOptions::new().max_connections(1) 建:Any 驱动每开一条连接都重新解析一次 URL,sqlite::memory: 的每条连接各得一个全新的内存库,默认多连接时在一条连接上建的表,换一条连接就看不到(实测 6 次全部报 no such table,限成 1 条连接后正常):

async fn get_user(pool: &AnyPool, id: i32) -> Result<Option<User>, Error> {
    // Any 层**不会**替你改写占位符:SQL 字符串原样交给底层驱动。
    // `$1` 在 Postgres 和 SQLite 上都合法(SQLite 把 `$AAA` 当具名参数),
    // 但 MySQL 只认 `?`——所以"跨 DB"之前先想清楚要跨的是哪两家。
    sqlx::query_as("SELECT id, name FROM users WHERE id = $1")
        .bind(id).fetch_optional(pool).await
}

这条模式要求 SQL 是 cross-compatible 子集——用户要自己把控语法兼容。顺带一提,AnyArguments 并没有覆盖 format_placeholder(sqlx-core/src/any/arguments.rs:13-30 的 impl Arguments<'q> for AnyArguments<'q> 里只有 reserve / add / len),所以走 QueryBuilder 拼 SQL 时 Any 会一律吐 ?——在 Postgres 上直接语法错。动态拼 SQL 这条路和 Any 是不搭的。

19.11.4 运行时选 DB 的小应用

比如一个 SQL REPL 工具——用户输入 URL、工具连、用户输入 SQL、工具执行:

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 让这种工具可能。要注意的是,用户如果输入 sqlite::memory:,pool 里的每条连接各是一个独立的内存库,上一句 CREATE TABLE 和下一句 SELECT 可能落在不同连接上;这类工具更适合直接持有一条 AnyConnection,或者用 AnyPoolOptions::new().max_connections(1) 建 pool。

19.12 编译期多态 vs 运行时多态的决策

用一张表总结 Any 和具体驱动的权衡:

维度 具体驱动(Postgres 等) Any 驱动
分发时机 编译期(泛型 DB) 运行时(trait object)
类型信息 精确(PgTypeInfo / OID) 9 种枚举 variant
性能开销 无额外转换层 多一层 dyn 调用、参数与结果行的类型转换
功能完整度 完整(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 + 函数指针 = 编译期生成的驱动目录——AnyDriver::with_migrate::<DB>() monomorphize 出具体 connect 函数,运行时经函数指针间接调用一次。

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 驱动 1800 行代码就能支撑跨 DB 通用的能力——巧妙的工程。

19.14 阶段小结

本章讲完了 sqlx 的 Any 驱动:

  1. 运行时多态的场景(§19.1)—— sqlx-cli、跨 DB 工具、测试 / 生产不同 DB、插件应用。
  2. Any 的 17 个公开 re-export(§19.2)—— Any Database trait 家族完整实现、委托给 backend。
  3. AnyConnectionBackend trait(§19.3)—— 20 个方法 + BoxFuture 返回让 trait 可以做成 trait object。
  4. AnyConnection 委托模式(§19.4)—— 单字段 Box<dyn Backend>、方法都转发。
  5. AnyValueKind 9 变体(§19.5)—— 三家类型系统交集、复杂类型不可用。
  6. AnyArguments 延迟 encode(§19.6)—— Vec<AnyValueKind>、不 encode 字节、backend 层转换。
  7. install_drivers + OnceCell(§19.7)—— 进程级单次注册。
  8. URL scheme 分发(§19.8)—— drivers.iter().find 按 scheme 找。
  9. const fn AnyDriver::with_migrate / without_migrate(§19.9)—— 编译期 monomorphize、运行时间接调用。
  10. 四条代价(§19.10)—— 动态分发与类型转换、类型信息压平、功能子集、query! 不可用。
  11. 四种合理场景(§19.11)—— CLI、跨 DB 工具、测试/生产分离、运行时选 DB。
  12. 决策表(§19.12)—— 业务用具体驱动、工具用 Any。

第五部分"驱动实现"到此正式结束——Postgres / MySQL / SQLite / Any 四章讲完。接下来是第六部分"工具与工程"(第 20 章迁移、第 21 章日志追踪)和第七部分"生产落地"(第 22 章生产实战)——本专栏最后三章。

19.15 Any 驱动的调用路径可视化

用 mermaid 画 Any 驱动的完整分发路径:

flowchart TD
    U["用户: AnyPool::connect(url)"]
    URL["URL::parse → scheme"]
    CELL["DRIVERS OnceCell"]
    FIND["drivers.iter.find scheme 匹配"]
    AD["AnyDriver"]
    CW["connect_with_db::&lt;Postgres&gt;"]
    BOX["Box dyn AnyConnectionBackend"]
    AC["AnyConnection"]
    U --> URL --> CELL --> FIND --> AD --> CW --> BOX --> AC
    U2["AnyConnection.fetch_many"]
    BACKEND["backend.fetch_many"]
    PG["具体 PgConnection.fetch_many"]
    CONV["result: PgRow → AnyRow 转换"]
    AC --> U2 --> BACKEND --> PG --> CONV --> U2

两段路径:

  • 连接建立: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/ 目录结构(1808 行,wc -l 实测):

any/
├── arguments.rs       AnyArguments + AnyArgumentBuffer + convert_to (91 行)
├── column.rs          AnyColumn (31 行)
├── connection/
│   ├── mod.rs         AnyConnection struct + Connection impl (125 行)
│   ├── backend.rs     AnyConnectionBackend trait (118 行)
│   ├── executor.rs    Executor impl (66 行)
├── database.rs        Any struct + Database impl (39 行)
├── driver.rs          AnyDriver + install_drivers (150 行)
├── error.rs           mismatched_types 辅助函数 (16 行)
├── kind.rs            AnyKind(已废弃,74 行)
├── migrate.rs         MigrateDatabase / Migrate 转发(82 行)
├── mod.rs             顶层 re-export + Option impl (83 行)
├── options.rs         AnyConnectOptions (65 行)
├── query_result.rs    AnyQueryResult (28 行)
├── row.rs             AnyRow + map_from (144 行)
├── statement.rs       AnyStatement (100 行)
├── transaction.rs     AnyTransactionManager (36 行)
├── type_info.rs       AnyTypeInfo + AnyTypeInfoKind (64 行)
├── types/             基础类型的 Encode/Decode(371 行)
└── value.rs           AnyValueKind + AnyValue + AnyValueRef (125 行)

总计 1808 行——是 sqlx-postgres(19841 行)的 9%。因为 Any 不实现协议——只做类型擦除 + 方法委托。

这条比例让"加新 DB 驱动到 Any 支持" 成本可控——该驱动实现 AnyConnectionBackend 和几组 TryFrom 转换(TypeInfo / Column / Row / ConnectOptions)、用宏声明 DRIVER 常量并注册进 install_drivers,Any 层本身不用改(Postgres 258 行 / MySQL 227 行 / SQLite 237 行的 adapter)。

19.17 Any 驱动的版本演进

按 CHANGELOG 可核实的几个节点:

  • 0.4.0-beta.1(2020-07-27):首次引入 "Runtime-determined database driver, Any, to support compile-once and run with a database"。
  • 0.7.0(2023-06-30,[#2039]):驱动拆成独立 crate;此前(0.6.x)AnyConnection 包的是按 feature 条件编译的 AnyConnectionKind 枚举(Postgres / MySql / Sqlite / Mssql 各一个变体),这一版改成 Box<dyn AnyConnectionBackend>,AnyDriver / AnyConnection 也改为运行时注册——必须先调 install_drivers() 或 install_default_drivers(),"the set of possible drivers is now determined at runtime";同时删掉 AnyEncode trait。
  • 0.8.0(2024-07-22,[#2973]):GAT 替换 HasValueRef / HasArguments / HasStatement,Any 也随之改用 GAT 形式的关联类型;[#3172] 修 Any 驱动的 Option 解码,[#3291] 给 Any 加上 Postgres Bool 类型支持。
  • 0.8.4(2025-04-13,[#3765],合并了 #3427):AnyConnectionBackend::get_transaction_depth 以"默认 unimplemented!()"的方式加入,注释里写明 0.9 会变成 required method(实际 v0.9.0 仍未改,见 §19.20)。

演进路径显示 Any 是 sqlx 团队持续维护的一块——sqlx-cli 深度依赖它、不能放弃。

19.18 Any 驱动的实战案例

最后给一个真实可用的 Any 驱动案例——一个通用 DB 探索工具:

// `AnyPool` 在 crate 根,`AnyRow` 只在 `sqlx::any` 下——两处 re-export 的位置不一样
use sqlx::{AnyPool, Row};
use sqlx::any::AnyRow;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 注册所有驱动
    sqlx::any::install_default_drivers();

    // 从命令行参数读 URL
    let url: String = std::env::args().nth(1).expect("usage: tool <url>");
    let pool = AnyPool::connect(&url).await?;

    // Any 不翻译 SQL:按实际连上的后端选元数据查询,并统一起别名 table_name
    let backend = pool.acquire().await?.backend_name().to_owned();
    let sql = match backend.as_str() {
        "PostgreSQL" => "SELECT table_name::text AS table_name FROM information_schema.tables \
                         WHERE table_schema = 'public'",
        "MySQL" => "SELECT table_name AS table_name FROM information_schema.tables \
                    WHERE table_schema = DATABASE()",
        "SQLite" => "SELECT name AS table_name FROM sqlite_master WHERE type = 'table'",
        other => return Err(format!("unsupported backend: {other}").into()),
    };
    let tables: Vec<AnyRow> = sqlx::query(sql).fetch_all(&pool).await?;

    println!("Tables ({backend}):");
    for row in tables {
        let name: String = row.try_get("table_name")?;
        println!("  - {}", name);
    }

    Ok(())
}

这段代码一份二进制处理三家 DB:

  • mysql://... 走 sqlx-mysql 的 backend。
  • postgres://... 走 sqlx-postgres。
  • sqlite://app.db 走 sqlx-sqlite(SQLite 分支用文件库实跑过;Postgres / MySQL 分支按源码判断,未实跑)。

跨 DB 细节的处理:

  • 元数据查询本身就不通用——SQLite 没有 information_schema;Postgres 的 table_name 是 sql_identifier 类型、不在 Any 的映射表里,得先 ::text;MySQL 返回的列名是大写 TABLE_NAME——所以三个分支各写一条 SQL,并统一用别名 table_name。
  • 占位符不能混用也不会被改写——Arguments::format_placeholder 只在 QueryBuilder::push_bind 里被调到(第 10 章 §10.10),而 AnyArguments 根本没覆盖它(默认吐 ?);像上面这样直接写 SQL 字面量时,字符串原样发给底层驱动。要跨 Postgres 和 MySQL 传参,只能按 DB 分别准备 SQL。

这种工具的代表就是 sqlx 自己的 sqlx-cli(§19.29)——一个二进制、靠 URL scheme 分派到三家 DB。

19.19 Any 驱动常见用法陷阱

生产代码里用 Any 的几个常见坑:

陷阱 1:忘了 install_drivers

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = AnyPool::connect("postgres://...").await?;  // panic!
    Ok(())
}

错误信息:No drivers installed. Please see the documentation in `sqlx::any` for details.(实测)。启动代码第一行必须调 sqlx::any::install_default_drivers() 或手动 install_drivers。

陷阱 2:写了 DB 特有语法期望跨 DB 运行

// 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 独有类型

sqlx::query("INSERT INTO events (data) VALUES ($1)")
    .bind(serde_json::json!({...}))  // Any 不支持直接 bind JSON
    .execute(&any_pool).await?;

serde_json::Value 没有实现 Encode<Any>,这段代码直接编译不过。AnyValueKind 没有 JSON variant——只能 bind Text:先 serde_json::to_string 再 bind。Postgres 上还得在 SQL 里写 CAST($1 AS JSONB):参数会按 TEXT 类型发过去,而 text 到 jsonb 没有隐式转换(按 PostgreSQL 类型转换规则判断,未实跑)。

陷阱 4:期望 Any 处理 nullability 推断

Any 的 describe 其实会转发给底层驱动、把 nullable 原样带回(sqlx-postgres/src/any.rs:151-188;SQLite 实测能拿到);但用得上这份信息的 query! 宏不支持 Any(见陷阱 5)。运行时 query_as 下,nullability 全靠 FromRow 结构体里的 Option<T>——列里有 NULL、字段却不是 Option 时,解码会报 expected TEXT, got Null(Text) 一类的错(实测)。

陷阱 5:Any 和 query! 混用

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.20 Any 的未来展望

源码里能直接读到的、已经写明的未来变更只有一条——AnyConnectionBackend::get_transaction_depth 的默认实现注释(sqlx-core/src/any/connection/backend.rs:51):

This is a provided method to avoid a breaking change, but it will become a required method in version 0.9 and later.

注释说 0.9 会把 get_transaction_depth 变成 required method;但对照 sqlx 仓库的 v0.9.0 tag(2026-05-21),它仍是带 unimplemented!() 默认实现的 provided method,这句注释也原样留着——注释里的计划并没有在 0.9.0 兑现。kind.rs 顶部注释(:4)留了另一句:

AnyKind may return at some point but it won't be a simple enum.

除此之外的方向(比如加 Json / Uuid variant)源码里没有承诺——属于社区讨论、不要当成既定路线。核心机制到 v0.9.0 也没有变(仍是 Box<dyn> + 全局注册表 + URL 分发,只是 OnceCell 换成了标准库的 OnceLock)——读懂本章的你能轻松跟上演进。

Any 作为 sqlx 架构的"运行时多态层"会继续存在——它的独特价值(sqlx-cli 的基础)无法被替代。哪怕 Rust 的 async trait 能力提升(dyn AsyncTrait 的新特性)让 Any 实现更简单——抽象角色不会消失。

19.21 Any 性能开销的定性分析

Any 相对具体驱动的额外开销来源(定性描述、具体数字请以你自己的 cargo bench 为准):

  • pool.acquire().await—— 这一步 Any 其实不额外收费:Pool::<DB>::acquire 是同一份泛型代码(sqlx-core/src/pool/mod.rs:357),对具体驱动和 Any 一样返回 impl Future。真正的 BoxFuture 在 Executor trait 上,而那是所有驱动共担的(见下一条)。
  • query.execute() / fetch_one()—— 执行路径上多一次经 dyn AnyConnectionBackend 的转发,后续 I/O 都是 DB RTT 主导。Any 的额外开销相对 query 总时间可忽略。
  • bind 参数—— 具体驱动把值直接编码到 driver-specific buffer;Any 先推进 AnyValueKind 枚举(字符串、字节用 Cow,可以借用)、再由 backend 转换。每个 bind 多一个 enum push 的小开销。
  • decode 解码—— 具体驱动直接按 Decode<DB> 展开;Any 在收到每一行时就由 AnyRow::map_from 把所有列解码成拥有所有权的 AnyValueKind(TEXT / BLOB 各复制一份),取值时再 match 一次。多一次整行转换和复制。

总体判断:

  • 稳态 query 的 Any 开销相对 DB RTT 可忽略——毫秒级 RTT 淹没微秒级 overhead。
  • 只有极高 QPS 的纯内存路径才可能看到 Any 开销累积——生产 DB 场景几乎遇不到。
  • Any 的代价主要是功能子集和类型精度(不能用 UUID/JSON 等类型、不能用 query!())——不是速度。

想知道自己的场景准确数字——cargo bench 跑一次对照,比读任何估算数据都可靠。

19.22 Any 驱动源码中的精彩细节

收尾前再提几条sqlx-core/src/any/ 源码里的亮点:

1. impl Encode<'q, Any> for Option<T>(sqlx-core/src/any/mod.rs:68-83)——特殊处理 NULL 的类型保留:

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(sqlx-core/src/any/value.rs:10):

#[non_exhaustive]
pub enum AnyValueKind<'a> { ... }

告诉用户代码不能 exhaustive match——sqlx 未来加新 variant 不破坏兼容。这条向前兼容考虑在很多 trait 和 enum 上都有——sqlx 团队的长期维护意识。

3. AnyConnectionBackend::get_transaction_depth 默认 panic(sqlx-core/src/any/connection/backend.rs:50):

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.4 加这个方法时给了默认实现(panic),避免破坏已有的 backend 实现;注释计划 0.9 改成 required method,不过 v0.9.0 并没有这么做(§19.20)。新功能渐进加入。

4. #[allow(deprecated)] pub use kind::AnyKind(sqlx-core/src/any/mod.rs)—— 保留 deprecated 的 AnyKind export 避免用户代码 break——老 API 的graceful deprecation。

5. // NOTE: required due to the lack of lazy normalization(sqlx-core/src/any/mod.rs:58)——impl_into_arguments_for_arguments!(AnyArguments<'q>); 的原因注释——解释 why、未来 Rust 改进了可以清理。

这五条细节不影响 Any 的核心功能——但体现代码持续维护的工艺。读这些细节让你学到"怎么长期维护一个库而不破坏用户代码"。

19.23 AnyDriver 结构体:几乎零成本的 backend 目录

Any 驱动的核心数据结构是 AnyDriver(sqlx-core/src/any/driver.rs:27-34):

#[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(["postgres", "postgresql"])——用来匹配 URL。
  • connect——函数指针包在 DebugFn 里—— 创建具体 backend 的 connect 逻辑——这一个字段就是多态派发点。
  • migrate_database——可选的 migrate-trait 适配器——只有启用 migrate feature 且底层 DB 实现了 MigrateDatabase 才 Some。

几乎零成本的驱动目录——所有字段都是 &'static、函数指针或 Option<AnyMigrateDatabase>(同样是四个函数指针)——编译期决定、运行时只读——不需要堆分配或运行时构造。

DebugFn 是一个小包装(sqlx-core/src/common/mod.rs:8)—— 给函数包一层、Debug 输出固定为 Function,并实现 Deref 让它能像原函数一样直接调用((driver.connect)(options))。AnyDriver 自己的 Debug 是手写的(sqlx-core/src/any/driver.rs:85-92),只打印 name 和 url_schemes。

19.24 declare_driver_with_optional_migrate 宏

源码(sqlx-core/src/any/driver.rs:14-25):

#[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 常量:

// 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.25 OnceCell 全局注册与 URL 分派

静态 DRIVERS(sqlx-core/src/any/driver.rs:12):

static DRIVERS: OnceCell<&'static [AnyDriver]> = OnceCell::new();

语义:

  • 进程生命周期内只能 set 一次——多次 set 报错。
  • set 后不可变——所有后续读都拿到同一份。
  • OnceCell::get() 是无锁的快路径——不过它只在建连接(AnyConnection::connect)和 Any 的 MigrateDatabase 方法里经 from_url 读取,普通 query 不碰它。

用户 API:

sqlx::any::install_default_drivers();
// 或者(二选一)手动:
// sqlx::any::install_drivers(&[sqlx::postgres::any::DRIVER, sqlx::mysql::any::DRIVER]).unwrap();

URL → Driver 分派逻辑——查 DRIVERS 中每个 driver 的 url_schemes、找第一个包含该 scheme 的(整串相等,不是前缀匹配):

// sqlx-core/src/any/driver.rs:137
pub(crate) fn from_url(url: &Url) -> crate::Result<&'static AnyDriver> {
    let scheme = url.scheme();

    let drivers: &[AnyDriver] = DRIVERS
        .get()
        .expect("No drivers installed. Please see the documentation in `sqlx::any` for details.");

    drivers
        .iter()
        .find(|driver| driver.url_schemes.contains(&url.scheme()))
        .ok_or_else(|| {
            Error::Configuration(format!("no driver found for URL scheme {scheme:?}").into())
        })
}

注意 DRIVERS.get() 用的是 expect 而非 ?——没装驱动直接 panic(§19.7)、不是返回 Err。

这个线性扫描 看起来低效——实际 install_default_drivers() 最多注册 3 个 driver(Postgres / MySQL / SQLite)—— n 是常数、无需 HashMap。

与 AnyKind 的关系——早期 sqlx 有一个 AnyKind 枚举(sqlx-core/src/any/kind.rs:12)显式列举四种 DB——现在标记为 #[deprecated]、不再使用——被这套 AnyDriver + url_schemes 分派取代——"枚举转切片"是 sqlx 架构演进的一步关键重构——让新 DB 加入不需要改 sqlx-core 的枚举。

19.26 AnyMigrateDatabase:Option 背后的 feature 分支

AnyDriver 里的 migrate_database: Option<AnyMigrateDatabase> 字段(sqlx-core/src/any/driver.rs:33)——Option 不是冗余、而是feature 差分的核心设计。

源码两个 with_migrate impl(:51-77):

#[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 的真正含义——"该 driver 支不支持 migration" 的运行时可见证据—— AnyDriver::get_migrate_database()(sqlx-core/src/any/driver.rs:79)碰到 None 时返回 Error::Configuration("... does not support migrations or the `migrate` feature was not enabled for it")——而不是编译期直接拒绝编译——让上层代码可以跨 feature 写一份。

这是 Rust 条件编译的高级用法—— "同一个 API 表面在有/无 feature 时行为不同但类型相同"—— 相比传统的 #[cfg] 分裂函数名—— 这种 Option 承载方式让用户代码简单。

19.27 AnyConnectOptions:极简的两字段

具体驱动的 PgConnectOptions 有十几个字段—— Any 的 AnyConnectOptions 只有两个(sqlx-core/src/any/options.rs:17-22):

#[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 不在构造 AnyConnectOptions 时解析—— from_str 和 from_url 只做 URL 语法校验、不解析 scheme 特定字段(比如 Postgres 的 sslmode / statement-cache-capacity、MySQL 的 charset / socket)—— 这些延迟到 AnyConnection::connect 里、根据 URL scheme 选具体 driver、再由该 driver 的 TryFrom<&AnyConnectOptions> 实现做完整解析(Postgres 调 parse_from_url,sqlx-postgres/src/any.rs:243-251)。

延迟解析的好处—— Any 驱动层不需要知道 backend-specific 参数—— 加一个新的 backend 字段(比如未来 Postgres 加个 prepare_threshold 参数)不改 Any 代码—— 开闭原则的完美体现。

from_url 只 clone(sqlx-core/src/any/options.rs:39-44,ConnectOptions impl 内):

fn from_url(url: &Url) -> Result<Self, Error> {
    Ok(AnyConnectOptions {
        database_url: url.clone(),
        log_settings: LogSettings::default(),
    })
}

6 行代码—— 对比 Postgres parse_from_url 的 100 来行—— Any 层薄得让人感动。

19.28 connect_with_db:泛型入口函数

AnyConnection::connect_with_db<DB>(sqlx-core/src/any/connection/mod.rs:43 起)—— 把具体 DB 的 Connection 包装成 AnyConnection:

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. options.connect().await(ConnectOptions::connect)
    // 3. 包装成 Box<dyn AnyConnectionBackend>
}

三步:

  1. TryFrom 转换—— AnyConnectOptions → 具体 ConnectOptions—— 具体 driver 实现这个 TryFrom、读 URL query string、填好所有字段。
  2. Connect—— 调 options.connect()(ConnectOptions::connect)建连接—— 此时具体 driver 已全部生效。
  3. 包装—— Box<dyn AnyConnectionBackend>—— 类型擦除完成。

where 子句里的 for<'a> TryFrom<&'a AnyConnectOptions, Error = Error> 是关键—— HRTB(Higher-Rank Trait Bound)—— 意思是"对任何 lifetime 'a 都能 TryFrom"——通用、无生命周期限制。

这个函数是 AnyDriver 结构体 connect 字段的函数指针值(见 §19.9 DebugFn(AnyConnection::connect_with_db::<DB>))—— 编译期 monomorphize 为每家 DB 一份——运行时通过函数指针表分派。

"编译期 monomorphize + 函数指针表" 只管"选哪家驱动、怎么建连接"这一步;连接建好之后,后续每次调用仍经 Box<dyn AnyConnectionBackend> 的 vtable 转发——两处都是一次间接调用,只是分派的时机不同。

19.29 Any 驱动在 sqlx-cli 的关键作用

sqlx-cli(sqlx-cli/src/)是 sqlx 官方的命令行工具—— 提供 migrate add/run/info、database create/drop、prepare 等命令—— 其中需要连库的都经 Any 驱动(migrate add 只写文件、不连库)。

设计动机:

  • 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/lib.rs:137-138 + :153、sqlx-cli/src/migrate.rs:281-288):

// lib.rs:所有子命令共用的连接入口
async fn connect(opts: &ConnectOpts) -> anyhow::Result<AnyConnection> {
    retry_connect_errors(opts, AnyConnection::connect).await
}

async fn retry_connect_errors<...>(opts: &ConnectOpts, mut connect: F) -> anyhow::Result<T> {
    sqlx::any::install_default_drivers();   // 第一件事
    let db_url = opts.required_db_url()?;
    backoff::future::retry(/* ExponentialBackoff */, || connect(db_url).map_err(...)).await
}

// migrate.rs 的 run:读迁移目录、再经 Any 连库
let migrator = Migrator::new(Path::new(migration_source)).await?;
// ...
let mut conn = crate::connect(connect_opts).await?;

三步就搞定—— install_default_drivers + AnyConnection::connect + Migrator—— CLI 通用化的魔法。注意 sqlx-cli 用的是单条 AnyConnection、不是 AnyPool。

Any 驱动的这个应用场景 让它无法被替代—— 即使某一天 dyn AsyncTrait 简化了运行时多态的实现—— sqlx-cli 的需求本身就是 Any 驱动的理由。

所以本章一再强调 "业务代码别用 Any、工具代码才用 Any"—— Any 不是劝退、而是定位—— 有人确实需要它。sqlx-cli 就是它最典型的使用者。

19.30 AnyValueKind 的编码 / 解码细节

AnyValueKind 枚举是 Any 驱动类型系统的核心交集(sqlx-core/src/any/value.rs:11-21):

#[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 和 SQLite 都没有(常见做法是存 BINARY(16) / BLOB 或 TEXT)—— 没有三家统一的表达—— Any 放弃。
  • Json—— Postgres jsonb/json、MySQL JSON、SQLite TEXT(无原生)—— 同上。
  • Timestamp—— 三家语义差异太大(Postgres timestamptz、MySQL DATETIME、SQLite 存 TEXT/INTEGER)—— Any 干脆不支持:这类列在 Any 下直接报错(SQLite 的 DATETIME 列实测如此),想读得在 SQL 里先转成文本。

用户绕过办法——把 UUID/JSON 序列化成 String、bind(string)、走 Text variant—— 在应用层维持类型语义。Postgres 上还要在 SQL 里显式 CAST($1 AS UUID) / CAST($1 AS JSONB),读的时候反过来转成 text(见 §19.19 陷阱 3)。

Cow<'a, str> / Cow<'a, [u8]>—— 允许借用或拥有—— 绑定 &str 时 Text 直接借用(零拷贝),绑定 String 时则 clone 一份存成 Owned(sqlx-core/src/any/types/str.rs:55-62)。

encode 路径——AnyArguments::convert_to(sqlx-core/src/any/arguments.rs:45-90)把 AnyValueKind match 分派到目标 Arguments 的 add:

for arg in &self.values.0 {
    match arg {
        AnyValueKind::Null(AnyTypeInfoKind::Null) => out.add(Option::<i32>::None),
        AnyValueKind::Null(AnyTypeInfoKind::Bool) => out.add(Option::<bool>::None),
        // ... 其余 Null(kind) 分支
        AnyValueKind::Bool(b) => out.add(b),
        AnyValueKind::SmallInt(i) => out.add(i),
        AnyValueKind::Integer(i) => out.add(i),
        AnyValueKind::BigInt(i) => out.add(i),
        AnyValueKind::Real(r) => out.add(r),
        AnyValueKind::Double(d) => out.add(d),
        AnyValueKind::Text(t) => out.add(&**t),
        AnyValueKind::Blob(b) => out.add(&**b),
    }?
}

每条分支落到具体驱动的 Encode<DB>—— 真正的 wire 编码在那里发生。省略掉的 Null 分支里有个小瑕疵:Null(Real) 配的是 Option::<f64>::None、Null(Double) 配的是 Option::<f32>::None,两者写反了(sqlx-core/src/any/arguments.rs:75-76)——对 Postgres 意味着一个 NULL 的 f32 参数会按 FLOAT8 类型发出去。SQLite 因为要保住 Cow 的生命周期、另写了 map_arguments 绕开 convert_to(§19.5.1)。

#[non_exhaustive]—— 意味着未来可能加 variant(比如 Uuid、Timestamp)—— 用户 match 必须带 _ =>—— 避免将来升级 breaking。

这 9 个 variant 就是 0.8.6 的 Any 对"多 DB 通用子集"的全部承诺——每多一个 variant,三家驱动的 any.rs 都要跟着补转换。

19.31 AnyRow::map_from:从具体 Row 做类型降级

Any 驱动怎么把 PgRow / MySqlRow / SqliteRow 转成 AnyRow?核心方法是 AnyRow::map_from(sqlx-core/src/any/row.rs:82-137):

#[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: Vec::with_capacity(row.columns().len()),
        values: Vec::with_capacity(row.columns().len()),
    };

    for col in row.columns() {
        let i = col.ordinal();
        let any_col = AnyColumn::try_from(col)?;
        let value = row.try_get_raw(i)?;

        // Map based on the _value_ type info, not the column type info.
        let type_info = AnyTypeInfo::try_from(&value.type_info()).map_err(...)?;

        let value_kind = match type_info.kind {
            k if value.is_null() => AnyValueKind::Null(k),
            AnyTypeInfoKind::Null => AnyValueKind::Null(AnyTypeInfoKind::Null),
            AnyTypeInfoKind::Bool => AnyValueKind::Bool(decode(value)?),
            // ... SmallInt / Integer / BigInt / Real / Double
            AnyTypeInfoKind::Blob => AnyValueKind::Blob(decode::<_, Vec<u8>>(value)?.into()),
            AnyTypeInfoKind::Text => AnyValueKind::Text(decode::<_, String>(value)?.into()),
        };

        row_out.columns.push(any_col);
        row_out.values.push(AnyValue { kind: value_kind });
    }
    Ok(row_out)
}

注意注释里那句 "Map based on the value type info, not the column type info"—— 降级依据是值的 type info、不是列声明的 type info。

where 子句里的 8 条 Type + Decode 约束—— 约束"R::Database 必须支持 bool/i16/i32/i64/f32/f64/String/Vec<u8>"—— 对应 AnyValueKind 除 Null 外的 8 个数据 variant(Blob 用 Vec<u8>、Text 用 String)。

每家具体 DB 都满足这 8 条—— Postgres 的 bool/int2/int4/int8/float4/float8/text/bytea—— MySQL 的同等类型—— SQLite 的存储类别。

注释里的重要决策(:76-80):

This is not a TryFrom impl 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 the Any driver gains support for more types.

这里说的是 map_from 这个泛型函数本身不做成 trait impl——sqlx-core 不提供"对任意 R: Row 都成立"的 TryFrom<&R> for AnyRow,这样 map_from 的 bound 可以随 Any 支持的类型增加而调整、不算 breaking change。各驱动自己倒是写了具体的 impl TryFrom<&PgRow> for AnyRow(sqlx-postgres/src/any.rs:235-241,MySQL、SQLite 同理),里面只有一行:调 AnyRow::map_from。

map_from 标了 #[doc(hidden)]—— 明确"只给驱动内部用"—— 保留未来调整 bound 的自由—— 这是公共 API 克制的一个例子:泛型的 trait impl 一旦公开就是长期承诺,不如用显式方法、保留改动空间。

19.32 URL scheme 到 backend 的落地

用户调用 sqlx::any::install_default_drivers() 之后、DRIVERS 里最多有三个 AnyDriver(取决于启用了哪些 feature)——怎么从连接 URL 选对一个?

代码路径:

  1. AnyConnection::connect(url) 被调用。
  2. 解析 URL——提取 scheme("postgres" / "postgresql" / "mysql" / "mariadb" / "sqlite")。
  3. 扫 DRIVERS —— 找第一个 url_schemes 切片里包含该 scheme 的 driver。
  4. 调用 (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:26-73)—— 每加一种 DB 要改 enum + FromStr—— 现在的 url_schemes 切片让 driver 自己声明——扩展不改 sqlx-core—— 是开放式架构对封闭式枚举的胜利。

这段演进—— AnyKind 被 #[deprecated] 标注(:10)但保留—— 是 sqlx 架构改进的活化石——读源码时能看到旧世界和新世界并存——可作为 Rust API 迁移的一个小型案例。

下一章第 20 章进入迁移系统——第六部分工具与工程的开始。