Mock 与 Proxy
主线位置:查询主线和写入主线在「
DatabaseConnection按inner分派到驱动」那一步换了终点。语句照样由 sea-query 生成、照样变成一个Statement,只是不再交给sqlx_query和连接池,而是交给MockDatabase(按顺序取预置结果)或者你自己实现的ProxyDatabaseTrait。它们之前的一切(Select、build、$N和Values)和真实连接完全相同,之后的一切(执行、类型、锁、事务语义)全不相同。
flowchart TB
stmt["Statement 和 Values"] --> inner{"DatabaseConnectionType"}
inner -->|"SqlxPostgres"| sqlx["sqlx_query 和连接池"]
inner -->|"MockDatabaseConnection"| mockc["两个计数器"]
inner -->|"ProxyDatabaseConnection"| proxyc["ProxyDatabaseTrait"]
mockc --> queue["按序号取预置结果"]
mockc --> log["记进 transaction_log"]
proxyc --> user["你写的 query 和 execute"]
queue --> qr["QueryResult 或 ExecResult"]
user --> qr
sqlx --> qr
实验 crate 依赖 sea-orm =2.0.4(sqlx-postgres、runtime-tokio-rustls、macros、with-json),mock、proxy 是 crate 自己的 feature(mock = ["sea-orm/mock"]、proxy = ["sea-orm/proxy"]),按实验分别打开;对照用的 1.1.20 是另一个 crate。平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。实体和 事务 一页相同:item,两列 id、n。
mock feature 改了什么
mock = [] 不带任何依赖,只打开一批 #[cfg(feature = "mock")]。最显眼的是 DatabaseConnectionType 多一个分支:
/// In-memory mock connection used for testing (`mock`).
#[cfg(feature = "mock")]
MockDatabaseConnection(Arc<crate::MockDatabaseConnection>),
跟着它,事务里的 InnerConnection、执行结果 ExecResultHolder、行 QueryResultRow 也各多一个 Mock 分支,ConnectionTrait 的每个方法里多一个 match 臂;Database::connect 在所有真实驱动都不认这个 URL 时,交给 MockDatabaseConnector(开着 sqlx-postgres 时 postgres:// 先被 sqlx 驱动接走,走不到这里)。
DatabaseConnectionType 没有 #[non_exhaustive],所以多一个分支是会被下游看见的:
use sea_orm::{DatabaseConnection, DatabaseConnectionType};
fn need_clone<T: Clone>() {}
fn main() {
need_clone::<DatabaseConnection>();
let db = DatabaseConnection::default();
match db.inner {
DatabaseConnectionType::SqlxPostgresPoolConnection(_) => println!("postgres"),
DatabaseConnectionType::Disconnected => println!("disconnected"),
}
}
== 2.0.4 no mock
disconnected
== 2.0.4 mock
error[E0004]: non-exhaustive patterns: `DatabaseConnectionType::MockDatabaseConnection(_)` not covered
--> src/bin/mock_shape.rs:8:9
|
8 | match db.inner {
| ^^^^^^^^ pattern `DatabaseConnectionType::MockDatabaseConnection(_)` not covered
|
note: `DatabaseConnectionType` defined here
--> ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-orm-2.0.4/src/database/db_connection.rs:47:1
|
47 | pub enum DatabaseConnectionType {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
66 | MockDatabaseConnection(Arc<crate::MockDatabaseConnection>),
| ---------------------- not covered
= note: the matched value is of type `DatabaseConnectionType`
...
need_clone::<DatabaseConnection>() 两次都过了:2.0.4 的 DatabaseConnection 和 DatabaseConnectionType 无条件 #[derive(Clone)]。
本站 payment crate 的注释
本站 crates/modules/payment/Cargo.toml 的 [dev-dependencies] 上面写着:
# 注意:不用 sea-orm 的 mock feature——它会经 feature 统一全局禁用
# `DatabaseConnection: Clone`,破坏 app-core 连接池。
本站用的是 1.1.20。1.1.20 的 DatabaseConnection 还是 enum,Clone 是条件派生的:
/// Handle a database connection depending on the backend enabled by the feature
/// flags. This creates a database pool. This will be `Clone` unless the feature
/// flag `mock` is enabled.
#[cfg_attr(not(feature = "mock"), derive(Clone))]
pub enum DatabaseConnection {
同一个 need_clone 在 1.1.20 上:
== 1.1.20 no mock
DatabaseConnection: Clone
== 1.1.20 mock
error[E0277]: the trait bound `DatabaseConnection: Clone` is not satisfied
--> src/main.rs:6:16
|
6 | need_clone::<DatabaseConnection>();
| ^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `DatabaseConnection`
|
note: required by a bound in `need_clone`
--> src/main.rs:3:18
|
3 | fn need_clone<T: Clone>() {}
| ^^^^^ required by this bound in `need_clone`
「经 feature 统一」要再精确一点。用一个两成员的 workspace 模拟(resolver = "2",和本站一样):core-x 依赖 sea-orm =1.1.20,有一个 db.clone() 返回 DatabaseConnection 的函数,像本站 crates/core/src/db/pool.rs 那样;pay-x 依赖 core-x,[dev-dependencies] 里给 sea-orm 加 features = ["mock"]:
== cargo build -p pay-x
exit 0
== cargo test -p pay-x
error[E0308]: mismatched types
--> core/src/lib.rs:5:3
|
4 | pub fn pool_clone(db: &DatabaseConnection) -> DatabaseConnection {
| ------------------ expected `DatabaseConnection` because of return type
5 | db.clone()
| ^^^^^^^^^^ expected `DatabaseConnection`, found `&DatabaseConnection`
|
note: `DatabaseConnection` does not implement `Clone`, so `&DatabaseConnection` was cloned instead
--> core/src/lib.rs:5:3
|
5 | db.clone()
| ^^
...
error: could not compile `core-x` (lib) due to 1 previous error
== cargo test -p core-x (1.1.20)
...
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
resolver 2 下,dev-dependency 的 feature 只在「这次构建要编译 dev-dependency」时才和普通依赖统一。所以 cargo build 不受影响;cargo test -p pay-x(或者对整个 workspace 跑 cargo test)时,整个构建图里只有一份 sea-orm,mock 打开了,core-x 里的 db.clone() 编译失败;单独测 core-x 没事。注释在 1.1.20 下成立,范围是「编译 payment 的测试时」,不是任何构建都「全局」。同样的 workspace 把版本换成 =2.0.4,cargo test -p pay-x 通过。升级到 2.0 之后,这条理由就不存在了,剩下的影响只是上面那个 match 的穷尽性。
#438「Make DatabaseConnection Clone with the default features enabled」说:「Currently the mock feature is by default enabled and that causes the sea_orm::DatabaseConnection to not implement Clone by default, because MockDatabaseConnection (which is a variant of DatabaseConnection) is not clonable」,像 actix-web 这样要求全局数据可克隆的框架因此只能关掉 mock。#562「Exclude mock from default features」把 mock 移出了默认 feature(0.6.0 的 default 里还有它,0.7.0 起没有),条件派生留着。随后的 #665「DatabaseConnection always derive Clone」没有合并,讨论里的意见是「I am more inclined to keep the semantics of a non cloneable MockConnection. It leads to weird behaviour anyway.」以及「the canonical way is to enable mock only as dev dependency」。本站正是这么做的,结果撞上了 dev-dependency 的 feature 统一。条件派生一直留到 #2683「Role Based Access Control」:其中一个提交把两处 #[cfg_attr(not(feature = "mock"), derive(Clone))] 换成了无条件的 #[derive(Clone)](PR 说明没有提这一点),git tag --contains 给出的第一个版本是 2.0.0-rc.28。mock 分支装的是 Arc<MockDatabaseConnection>,克隆的只是 Arc,几个克隆共用同一份结果队列和日志。
MockDatabase:两个队列、两个计数器、一份日志
pub struct MockDatabase {
db_backend: DbBackend,
transaction: Option<OpenTransaction>,
transaction_log: Vec<Transaction>,
exec_results: Vec<Result<MockExecResult, DbErr>>,
query_results: Vec<Result<Vec<MockRow>, DbErr>>,
}
append_query_results 收「结果集的列表」,每个结果集是一串能 IntoMockRow 的东西(Model、(Model, Model)、BTreeMap<String, Value>);append_exec_results 收 MockExecResult { last_insert_id, rows_affected };还有对应的 append_*_errors。into_connection() 把它装进 MockDatabaseConnection:
pub struct MockDatabaseConnection {
execute_counter: AtomicUsize,
query_counter: AtomicUsize,
mocker: Mutex<Box<dyn MockDatabaseTrait>>,
}
execute 和 query_one / query_all 各有一个计数器,每次调用 fetch_add(1) 拿一个序号,交给 MockDatabase::query:
fn query(&mut self, counter: usize, statement: Statement) -> Result<Vec<QueryResult>, DbErr> {
if let Some(transaction) = &mut self.transaction {
transaction.push(statement);
} else {
self.transaction_log.push(Transaction::one(statement));
}
if counter < self.query_results.len() {
match std::mem::replace(
&mut self.query_results[counter],
Err(query_err("this value has been consumed already")),
) {
// ...
}
} else {
Err(query_err("`query_results` buffer is empty."))
}
}
先记日志,再按序号取结果:第 N 次查询拿第 N 个结果集,和 SQL 写了什么无关。mocker 是 Box<dyn MockDatabaseTrait>,MockDatabaseConnection::new 是公开的,想按 SQL 匹配结果可以自己实现 MockDatabaseTrait。
// P
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([
vec![item::Model { id: 1, n: 10 }],
vec![item::Model { id: 7, n: 70 }, item::Model { id: 8, n: 80 }],
])
.append_exec_results([MockExecResult { last_insert_id: 0, rows_affected: 3 }])
.into_connection();
let m = item::ActiveModel { n: Set(10), ..Default::default() }.insert(&db).await?;
let v = item::Entity::find().filter(item::Column::N.gt(1000)).all(&db).await?;
let r = db
.execute_raw(Statement::from_string(DatabaseBackend::Postgres, "DELETE FROM no_such_table"))
.await?;
let e = item::Entity::find_by_id(1).one(&db).await;
show(db.into_transaction_log()); // 每个 Transaction 一行一条语句,后面是 Values
--- P: 预置结果,按顺序取 ---
insert 返回 Model { id: 1, n: 10 }
find().filter(n > 1000).all 返回 [Model { id: 7, n: 70 }, Model { id: 8, n: 80 }]
DELETE FROM no_such_table 返回 rows_affected = 3
第三次查询 = Err(Query(Internal("`query_results` buffer is empty.")))
Transaction 0:
INSERT INTO "item" ("n") VALUES ($1) RETURNING "id", "n" [Int(Some(10))]
Transaction 1:
SELECT "item"."id", "item"."n" FROM "item" WHERE "item"."n" > $1 [Int(Some(1000))]
Transaction 2:
DELETE FROM no_such_table
Transaction 3:
SELECT "item"."id", "item"."n" FROM "item" WHERE "item"."id" = $1 LIMIT $2 [Int(Some(1)), BigUnsigned(Some(1))]
- Postgres 上的
insert带RETURNING,走的是查询,消耗的是query_results的第 0 个,不是exec_results。哪个操作走哪个队列取决于后端和语句,预置结果时要知道这一点。 n > 1000的查询拿回了n是 70、80 的行,DELETE FROM no_such_table返回 3 行受影响:SQL 根本没被看过。- 失败的那次查询也进了日志。日志里的
Statement和发给 sqlx 的是同一个值:$N占位符、Values原样,LIMIT是BigUnsigned参数。测试里通常拿它和Transaction::from_sql_and_values(DbBackend::Postgres, sql, [values])做assert_eq!,into_transaction_log把日志取走(drain),连接不是 mock 时 panic。
事务在 mock 里
db.begin() 在 mock 分支走 new_mock,同样是 DatabaseTransaction::begin,InnerConnection::Mock 分支调 MockDatabase 的 begin。事务状态是 MockDatabase 里的一个 Option<OpenTransaction>,深度记在它身上,SQL 文本是 mock 自己写的(OpenTransaction):
fn init() -> Self {
Self {
stmts: vec![Statement::from_string(DbBackend::Postgres, "BEGIN")],
transaction_depth: 0,
}
}
fn begin_nested(&mut self, db_backend: DbBackend) {
self.transaction_depth += 1;
self.push(Statement::from_string(
db_backend,
format!("SAVEPOINT savepoint_{}", self.transaction_depth),
));
}
commit / rollback 在深度 0 时写 COMMIT / ROLLBACK 并把整个 OpenTransaction 收进日志,否则写 RELEASE SAVEPOINT savepoint_N / ROLLBACK TO SAVEPOINT savepoint_N。DatabaseTransaction 的 start_rollback 在 mock 分支直接调 c.rollback():没有写缓冲可排,Drop 当场记一条 ROLLBACK。
M2 Max、macOS、rustc 1.99.0,mock feature 打开:
// Q
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_exec_results(vec![MockExecResult { last_insert_id: 0, rows_affected: 1 }; 4])
.into_connection();
let up = |n: i32| {
item::Entity::update_many()
.col_expr(item::Column::N, sea_orm::sea_query::Expr::value(n))
.filter(item::Column::Id.eq(1))
};
let txn = db
.begin_with_config(Some(IsolationLevel::Serializable), Some(AccessMode::ReadOnly))
.await?;
up(1).exec(&txn).await?;
let sp = txn.begin().await?;
up(2).exec(&sp).await?;
sp.rollback().await?;
up(3).exec(&db).await?; // &db, not &txn
txn.commit().await?;
let txn = db.begin().await?;
up(4).exec(&txn).await?;
drop(txn);
show(db.into_transaction_log());
日志里有几个 Transaction?up(3)(用的是 &db)是单独一个 Transaction,还是夹在第一个事务的 ROLLBACK TO SAVEPOINT 和 COMMIT 之间?有没有 SET TRANSACTION?
它测不了什么
MockDatabase 证明的是「sea-orm 把这个调用翻译成了这些 Statement,按这个顺序」。凡是要数据库回答的问题,它都答不了。上面的实验和下面这一个,每一条都对应一种测不出来的错误:
| 问题 | 真实 Postgres | MockDatabase |
|---|---|---|
| SQL 能不能跑(表名、列名、语法、类型) | 报错 | 不看 SQL,DELETE FROM no_such_table 返回预置的 rows_affected |
WHERE / LIMIT 对不对 | 决定返回哪些行 | 原样返回预置的行 |
| 约束(非空、唯一、外键) | 报错,sql_err() 能认出来 | 除非 append_*_errors 预置一个错误 |
| 列的类型和解码 | sqlx 按 OID 检查,int4 读成 i64 报 mismatched types | 按 Value 分支转换,转换失败一律报成 NULL(见下) |
| 隔离级别、只读 | SET TRANSACTION ... | new_mock 不传配置,日志里没有这条语句 |
&db 和 &txn 是两条连接 | 看不到未提交的写入、可能等锁或等池 | 只有一个 transaction 槽,&db 的语句被记进开着的事务(动脑筋答案) |
| 保存点的名字 | _sqlx_savepoint_N | savepoint_N |
Drop 回滚的时机 | 排进写缓冲,归还或下次使用时发出 | 当场记一条 ROLLBACK |
| 连接池、超时、取消 | 见事务 | 没有池 |
类型那一行:mock 的行是 BTreeMap<String, Value>,TryGetable 在 QueryResultRow::Mock 那一臂把 MockRow::try_get 的任何错误都换成「遇到了 NULL」:
#[cfg(feature = "mock")]
QueryResultRow::Mock(row) => row.try_get(idx).map_err(|e| {
debug_print!("{:#?}", e.to_string());
err_null_idx_col(idx)
}),
count() 读的是 num_items 列、i64。给它预置三种值:
// X
let db = MockDatabase::new(DatabaseBackend::Postgres)
.append_query_results([[BTreeMap::from([("num_items", Value::BigInt(Some(3)))])]])
.append_query_results([[BTreeMap::from([("num_items", Value::Int(Some(3)))])]])
.append_query_results([[BTreeMap::from([("num_items", Value::String(Some("3".into())))])]])
.into_connection();
for label in ["BigInt", "Int", "String"] {
println!("{label}: {:?}", item::Entity::find().count(&db).await);
}
--- X: count() 读 num_items ---
BigInt: Ok(3)
Int: Err(Type("A null value was encountered while decoding \"num_items\""))
String: Err(Type("A null value was encountered while decoding \"num_items\""))
SELECT COUNT(*) AS num_items FROM (SELECT "item"."id", "item"."n" FROM "item") AS "sub_query"
值明明不是 NULL,报的却是 NULL。用 mock 时看到这条错误,先查预置的 Value 分支和字段类型对不对得上;真实的类型错误长什么样,见 列与值。
问: 既然测不了 SQL 能不能跑,MockDatabase 还有什么用?
答: 它测的是你的代码,不是数据库:一个 handler 在某种输入下发了几条语句、按什么顺序、带什么参数,update 有没有只 SET 改过的列,出错的分支有没有走到 rollback,某个 DbErr 有没有被正确地翻译成业务错误(用 append_query_errors 注入)。这些断言不需要数据库,跑得快,结果确定。要证明 SQL 在 Postgres 上能跑、结果对,得连一个真的库,例如 sqlx 卷的 #[sqlx::test] 那样每个测试一个临时库。
ProxyDatabase:把语句交给你
proxy feature(proxy = ["serde/derive"])打开另一个分支 ProxyDatabaseConnection。连接由 Database::connect_proxy 造,参数是后端(决定 sea-query 用哪种方言)和一个 Arc<Box<dyn ProxyDatabaseTrait>>:
#[async_trait::async_trait]
pub trait ProxyDatabaseTrait: Send + Sync + Debug {
/// Execute a query in the [ProxyDatabase], and return the query results
async fn query(&self, statement: Statement) -> Result<Vec<ProxyRow>, DbErr>;
/// Execute a command in the [ProxyDatabase], and report the number of rows affected
async fn execute(&self, statement: Statement) -> Result<ProxyExecResult, DbErr>;
/// Begin a transaction in the [ProxyDatabase]
async fn begin(&self) {}
/// Commit a transaction in the [ProxyDatabase]
async fn commit(&self) {}
/// Rollback a transaction in the [ProxyDatabase]
async fn rollback(&self) {}
/// Start rollback a transaction in the [ProxyDatabase]
fn start_rollback(&self) {}
/// Ping the [ProxyDatabase], it should return an error if the database is not available
async fn ping(&self) -> Result<(), DbErr> {
Ok(())
}
}
ProxyDatabaseTrait 只有 query 和 execute 必须实现。你拿到的是完整的 Statement(SQL 加 Values),怎么执行、发到哪里由你决定;返回的 ProxyRow 和 MockRow 一样是 BTreeMap<String, Value>,解码也走上面那条「错误一律报成 NULL」的路。仓库里的两个例子 proxy_gluesql_example、proxy_cloudflare_worker_example 用它把 sea-orm 接到 GlueSQL 和 Cloudflare Worker 上:sea-orm 负责生成语句、解码结果,执行交给一个 sqlx 不支持的环境。
事务的四个钩子都有空的默认实现、没有返回值,也不带深度:嵌套 begin 只是再调一次 begin()。DatabaseTransaction 在 proxy 分支里调它们(start_rollback 是同步的,Drop 里调),但事务是不是真的事务,完全看你在钩子里做了什么。实验写一个转发到真实连接池的代理,钩子只打印:
#[derive(Debug)]
struct Forward {
db: DatabaseConnection,
}
#[async_trait::async_trait]
impl ProxyDatabaseTrait for Forward {
async fn query(&self, stmt: Statement) -> Result<Vec<ProxyRow>, DbErr> {
println!(" proxy.query {}", stmt.sql);
let rows = self.db.query_all_raw(stmt).await?;
Ok(rows.iter().map(from_query_result_to_proxy_row).collect())
}
async fn execute(&self, stmt: Statement) -> Result<ProxyExecResult, DbErr> {
println!(" proxy.execute {}", stmt.sql);
Ok(self.db.execute_raw(stmt).await?.into())
}
async fn begin(&self) { println!(" proxy.begin()"); }
async fn rollback(&self) { println!(" proxy.rollback()"); }
fn start_rollback(&self) { println!(" proxy.start_rollback()"); }
}
let db = Database::connect_proxy(DbBackend::Postgres, Arc::new(Box::new(Forward { db: real })))
.await?;
// S:insert、find_by_id
// T
let txn = db.begin().await?;
item::ActiveModel { n: Set(2), ..Default::default() }.insert(&txn).await?;
let sp = txn.begin().await?;
sp.commit().await?;
txn.rollback().await?;
let txn = db.begin().await?;
item::ActiveModel { n: Set(3), ..Default::default() }.insert(&txn).await?;
drop(txn);
// U:select interval '1 day' as d
--- S: 经 ProxyDatabase 插入、查询 ---
proxy.query INSERT INTO "item" ("n") VALUES ($1) RETURNING "id", "n"
insert 返回 Model { id: 1, n: 1 }
proxy.query SELECT "item"."id", "item"."n" FROM "item" WHERE "item"."id" = $1 LIMIT $2
find_by_id 返回 Some(Model { id: 1, n: 1 })
--- T: 事务:begin、嵌套 begin、rollback、drop ---
proxy.begin()
proxy.query INSERT INTO "item" ("n") VALUES ($1) RETURNING "id", "n"
proxy.begin()
proxy.rollback()
proxy.begin()
proxy.query INSERT INTO "item" ("n") VALUES ($1) RETURNING "id", "n"
proxy.start_rollback()
item.n = [1, 2, 3]
--- U: 一个 ProxyRow 不认识的列类型 ---
proxy.query select interval '1 day' as d
thread 'tokio-rt-worker' (3854271) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/sea-orm-2.0.4/src/driver/sqlx_postgres.rs:945:30:
internal error: entered unreachable code: Unknown column type: INTERVAL
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
结果 Err(true)
- T:钩子按调用顺序被调到(
sp.commit()调的commit用的是默认空实现,所以没打印),但代理什么也没做,回滚了的 2 和被 drop 的 3 都留在表里。转发到池的代理要实现事务,得自己在begin里拿一条连接、记住深度、让之后的语句走这条连接,这正是DatabaseTransaction在 sqlx 分支里做的事。 - U:把真实的 sqlx 行转成
ProxyRow的from_query_result_to_proxy_row,Postgres 分支是from_sqlx_postgres_row_to_proxy_row:按列的类型名逐个match,没列到的类型(这里是INTERVAL)走unreachable!,整个任务 panic。
ProxyDatabase 是 #2000「Database Proxy」加进来的(首次发布 0.12.10)。trait 上从一开始就有 begin / commit / rollback 三个空钩子,db.begin() 也会走到 new_proxy,但 DatabaseTransaction 的 begin、commit、rollback 里只有 sqlx 和 mock 的分支,proxy 连接落进 _ => Err(conn_err("Disconnected")):钩子永远调不到,db.begin() 返回 Disconnected 错误。#2573「Add proxy transaction impl」补上了四个 InnerConnection::Proxy 分支,并给 trait 加了同步的 start_rollback 钩子供 Drop 调用,说明只有一句「Support ProxyDatabase in DatabaseTransaction impl.」。CHANGELOG 记在 1.1.11(2025-05-07)。新钩子同样有空的默认实现,已有的代理不用改;不实现钩子的代理,事务就是上面 T 段那样一串普通语句。
动脑筋答案
--- Q: 事务、保存点、隔离级别、drop ---
Transaction 0:
BEGIN
UPDATE "item" SET "n" = $1 WHERE "item"."id" = $2 [Int(Some(1)), Int(Some(1))]
SAVEPOINT savepoint_1
UPDATE "item" SET "n" = $1 WHERE "item"."id" = $2 [Int(Some(2)), Int(Some(1))]
ROLLBACK TO SAVEPOINT savepoint_1
UPDATE "item" SET "n" = $1 WHERE "item"."id" = $2 [Int(Some(3)), Int(Some(1))]
COMMIT
Transaction 1:
BEGIN
UPDATE "item" SET "n" = $1 WHERE "item"."id" = $2 [Int(Some(4)), Int(Some(1))]
ROLLBACK
两个 Transaction,up(3) 夹在第一个事务里,没有 SET TRANSACTION。
MockDatabase只有一个transaction槽,execute一看它是Some就把语句推进去,不管语句是从&db还是&txn来的。真实 Postgres 上up(3)在另一条连接上自动提交,还会等up(1)持有的行锁(事务 的 K 段);mock 的日志却显示它在事务里,测试会以为它是原子的。begin_with_config的隔离级别和只读在 mock 分支被丢掉:TransactionTrait for DatabaseConnection的 mock 臂调的是new_mock(Arc::clone(conn), None),DatabaseTransaction::begin的 mock 分支也不调set_transaction_config。- 保存点叫
savepoint_1,不是 sqlx 的_sqlx_savepoint_1;第一行的BEGIN不管后端是什么都用DbBackend::Postgres构造。拿 mock 日志断言保存点名字,断言的是 mock 自己的写法。 drop(txn)当场记下ROLLBACK,和真实连接上「排进写缓冲、归还时发出」的时机不同,但日志看不出时机,只看得出有这条语句。
小结
mock是纯cfg开关:给DatabaseConnectionType、InnerConnection、QueryResultRow、ExecResultHolder各加一个分支。DatabaseConnectionType不是non_exhaustive,打开mock会让下游对db.inner的穷尽match编译失败。- 1.1.20 里
mock还去掉DatabaseConnection: Clone;本站 payment 的注释在 1.1.20 下成立,范围是编译它的测试(dev-dependency 的 feature 在 resolver 2 下只在构建测试时统一),2.0.0-rc.28 起Clone无条件派生。 MockDatabase按调用序号从query_results/exec_results取结果,不看 SQL;Postgres 上带RETURNING的写入消耗的是查询队列。日志里是完整的Statement($N和Values),用来断言生成了什么 SQL、几条、什么顺序。- mock 的事务是一个全局槽:
&db的语句会被记进开着的事务,隔离级别被丢掉,保存点叫savepoint_N,Drop当场记ROLLBACK。类型转换失败一律报成「A null value was encountered」。 ProxyDatabaseTrait把Statement交给你执行,事务钩子默认是空的,不带深度;from_query_result_to_proxy_row碰到没列出的列类型会 panic。