Rust in Everything
基于 sqlx 0.9.0

Database 与关联类型

主线位置:三条主线共用的类型表。查询主线里 sqlx::query("...") 的类型是 Query<'_, Postgres, PgArguments>,PgArguments 就是 <Postgres as Database>::Arguments,fetch_one 返回的 PgRow 是 Database::Row;连接池主线的 Pool<Postgres> 里放的是 Database::Connection;编译期检查主线里 describe 返回 Describe<DB>,列类型是 Database::TypeInfo。这一页只讲这张表本身和它的两个替代方案(泛型约束、Any 驱动),表里每一项的机制在各自的页面。

flowchart TB
  db["Postgres: Database"] --> conn[PgConnection]
  db --> row[PgRow]
  db --> args[PgArguments]
  db --> ti[PgTypeInfo]
  db --> vr["PgValueRef 'r"]
  db --> st[PgStatement]
  conn -->|"Database = Self"| db
  row -->|"Database = Self"| db
  args -->|"Database = Self"| db

一个数据库就是一组关联类型

Database 没有一个方法,只有关联类型和两个常量(每一项上面的 /// 文档注释都删掉了):

pub trait Database: 'static + Sized + Send + Debug {
    type Connection: Connection<Database = Self>;

    type TransactionManager: TransactionManager<Database = Self>;

    type Row: Row<Database = Self>;

    type QueryResult: 'static + Sized + Send + Sync + Default + Extend<Self::QueryResult>;

    type Column: Column<Database = Self>;

    type TypeInfo: TypeInfo;

    type Value: Value<Database = Self> + 'static;
    type ValueRef<'r>: ValueRef<'r, Database = Self>;

    type Arguments: Arguments<Database = Self>;
    type ArgumentBuffer;

    type Statement: Statement<Database = Self>;

    const NAME: &'static str;

    const URL_SCHEMES: &'static [&'static str];
}

Postgres 驱动的实现就是把这张表填满:

impl Database for Postgres {
    type Connection = PgConnection;

    type TransactionManager = PgTransactionManager;

    type Row = PgRow;

    type QueryResult = PgQueryResult;

    type Column = PgColumn;

    type TypeInfo = PgTypeInfo;

    type Value = PgValue;
    type ValueRef<'r> = PgValueRef<'r>;

    type Arguments = PgArguments;
    type ArgumentBuffer = PgArgumentBuffer;

    type Statement = PgStatement;

    const NAME: &'static str = "PostgreSQL";

    const URL_SCHEMES: &'static [&'static str] = &["postgres", "postgresql"];
}

Postgres 本身是个空结构体,从不被构造,只当类型参数用:Pool<Postgres>、Query<'_, Postgres, _>、Encode<'_, Postgres>。各项的用途和讲在哪:

关联类型Postgres在哪儿出现详见
ConnectionPgConnection&mut PgConnection 是 Executor;池里存的就是它Executor 与 Acquire、连接选项与建立连接
TransactionManagerPgTransactionManagerTransaction 靠它发 BEGIN / SAVEPOINT事务
Row / ColumnPgRow / PgColumnfetch_* 的结果、FromRow 的输入Row 与 FromRow
QueryResultPgQueryResultexecute 的结果,只有 rows_affected;Extend 用来把多条语句的结果加起来本页下文、Executor 与 Acquire
TypeInfoPgTypeInfoType::type_info()、列和参数的类型类型、Postgres 类型
Value / ValueRef<'r>PgValue / PgValueRef<'r>Decode 的输入类型
Arguments / ArgumentBufferPgArguments / PgArgumentBufferbind 往里写,Encode 的输出缓冲Query、QueryAs 与 SqlSafeStr
StatementPgStatementprepare 的结果,带参数和列的元数据扩展查询协议与语句缓存

两个常量也有消费者:Any 驱动登记一个驱动时,AnyDriver::without_migrate 直接取 DB::NAME 和 DB::URL_SCHEMES,后者决定哪个 URL 前缀交给哪个驱动(见本页最后一节)。

每一项都指回 Database

表里几乎每个 trait 都带 type Database 并要求等于 Self,Connection 反过来还要求 Database<Connection = Self>:

pub trait Connection: Send {
    type Database: Database<Connection = Self>;

    type Options: ConnectOptions<Connection = Self>;
    // ...
}

双向约束让泛型代码从任何一端出发都能拿到整张表。只拿着一个 R: Row,就能写 <R::Database as Database>::ValueRef<'_>;只拿着一个 C: Connection,就能写 Transaction<'_, C::Database>。Database = Self 同时保证不会出现「PgRow 声称自己属于 SQLite」这种组合,编译器在 impl 处就拒绝。

TypeInfo 和 ArgumentBuffer 不指回来:TypeInfo 只要求能打印、比较、跨线程(Debug + Display + Clone + PartialEq + Send + Sync),ArgumentBuffer 连约束都没有,它只是 Encode::encode_by_ref(&self, buf: &mut DB::ArgumentBuffer) 的参数类型。

生命周期:只剩 ValueRef<'r>

0.9 的表里只有一个泛型关联类型 ValueRef<'r>。'r 是行缓冲的生命周期:PgValueRef<'r> 借用 PgRow 里那条 DataRow 的字节,解码成 &'r str 时不用拷贝。这和 serde 的 'de 是同一个思路,见 生命周期 'de 与借用。

0.8.6 的同一段还有三个:

    /// The concrete `Arguments` implementation for this database.
    type Arguments<'q>: Arguments<'q, Database = Self>;
    /// The concrete type used as a buffer for arguments while encoding.
    type ArgumentBuffer<'q>;

    /// The concrete `Statement` implementation for this database.
    type Statement<'q>: Statement<'q, Database = Self>;

'q 是查询的生命周期:参数缓冲可以借用被绑定的值,语句可以借用 SQL 字符串。Postgres 的 PgArguments 其实一直是自有数据(Encode 立刻把值写成字节),这个 'q 是为了给借用参数的驱动留余地。0.9 把它删了:Statement 改为持有一个自有的 SqlStr,Arguments 要求所有驱动的参数缓冲都自有数据。代价是 SQLite 驱动绑定 &str 时要拷贝一次,收益是 Query<'static, ...> 可以从函数里返回(实验见 Query、QueryAs 与 SqlSafeStr),也不再有一串 'q 跟着每个签名走。

📝NOTE
冷知识:这张表的 GAT 在 0.8 才来,0.9 又走了三个

0.8 之前没有 type Arguments<'q> 这种写法,Database 靠三个辅助 trait 作超 trait:for<'r> HasValueRef<'r, Database = Self>、for<'q> HasArguments<'q, Database = Self>、for<'q> HasStatement<'q, Database = Self>,带生命周期的类型放在它们里面,用的时候写 <DB as HasArguments<'q>>::Arguments。源码里当时有一句注释:「The upcoming Rust feature, Generic Associated Types, should obviate the need for this trait.」#2973(首次发布于 0.8.0)把三个辅助 trait 换成了 Database 里的 GAT,理由是「GATs have been stable since Rust 1.65」。

到 0.9.0,#3723「Removes the lifetime of Statement since the underlying sql string is now owned」,#3960 去掉 Arguments 和 ArgumentBuffer 的生命周期,PR 说明写的是「it standardizes that all argument buffers own their data」,并且让 query! 绑定参数时对引用的处理在所有驱动上「similar to the println! macro」。于是今天只剩 ValueRef<'r> 一个 GAT。

HasStatementCache

HasStatementCache 是一个空的标记 trait:

/// A [`Database`] that maintains a client-side cache of prepared statements.
pub trait HasStatementCache {}

它只出现在约束里:Query::persistent、QueryAs::persistent、QueryScalar::persistent 要求 DB: HasStatementCache,Connection::cached_statements_size 和 clear_cached_statements 也要求 Self::Database: HasStatementCache(这两个方法的默认实现分别返回 0 和什么也不做)。意思是:没有客户端语句缓存的驱动,连 .persistent(false) 这个开关都不该有。

0.9.0 里 Postgres、MySQL、SQLite 三个驱动和 Any 都实现了它,Any 那一行旁边注释写着「This may be true, depending on the selected database」。所以今天它不挡任何调用,只是给将来没有缓存的驱动留的位置。Postgres 的缓存怎么工作见 扩展查询协议与语句缓存。

写一个对所有数据库都成立的函数

关联类型的好处是零开销:Pool<Postgres> 上的调用单态化成直接调 PgConnection 的代码,没有虚表。代价落在泛型代码上:想写一个对任意 DB 都成立的函数,每一处用到的能力都要写成约束。

下面的函数对 Postgres 和 SQLite 都能用(src/bin/generic_db.rs,依赖是实验 crate 的 sqlx =0.9.0,开了 postgres、sqlite、macros 等 feature;平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

use sqlx::any::{AnyPoolOptions, install_default_drivers};
use sqlx::{Database, Encode, Executor, Pool, Type};

// Works for any driver: everything database-specific comes from DB's associated types.
async fn add_one<DB>(pool: &Pool<DB>, x: i64) -> anyhow::Result<i64>
where
  DB: Database,
  for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
  for<'q> i64: Encode<'q, DB> + Type<DB>,
  DB::Arguments: sqlx::IntoArguments<DB>,
  (i64,): for<'r> sqlx::FromRow<'r, DB::Row>,
{
  let (y,): (i64,) = sqlx::query_as("select $1 + 1").bind(x).fetch_one(pool).await?;
  Ok(y)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let url = std::env::var("DATABASE_URL")?;
  let pg = sqlx::PgPool::connect(&url).await?;
  let lite = sqlx::SqlitePool::connect("sqlite::memory:").await?;
  println!("Postgres: {} ({})", add_one(&pg, 41).await?, <sqlx::Postgres as Database>::NAME);
  println!("SQLite  : {} ({})", add_one(&lite, 41).await?, <sqlx::Sqlite as Database>::NAME);

  install_default_drivers();
  for u in [url.as_str(), "sqlite::memory:"] {
    let any = AnyPoolOptions::new().max_connections(1).connect(u).await?;
    let backend = any.acquire().await?.backend_name().to_owned();
    println!("Any -> {backend:<10}: {}", add_one(&any, 41).await?);
  }
  // ... 后面一段见本页的动脑筋
  Ok(())
}
Postgres: 42 (PostgreSQL)
SQLite  : 42 (SQLite)
Any -> PostgreSQL: 42
Any -> SQLite    : 42

四条约束各对应函数体里的一个调用:

  • for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>:&Pool<DB> 的 Executor 实现要求这一条(见 Executor 与 Acquire)。Connection trait 本身不要求「&mut Self 是 Executor」,所以泛型代码只能自己补上。
  • for<'q> i64: Encode<'q, DB> + Type<DB>:bind(x) 的要求。
  • DB::Arguments: IntoArguments<DB>:fetch_one 所在的 impl 块要求参数类型能转成 DB::Arguments。对具体驱动这是一条恒等转换,由 impl_into_arguments_for_arguments! 宏逐个驱动生成,宏上面的注释是「NOTE: required due to lack of lazy normalization」:没法写成一个对所有 DB 成立的 blanket impl,泛型代码里就看不到它,只能再写一遍。删掉这一行,rustc 报 the following trait bounds were not satisfied: `<DB as sqlx::Database>::Arguments: IntoArguments<DB>`。
  • (i64,): for<'r> FromRow<'r, DB::Row>:query_as 把行转成元组。

一个 select $1 + 1 要四行约束,这就是「关联类型而不是 trait 对象」的代价。实际项目里常见的做法是不写泛型,直接用 PgPool;需要在运行时选数据库时用 Any。

为什么不用 trait 对象

能不能干脆存一个 Box<dyn Connection>?0.9.0 里不行:

use sqlx::postgres::{PgConnectOptions, Postgres};
use sqlx::Connection;

fn main() {
  let _conns: Vec<Box<dyn Connection<Database = Postgres, Options = PgConnectOptions>>> = Vec::new();
}
error[E0038]: the trait `sqlx::Connection` is not dyn compatible
   --> src/bin/dyn_conn.rs:5:27
    |
  5 |   let _conns: Vec<Box<dyn Connection<Database = Postgres, Options = PgConnectOptions>>> = Vec::new();
    |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `sqlx::Connection` is not dyn compatible
    |
note: for a trait to be dyn compatible it needs to allow building a vtable
      for more information, visit <https://doc.rust-lang.org/reference/items/traits.html#dyn-compatibility>
   --> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sqlx-core-0.9.0/src/connection.rs:37:23
    |
 37 |     fn close(self) -> impl Future<Output = Result<(), Error>> + Send + 'static;
    |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait is not dyn compatible because method `close` references an `impl Trait` type in its return type
...

Connection 的异步方法在 0.9 改成了返回 impl Future(#3525「Remove unnecessary boxfutures」),省掉每次调用的一次堆分配,但带 impl Trait 返回值的 trait 不能做成 trait 对象。同一行代码在 0.8.6 里能编译,那时 close、ping、begin 都返回 BoxFuture。即使在 0.8.6,dyn Connection 也没多大用:执行查询的 Executor 是给 &mut PgConnection 这样的具体类型实现的,方法本身是泛型的(fn fetch_many<'e, 'q: 'e, E>(...)),带泛型参数的方法进不了虚表。

所以 sqlx 的分工是:编译期确定数据库时,全走关联类型和单态化;运行时才知道数据库时,用一个专门的驱动 Any,它自己也是一个 Database。

Any 驱动:在运行时补上这一块

Any 和 Postgres 一样填满这张表,只是填的都是「装着某个真实驱动」的类型:

impl Database for Any {
    type Connection = AnyConnection;
    // ...
    type Arguments = AnyArguments;
    type ArgumentBuffer = AnyArgumentBuffer;

    type Statement = AnyStatement;

    const NAME: &'static str = "Any";

    const URL_SCHEMES: &'static [&'static str] = &[];
}

动态分派发生在连接这一层。AnyConnection 只有一个字段 backend: Box<dyn AnyConnectionBackend>,AnyConnectionBackend 是专门为 trait 对象设计的:方法返回 BoxFuture,close 收 self: Box<Self>,查询方法收的是具体的 AnyArguments 而不是泛型 E: Execute。Postgres 驱动给 PgConnection 实现这个 trait,在里面把 AnyArguments 转成 PgArguments、把 PgRow 转成 AnyRow。

参数和值的表示也换成了一个封闭的枚举。AnyArgumentBuffer 是 Vec<AnyValueKind>,AnyValueKind 只有这几种:

pub enum AnyValueKind {
    Null(AnyTypeInfoKind),
    Bool(bool),
    SmallInt(i16),
    Integer(i32),
    BigInt(i64),
    Real(f32),
    Double(f64),
    Text(Arc<String>),
    TextSlice(Arc<str>),
    Blob(Arc<Vec<u8>>),
}

这是所有驱动都能表示的公共子集。Postgres 一侧的映射在 TryFrom<&PgTypeInfo> for AnyTypeInfo:BOOL、INT2/4/8、FLOAT4/8、BYTEA、TEXT、VARCHAR、citext 有对应,其余一律返回 Error::AnyDriverError("Any driver does not support the Postgres type ...")。

运行时选驱动靠一张全局表。sqlx::any::install_default_drivers() 把按 feature 编译进来的驱动(MySQL、Postgres、SQLite 各自的 DRIVER 常量)放进 driver.rs 里的 static DRIVERS: OnceLock<&'static [AnyDriver]>,连接时按 URL 的 scheme 在表里找 url_schemes 包含它的那个驱动。不调 install_default_drivers 就连接,from_url 会 panic:「No drivers installed. Please see the documentation in sqlx::any for details.」

上面实验的后两行就是 Any 走通的样子:同一个 AnyPool 类型,URL 换一下就换了数据库,add_one 的四条约束对 Any 同样成立。

❗IMPORTANT
动脑筋:Any 能不能跑 select now()?

接着上面的程序,在同一个 Postgres URL 上建一个 AnyPool,执行一条 Postgres 自己当然能跑的查询(M2 Max、macOS、rustc 1.99.0、Postgres 16.14):

  let any = AnyPoolOptions::new().max_connections(1).connect(&url).await?;
  match sqlx::query("select now()").fetch_one(&any).await {
    Ok(_) => println!("Any select now(): ok"),
    Err(err) => println!("Any select now(): {err}"),
  }

这里没有解码任何列,只是把行取回来。输出是 ok 还是一个错误?如果是错误,是哪一种?

动脑筋答案

Any select now(): error occurred while decoding column now: error in Any driver mapping: Any driver does not support the Postgres type PgTypeInfo(Timestamptz)

是错误,而且是在取行的时候,不是在解码的时候。PgConnection 的 AnyConnectionBackend 实现拿到 PgRow 后立刻转成 AnyRow,转换时逐列做 AnyTypeInfo::try_from(&col.type_info),TIMESTAMPTZ 不在映射表里,TryFrom<&PgColumn> for AnyColumn 把 AnyDriverError 包进 Error::ColumnDecode { index: "now", .. },所以外层写的是 error occurred while decoding column now。用户代码还没碰到这一列,整行就已经拿不回来了:Any 能处理的列类型就是 AnyValueKind 那十种。

小结

  • Database 没有方法,是一张关联类型表;各个组件 trait 用 Database = Self 指回来,泛型代码从任一端都能拿到整张表。
  • 0.9 只剩 ValueRef<'r> 一个 GAT:Statement 持有自有的 SqlStr,Arguments 不再借用被绑定的值。
  • HasStatementCache 是空标记,挡住没有语句缓存的驱动调用 persistent;0.9.0 的四个 Database 都实现了它。
  • 写跨数据库的泛型函数要逐条补约束,IntoArguments 那条是因为没法写 blanket impl。
  • Connection 在 0.9 不再是 dyn 兼容的;运行时选库用 Any:Box<dyn AnyConnectionBackend> 做分派,值收窄到 AnyValueKind 的十种,TIMESTAMPTZ 这类列连取行都会失败。

💬 关联讨论

发起讨论
加载中...