Rust in Everything
基于 sea-orm 2.0.4

错误

主线位置:三条主线的失败出口。查询主线上 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);pingConn
每条语句前的 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 能走到的)
ConnectionAcquiresqlx_conn_acquire_err:PoolTimedOut → Timeout,PoolClosed → ConnectionClosed(ConnAcquireErr 的 Display 分别是 Connection pool timed out、Connection closed)
Conn建池、ping、acquire 的其他错误;conn_err:URL 解析失败、没有驱动、Disconnected
Execexecute_raw、execute_unprepared、SET TRANSACTION
Queryquery_*_raw、stream、BEGIN / COMMIT / ROLLBACK、QueryResult::try_get 的 sqlx 解码错误;query_err:Loader 的关系检查
Typetype_err:TryGetError::Null 转过来的 A null value was encountered while decoding ...、ActiveEnum 读到未知值、宏生成的 Missing value for column '...'(见列与值)、ActiveModel 的 set_parent_key / get_parent_key 一类方法发现关系类型不对
RecordNotFoundrequire_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 没匹配到行,见更新与删除
PrimaryKeyNotSetUpdate::one / Delete::one 收到主键 NotSet 的 ActiveModel(ctx 是 UpdateOne / DeleteOne);1.x 这里是 panic
UpdateGetPrimaryKey更新后取不到主键值,executor/update.rs 一处
AttrNotSetModel::try_from(ActiveModel)(宏生成)、ActiveModelEx 的宏、query/util.rs
KeyArityMismatch游标分页的键列数和值个数对不上(executor/cursor.rs)
UnpackInsertId插入后从返回行或 last_insert_id 里拼不出主键
TryIntoErrTryFromU64 的整数转换(u64 转 i32 溢出);SQLite 的 f64 转 Decimal
ConvertFromU64非整数主键类型的 TryFromU64(字符串、UUID、元组、ActiveEnum、TextUuid)
JsonActiveModel::from_json 收到的不是 JSON 对象、TryGetableFromJson
BackendNotSupported不支持 RETURNING 的后端(默认 feature 下的 MySQL、SQLite)上调 exec_with_returning 一类方法、部分 stream、sea-orm-migration 的部分方法
RbacError / AccessDeniedRestrictedConnection(rbac feature):*_raw 一律拒绝、权限检查不通过
Migration只有 sea-orm-migration 里 MigrationTrait::down 的默认实现:We Don't Do That Here
Customsea-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());
}
📝NOTE
冷知识: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 两个唯一约束时,它不告诉你是哪一个。

📝NOTE
没有蠢问题

问: 注册接口要区分「邮箱已注册」和「用户名已注册」,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、routineKey (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 靠不住的原因之一。

❗IMPORTANT
动脑筋:两个 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() 只认两种:SQLSTATE 23505 和 23503,只看 Exec / Query,带回的是消息原文;可延迟外键在 commit 时报错,同样认得出。CHECK、NOT NULL、死锁要自己取 code(),区分是哪个约束用 constraint()。
  • Display 带出约束名、表名、服务端行号,连接错误带出整个连接串和密码;Debug 还带出冲突的值。不要把它们原样给用户。
  • PartialEq 比较的是 Display 文本:结构不同、文字相同就相等,sqlx 换个版本文字就变。

💬 关联讨论

发起讨论
加载中...