错误
主线位置:三条主线的失败出口。查询主线上
Selector::one里的每个?、写入主线上insert/update的每个?,返回的都是DbErr。本页讲它有哪些分支、各由谁构造、Display出来是什么,以及怎么从里面认出「唯一约束冲突」这类需要单独处理的错误。sqlx 一侧的Error和PgDatabaseError字段见 sqlx 卷的错误。
flowchart TB sqlxerr["sqlx::Error"] -->|"connect / ping"| conn["DbErr::Conn"] sqlxerr -->|"execute / execute_unprepared"| exec["DbErr::Exec"] sqlxerr -->|"query_* / begin / commit"| query["DbErr::Query"] sqlxerr -->|"acquire 超时或池已关"| acq["DbErr::ConnectionAcquire"] own["sea-orm 自己的检查"] --> rest["RecordNotUpdated / PrimaryKeyNotSet ..."] exec --> sqlerr["sql_err 按 SQLSTATE"] query --> sqlerr sqlerr --> se["SqlErr 两种冲突"]
DbErr:一个扁平的 enum
DbErr 用 thiserror 派生,每个分支一个 #[error(...)] 模板(文档注释删掉了):
#[derive(Error, Debug, Clone)]
#[non_exhaustive]
pub enum DbErr {
#[error("Failed to acquire connection from pool: {0}")]
ConnectionAcquire(#[source] ConnAcquireErr),
#[error("Error converting `{from}` into `{into}`: {source}")]
TryIntoErr {
from: &'static str,
into: &'static str,
source: Arc<dyn std::error::Error + Send + Sync>,
},
#[error("Connection Error: {0}")]
Conn(#[source] RuntimeErr),
#[error("Execution Error: {0}")]
Exec(#[source] RuntimeErr),
#[error("Query Error: {0}")]
Query(#[source] RuntimeErr),
#[error("Type '{0}' cannot be converted from u64")]
ConvertFromU64(&'static str),
#[error("Failed to unpack last_insert_id")]
UnpackInsertId,
#[error("Failed to get primary key from model")]
UpdateGetPrimaryKey,
#[error("RecordNotFound Error: {0}")]
RecordNotFound(String),
#[error("Attribute {0} is NotSet")]
AttrNotSet(String),
#[error("Custom Error: {0}")]
Custom(String),
#[error("Type Error: {0}")]
Type(String),
#[error("Json Error: {0}")]
Json(String),
#[error("Migration Error: {0}")]
Migration(String),
#[error("None of the records are inserted")]
RecordNotInserted,
#[error("None of the records are updated")]
RecordNotUpdated,
#[error("Operation not supported by backend {db}: {ctx}")]
BackendNotSupported { db: &'static str, ctx: &'static str },
#[error("Key arity mismatch: expected {expected}, received {received}")]
KeyArityMismatch { expected: u8, received: u8 },
#[error("Primay key not set for {ctx}")]
PrimaryKeyNotSet { ctx: &'static str },
#[error("RBAC error: {0}")]
RbacError(String),
#[error("Access denied: cannot perform `{permission}` on `{resource}`")]
AccessDenied { permission: String, resource: String },
#[error("Mutex poisoned")]
MutexPoisonError,
}
几个形状上的特点:
- 它没有按「谁的错」分层。驱动错误、sea-orm 的业务检查(没更新到行、主键没设)、类型转换、迁移、RBAC 全在同一层。
- 携带驱动错误的只有
Conn、Exec、Query三个,都包一个RuntimeErr;其他分支要么没有数据,要么是一个String或几个&'static str。 #[non_exhaustive]:match必须带_分支。这个属性是 2.0 加的,同时相比 1.1.20 多了六个分支:BackendNotSupported、KeyArityMismatch、PrimaryKeyNotSet、RbacError、AccessDenied、MutexPoisonError。以后再加分支不算破坏性改动。Clone:2.0 起才有,靠的是把 sqlx 的错误放进Arc(下一节)。PartialEq是手写的:impl PartialEq for DbErr只有一句self.to_string() == other.to_string(),比较的是Display文本,见本页的动脑筋。
RuntimeErr 有三个分支(rusqlite 那个只在开了 rusqlite feature 时存在),Display 都是 {0},原样转出里面的错误:
#[derive(Error, Debug, Clone)]
pub enum RuntimeErr {
/// SQLx Error
#[cfg(feature = "sqlx-dep")]
#[error("{0}")]
SqlxError(Arc<sqlx::error::Error>),
/// Rusqlite Error
#[cfg(feature = "rusqlite")]
#[error("{0}")]
Rusqlite(Arc<crate::driver::rusqlite::RusqliteError>),
/// Error generated from within SeaORM
#[error("{0}")]
Internal(String),
}
Internal 装 sea-orm 自己写的文字,由 conn_err、exec_err、query_err 三个辅助函数构造,例如 Disconnected、has no supporting driver、Loader 的 Relation is HasMany instead of HasOne。CHANGELOG 的 2.0.0 破坏性改动里写着:「RuntimeErr::SqlxError is now held in Arc to make DbErr clonable and smaller」。1.1.20 里这里是 SqlxError(sqlx::error::Error),DbErr 不能 Clone;匹配写法也跟着变了,1.1.20 能一层模式写到底 DbErr::Query(RuntimeErr::SqlxError(sqlx::Error::Database(e))),2.0 中间隔着一个 Arc,要先拿到 e 再 e.deref()(下文 sql_err 的源码就是这么写的)。
同一个 sqlx 错误包成哪个分支,看它在哪一步出错
驱动里把 sqlx::Error 变成 DbErr 的是 sqlx_common.rs 里的四个函数:
pub fn sqlx_error_to_exec_err(err: sqlx::Error) -> DbErr {
DbErr::Exec(RuntimeErr::SqlxError(err.into()))
}
pub fn sqlx_error_to_query_err(err: sqlx::Error) -> DbErr {
DbErr::Query(RuntimeErr::SqlxError(err.into()))
}
pub fn sqlx_error_to_conn_err(err: sqlx::Error) -> DbErr {
DbErr::Conn(RuntimeErr::SqlxError(err.into()))
}
pub fn sqlx_conn_acquire_err(sqlx_err: sqlx::Error) -> DbErr {
match sqlx_err {
sqlx::Error::PoolTimedOut => DbErr::ConnectionAcquire(ConnAcquireErr::Timeout),
sqlx::Error::PoolClosed => DbErr::ConnectionAcquire(ConnAcquireErr::ConnectionClosed),
_ => DbErr::Conn(RuntimeErr::SqlxError(sqlx_err.into())),
}
}
它们看的不是错误本身,而是调用的位置。Postgres 驱动和事务里的调用点:
| 出错的位置 | 分支 |
|---|---|
Database::connect 解析 URL、建池(sqlx_error_to_conn_err);ping | Conn |
每条语句前的 pool.acquire()(sqlx_conn_acquire_err) | ConnectionAcquire(Timeout) / ConnectionAcquire(ConnectionClosed) / 其他情况 Conn |
execute_raw、execute_unprepared、SET TRANSACTION(sqlx_error_to_exec_err) | Exec |
query_one_raw、query_all_raw、stream、从 QueryResult 取列时的解码错误、事务的 BEGIN / commit / rollback(sqlx_error_to_query_err) | Query |
所以「插入撞了唯一约束」在 Postgres 上通常是 Query 而不是 Exec:ActiveModel::insert 带 RETURNING,走的是 query_all;换成 exec_without_returning 才是 Exec。删除走 execute,是 Exec。事务的 COMMIT 失败也是 Query。下面的实验里都能看到。按分支判断错误种类几乎总是错的,Exec 和 Query 的区别只说明 sea-orm 走了哪个方法。
同一个超时在 Database::connect 里是 Conn(SqlxError(PoolTimedOut)),在语句执行时是 ConnectionAcquire(Timeout),Display 也不同,实验见 DatabaseConnection 与 ConnectOptions 的动脑筋。
每个分支由谁构造
在 2.0.4 的 src/、sea-orm-macros/src/、sea-orm-migration/src/ 里 grep -rnE "DbErr::<分支>\b",去掉 match 分支、matches! 和文档注释,再加上 conn_err / exec_err / query_err / type_err / json_err 这几个辅助函数的调用:
| 分支 | 2.0.4 里谁构造(Postgres 能走到的) |
|---|---|
ConnectionAcquire | sqlx_conn_acquire_err:PoolTimedOut → Timeout,PoolClosed → ConnectionClosed(ConnAcquireErr 的 Display 分别是 Connection pool timed out、Connection closed) |
Conn | 建池、ping、acquire 的其他错误;conn_err:URL 解析失败、没有驱动、Disconnected |
Exec | execute_raw、execute_unprepared、SET TRANSACTION |
Query | query_*_raw、stream、BEGIN / COMMIT / ROLLBACK、QueryResult::try_get 的 sqlx 解码错误;query_err:Loader 的关系检查 |
Type | type_err:TryGetError::Null 转过来的 A null value was encountered while decoding ...、ActiveEnum 读到未知值、宏生成的 Missing value for column '...'(见列与值)、ActiveModel 的 set_parent_key / get_parent_key 一类方法发现关系类型不对 |
RecordNotFound | require_one(record_not_found:None of the models match the query)、插入 / 更新后再查一次没查到(Failed to find inserted item / Failed to find updated item)、Loader、schema 同步 |
RecordNotInserted | 插入:RETURNING 一行都没回来,或 rows_affected 为 0(ON CONFLICT DO NOTHING 撞了),见插入 |
RecordNotUpdated | 更新的 WHERE 没匹配到行,见更新与删除 |
PrimaryKeyNotSet | Update::one / Delete::one 收到主键 NotSet 的 ActiveModel(ctx 是 UpdateOne / DeleteOne);1.x 这里是 panic |
UpdateGetPrimaryKey | 更新后取不到主键值,executor/update.rs 一处 |
AttrNotSet | Model::try_from(ActiveModel)(宏生成)、ActiveModelEx 的宏、query/util.rs |
KeyArityMismatch | 游标分页的键列数和值个数对不上(executor/cursor.rs) |
UnpackInsertId | 插入后从返回行或 last_insert_id 里拼不出主键 |
TryIntoErr | TryFromU64 的整数转换(u64 转 i32 溢出);SQLite 的 f64 转 Decimal |
ConvertFromU64 | 非整数主键类型的 TryFromU64(字符串、UUID、元组、ActiveEnum、TextUuid) |
Json | ActiveModel::from_json 收到的不是 JSON 对象、TryGetableFromJson |
BackendNotSupported | 不支持 RETURNING 的后端(默认 feature 下的 MySQL、SQLite)上调 exec_with_returning 一类方法、部分 stream、sea-orm-migration 的部分方法 |
RbacError / AccessDenied | RestrictedConnection(rbac feature):*_raw 一律拒绝、权限检查不通过 |
Migration | 只有 sea-orm-migration 里 MigrationTrait::down 的默认实现:We Don't Do That Here |
Custom | sea-orm 本体从不构造;sea-orm-migration 用它报「库里记录已执行、文件却找不到」的迁移,和 has_column 等查询没拿到结果;其余留给用户自己用 |
MutexPoisonError | 只在 sync feature(sea-orm-sync 那份同步代码)的事务里,异步版本用的是不会中毒的异步锁 |
实验:常见错误长什么样
三张表,email 唯一、age 有 CHECK (age >= 0)、两张子表各有一个外键(其中 dchild 的外键是 DEFERRABLE INITIALLY DEFERRED,到 COMMIT 才检查),parent 里有一行 alice@example.com。实体用 #[sea_orm::model] 写,每个错误打印分支、Display、sql_err() 和 SQLSTATE(src/bin/errors.rs):
fn sqlstate(e: &DbErr) -> Option<String> {
if let DbErr::Query(RuntimeErr::SqlxError(e)) | DbErr::Exec(RuntimeErr::SqlxError(e)) | DbErr::Conn(RuntimeErr::SqlxError(e)) = e
&& let sqlx::Error::Database(db_err) = e.deref()
{
return db_err.code().map(|c| c.into_owned());
}
None
}
fn report(case: &str, e: &DbErr) {
println!("## {case}");
println!(" 分支 = {}", variant(e));
println!(" Display = {e}");
println!(" sql_err = {:?}", e.sql_err());
println!(" SQLSTATE = {:?}", sqlstate(e));
}
// ...
let dup = parent::ActiveModel { email: Set("alice@example.com".into()), age: Set(1), ..Default::default() };
let e = dup.clone().insert(&db).await.err().expect("dup");
report("唯一约束:ActiveModel::insert", &e);
println!(" Debug = {e:?}");
// ...
let e = parent::Entity::insert(dup).exec_without_returning(&db).await.err().expect("dup");
report("唯一约束:exec_without_returning", &e);
// ...
let txn = db.begin().await?;
dchild::ActiveModel { parent_id: Set(999), ..Default::default() }.insert(&txn).await?;
println!("## 可延迟外键:insert 成功,错误留到 commit");
let e = txn.commit().await.err().expect("deferred fk");
report("可延迟外键:commit", &e);
M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上的输出:
## 唯一约束:ActiveModel::insert
分支 = Query
Display = Query Error: error returned from database: duplicate key value violates unique constraint "parent_email_key" at line 666
sql_err = Some(UniqueConstraintViolation("duplicate key value violates unique constraint \"parent_email_key\""))
SQLSTATE = Some("23505")
Debug = Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "23505", message: "duplicate key value violates unique constraint \"parent_email_key\"", detail: Some("Key (email)=(alice@example.com) already exists."), hint: None, position: None, where: None, schema: Some("so_conn1"), table: Some("parent"), column: None, data_type: None, constraint: Some("parent_email_key"), file: Some("nbtinsert.c"), line: Some(666), routine: Some("_bt_check_unique") })))
...
## 唯一约束:exec_without_returning
分支 = Exec
Display = Execution Error: error returned from database: duplicate key value violates unique constraint "parent_email_key" at line 666
sql_err = Some(UniqueConstraintViolation("duplicate key value violates unique constraint \"parent_email_key\""))
SQLSTATE = Some("23505")
## 外键:插入 child.parent_id = 999
分支 = Query
Display = Query Error: error returned from database: insert or update on table "child" violates foreign key constraint "child_parent_id_fkey" at line 2608
sql_err = Some(ForeignKeyConstraintViolation("insert or update on table \"child\" violates foreign key constraint \"child_parent_id_fkey\""))
SQLSTATE = Some("23503")
## 外键:删除被引用的 parent
分支 = Exec
Display = Execution Error: error returned from database: update or delete on table "parent" violates foreign key constraint "child_parent_id_fkey" on table "child" at line 2621
sql_err = Some(ForeignKeyConstraintViolation("update or delete on table \"parent\" violates foreign key constraint \"child_parent_id_fkey\" on table \"child\""))
SQLSTATE = Some("23503")
## 可延迟外键:insert 成功,错误留到 commit
## 可延迟外键:commit
分支 = Query
Display = Query Error: error returned from database: insert or update on table "dchild" violates foreign key constraint "dchild_parent_id_fkey" at line 2608
sql_err = Some(ForeignKeyConstraintViolation("insert or update on table \"dchild\" violates foreign key constraint \"dchild_parent_id_fkey\""))
SQLSTATE = Some("23503")
## CHECK 约束:age = -1
分支 = Query
Display = Query Error: error returned from database: new row for relation "parent" violates check constraint "parent_age_check" at line 2109
sql_err = None
SQLSTATE = Some("23514")
## 语法错误:execute_unprepared
分支 = Exec
Display = Execution Error: error returned from database: syntax error at or near "selec" at line 1244
sql_err = None
SQLSTATE = Some("42601")
## update 一个不存在的 id
分支 = RecordNotUpdated
Display = None of the records are updated
sql_err = None
SQLSTATE = None
## require_one 找不到
分支 = RecordNotFound
Display = RecordNotFound Error: None of the models match the query
sql_err = None
SQLSTATE = None
## delete 一个主键 NotSet 的 ActiveModel
分支 = PrimaryKeyNotSet
Display = Primay key not set for DeleteOne
sql_err = None
SQLSTATE = None
## Model::try_from(ActiveModel)
分支 = AttrNotSet
Display = Attribute email is NotSet
sql_err = None
SQLSTATE = None
## text 列读成 i32
分支 = Query
Display = Query Error: error occurred while decoding column "email": mismatched types; Rust type `core::option::Option<i32>` (as SQL type `INT4`) is not compatible with SQL type `TEXT`
sql_err = None
SQLSTATE = None
## 读一个不存在的列
分支 = Query
Display = Query Error: no column found for name: nope
sql_err = None
SQLSTATE = None
## Database::connect(postgres://lab:wrong-pw@192.168.64.106/lab)
分支 = Conn
Display = Connection Error: error returned from database: password authentication failed for user "lab" at line 331
sql_err = None
SQLSTATE = Some("28P01")
## Database::connect(postgres://lab:s3cret@192.168.64.106:99999/lab)
分支 = Conn
Display = Connection Error: The connection string 'postgres://lab:s3cret@192.168.64.106:99999/lab' cannot be parsed.
sql_err = None
SQLSTATE = None
(第一段 Debug 之后省掉的一行是本页动脑筋的答案。)
- 撞同一个唯一约束,
insert是Query,exec_without_returning是Exec;sql_err()两种都认。 - 可延迟外键的
insert本身成功,错误在commit时才出现,分支是Query,sql_err()照样认得出来。只在insert处判断冲突的代码会漏掉它。 - 主键没设、
WHERE没匹配到行、require_one没找到,都是 sea-orm 自己发现的,不经过数据库,没有 SQLSTATE。PrimaryKeyNotSet的Display里那个Primay是源码里的拼写。 QueryResult::try_get的解码错误是Query,文字是 sqlx 的;Option<i32>是因为try_get先按可空类型取(列与值)。- 密码错是
28P01,第一次就返回(不在 sqlx 的重试名单里),不像连接被拒那样等到超时;端口写成 99999 时url解析失败,错误文本带着整个连接串。
sql_err():按 SQLSTATE 认,不看文字
DbErr::sql_err 把错误归到 SqlErr 的两个分支之一,认不出来返回 None。Postgres 的部分:
pub fn sql_err(&self) -> Option<SqlErr> {
// ...
{
use std::ops::Deref;
if let DbErr::Exec(RuntimeErr::SqlxError(e)) | DbErr::Query(RuntimeErr::SqlxError(e)) =
self
&& let sqlx::Error::Database(e) = e.deref()
{
let error_code = e.code().unwrap_or_default();
let _error_code_expanded = error_code.deref();
// ... MySQL 分支:按错误号 1062、1452 等
#[cfg(feature = "sqlx-postgres")]
if e.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
.is_some()
{
match _error_code_expanded {
"23505" => {
return Some(SqlErr::UniqueConstraintViolation(e.message().into()));
}
"23503" => {
return Some(SqlErr::ForeignKeyConstraintViolation(e.message().into()));
}
_ => return None,
}
}
// ... SQLite 分支:按扩展错误码 1555、2067、787
}
}
// ... rusqlite
None
}
- 判断依据是 SQLSTATE:
23505(unique_violation)和23503(foreign_key_violation)。它不匹配消息文字,所以不受服务端语言设置(lc_messages)和 sqlx 0.9 加的at line N影响。MySQL 按错误号、SQLite 按扩展错误码,各自一张名单。 - 先用
try_downcast_ref::<PgDatabaseError>()确认是 Postgres 驱动的错误,才看 Postgres 的码;三个驱动同时编进来时不会串。 - 只看
Exec和Query。Conn、ConnectionAcquire和 sea-orm 自己的分支一律None。 SqlErr里只带e.message(),也就是duplicate key value violates unique constraint "parent_email_key"这一句,没有单独的约束名字段,也没有detail。- 只有这两种。
23514(CHECK)、23502(NOT NULL)、40001(序列化失败)、40P01(死锁)、57014(语句超时被取消)都是None,要自己取code()。
sql_err 的文档注释给了退回驱动错误的写法,和实验里的 sqlstate 函数是一回事:
if let Some(sql_err) = err.sql_err() {
// Portable across MySQL / Postgres / SQLite.
eprintln!("constraint violation: {sql_err}");
} else if let DbErr::Query(RuntimeErr::SqlxError(e)) | DbErr::Exec(RuntimeErr::SqlxError(e)) =
&err
&& let sea_orm::sqlx::Error::Database(db_err) = e.deref()
{
// Backend-specific: inspect the raw driver error.
eprintln!("SQLSTATE: {:?}", db_err.code());
}
sql_err 之前,大家在 match 里手写 "23505"issue #1194(「simplify specific db errors matching」)的动机一段是:「Currently, matching DB specific errors is highly verbose, and I believe some common errors could benefit from being simplified.」附的例子是一个四层的模式 Err(DbErr::Query(RuntimeErr::SqlxError(sqlx::Error::Database(e)))) if e.code().unwrap().eq("23505"),旁边注释「23505 is postgresql error code for violation on column unique constraint」。PR #1707(「Added enum for SQLx Error and basic functions for it」,首次发布于 0.12.0-rc.3,第一个正式版是 0.12.1)加了 SqlErr 和 sql_err(),说明里是「added a simpler SQL error enum in addition to the general DbErr」。合入时的实现和 2.0.4 的 Postgres 分支一样,就是按 "23505" / "23503" 匹配,相当于把 issue 里那段手写的 match 收进了库里;CHANGELOG 给的用法是 matches!(err.sql_err(), Some(SqlErr::UniqueConstraintViolation(_)))。2.0 唯一的变化是 SqlxError 进了 Arc,模式拆成了 if let ... && let ... 两段。
上面这段 sql_err 能告诉你「撞了唯一约束」,但一张表有主键和 email 两个唯一约束时,它不告诉你是哪一个。
问: 注册接口要区分「邮箱已注册」和「用户名已注册」,SqlErr::UniqueConstraintViolation 里的字符串能用吗?
答: 能用但不该用:那是服务端的 message 原文,约束名嵌在引号里,格式随语言设置变。约束名在 PgDatabaseError::constraint() 里有单独的字段(上面 Debug 输出里的 constraint: Some("parent_email_key")),sqlx 的 DatabaseError trait 也直接提供 constraint()。先用 sql_err() 判断是唯一冲突,再按上面文档注释的写法拿到 db_err,比较 db_err.constraint() == Some("parent_email_key")。约束名由建表语句决定(Postgres 默认 表_列_key),在迁移里显式命名,代码里比较的才是稳定的值。
Display 和 Debug 会带出什么
把 DbErr 直接 to_string() 返回给 HTTP 客户端,实验输出里能看到的东西:
| 来源 | 带出的内容 | 例子 |
|---|---|---|
Display,约束冲突 | 约束名、表名,即库结构 | unique constraint "parent_email_key"、on table "child" |
Display,Postgres 错误 | 服务端 C 源码行号(sqlx 0.9 加的) | at line 666 |
Display,CHECK / 语法 / 解码错误 | 表名、约束名、SQL 片段、列名和 Rust 类型 | near "selec"、decoding column "email" |
Display,连接错误 | 用户名;URL 解析失败和没有驱动时是整个连接串,包括密码 | for user "lab"、'postgres://lab:s3cret@...' |
Debug | 以上全部,加上 detail(冲突的那个值)、schema、file、routine | Key (email)=(alice@example.com) already exists. |
Display 不含 detail,所以用户输入的值通常不在 to_string() 里;但日志里常用的 {:?} 有。tracing 的 ?err 字段、println!("{err:?}")、unwrap() / expect() 的 panic 消息都走 Debug,用户的邮箱、手机号会原样进日志。返回给用户的应该是你自己按 sql_err() / SQLSTATE / 分支映射出来的文字,原始错误只进服务端日志,并且想清楚日志里能不能出现 detail。
at line N 是 sqlx 0.9 才有的,同样的两个错误在 sea-orm 1.1.20(sqlx 0.8.6,exp-seaorm-conn1-old 的 errors_old)下:
Display = Execution Error: error returned from database: duplicate key value violates unique constraint "parent_email_key"
sql_err = Some(UniqueConstraintViolation("duplicate key value violates unique constraint \"parent_email_key\""))
Display = Execution Error: error returned from database: syntax error at or near "selec"
Display = Connection Error: error returned from database: password authentication failed for user "lab"
sea-orm 自己的模板在两个版本之间没变,变的是 sqlx 那一段(sqlx 卷错误的「0.9 的「at line N」」)。拿 err.to_string() 和固定字符串比较的测试,升级到 2.0 后会断;这也是 DbErr 的 PartialEq 靠不住的原因之一。
DbErr 相等吗?接着上面实验的第一个错误 e(insert 撞唯一约束得到的 DbErr::Query(RuntimeErr::SqlxError(..))),造一个内容完全不同的错误:
let same_text = DbErr::Query(RuntimeErr::Internal(match &e {
DbErr::Query(inner) => inner.to_string(),
_ => unreachable!("insert with RETURNING goes through query_one"),
}));
println!(" 和 Query(Internal(同样的文字)) 相等?{}", e == same_text);
一个包着 sqlx 的 PgDatabaseError(有 SQLSTATE、detail、约束名),一个只包着一个 String。e == same_text 打印 true 还是 false?
动脑筋答案
被省掉的那一行:
和 Query(Internal(同样的文字)) 相等?true
true。impl PartialEq for DbErr 是 self.to_string() == other.to_string():两边都是 Query Error: 加上同一段文字,就相等,里面是 sqlx 的结构化错误还是一个字符串、SQLSTATE 是什么,一概不看。反过来,同一个约束冲突在 sqlx 0.8.6 和 0.9.0 下 Display 不同(多了 at line 666),就不相等;Exec 和 Query 前缀不同,也不相等。assert_eq!(err, DbErr::RecordNotUpdated) 这种和无数据分支比较的写法是可靠的,和驱动错误比较就只是在比字符串,应该改用 matches! 加 sql_err() 或 SQLSTATE。
小结
DbErr是一层扁平的 enum:Conn/Exec/Query包着RuntimeErr(2.0 起SqlxError在Arc里,DbErr因此能Clone),其余分支是 sea-orm 自己的检查,没有 SQLSTATE。- 一个 sqlx 错误包成哪个分支,取决于出错的调用:建池是
Conn,借连接超时是ConnectionAcquire,execute*是Exec,query_*和事务的BEGIN/COMMIT是Query;Postgres 上insert撞约束是Query。 sql_err()只认两种:SQLSTATE23505和23503,只看Exec/Query,带回的是消息原文;可延迟外键在commit时报错,同样认得出。CHECK、NOT NULL、死锁要自己取code(),区分是哪个约束用constraint()。Display带出约束名、表名、服务端行号,连接错误带出整个连接串和密码;Debug还带出冲突的值。不要把它们原样给用户。PartialEq比较的是Display文本:结构不同、文字相同就相等,sqlx 换个版本文字就变。