Rust in Everything
基于 sea-orm 2.0.4

column "status" is of type ticket_status but expression is of type text

主线位置:定义主线上的 DeriveActiveEnum。它在编译期把 db_type 和 enum_name 写进生成的代码,决定写入时加不加 ::"类型名"、读取时加不加 CAST(... AS "text");宏不连数据库,写错了照样编译通过。机制和每种写法发出的 SQL 见 ActiveEnum 与 ValueType,这一页只讲对不上时的几种报错和怎么查。

flowchart TB
  decl["DeriveActiveEnum 声明"] --> q{"和库里一致吗"}
  q -->|"db_type = Text"| a["写和读都报错"]
  q -->|"enum_name 写错"| b["写 42704, 读正常"]
  q -->|"库里多一个标签"| d["unexpected value"]
  q -->|一致| ok["写带类型名, 读 CAST"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sea-orm = "=2.0.4",对照组 =1.1.20。表:

CREATE TYPE ticket_status AS ENUM ('open', 'closed', 'archived');
CREATE TABLE ticket (id serial PRIMARY KEY, status ticket_status NOT NULL DEFAULT 'open');
INSERT INTO ticket (status) VALUES ('open');

现象

src/bin/enum_mismatch.rs 用一个宏定义三个只有 status 一列的实体,分别配三个 Rust 枚举(每个都有 Open / Closed 两个分支,string_value 是 open / closed):

#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum StatusAsText { /* ... */ }          // A. 库里是 enum,声明成 Text

#[sea_orm(db_type = "Enum", enum_name = "status")]
pub enum StatusWrongName { /* ... */ }       // B. 类型名写错

#[sea_orm(db_type = "Enum", enum_name = "ticket_status")]
pub enum Status { /* ... */ }                // C / D. 正确,但库里多一个 'archived'

A 的输出(sqlx 日志删掉了 sqlx 为陌生 OID 发的那条 pg_type 查询):

=== A. db_type = "Text"
 INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\") VALUES ($1) RETURNING \"id\", \"status\"\n" rows_affected=0 rows_returned=0 elapsed=2.04875ms elapsed_secs=0.00204875
insert: Query Error: error returned from database: column "status" is of type ticket_status but expression is of type text at line 586
 INFO sqlx::query: summary="SELECT \"ticket\".\"id\", \"ticket\".\"status\" FROM …" db.statement="\n\nSELECT \"ticket\".\"id\", \"ticket\".\"status\" FROM \"ticket\" WHERE \"ticket\".\"status\" = $1\n" rows_affected=0 rows_returned=0 elapsed=1.291ms elapsed_secs=0.001291
filter: Query Error: error returned from database: operator does not exist: ticket_status = text at line 647
...
 INFO sqlx::query: summary="SELECT \"ticket\".\"id\", \"ticket\".\"status\" FROM …" db.statement="\n\nSELECT \"ticket\".\"id\", \"ticket\".\"status\" FROM \"ticket\" WHERE \"ticket\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=6.639375ms elapsed_secs=0.006639375
select: Query Error: error occurred while decoding column "status": mismatched types; Rust type `core::option::Option<alloc::string::String>` (as SQL type `TEXT`) is not compatible with SQL type `ticket_status`

sea-orm 把这一列当成普通字符串列:写不加转型,读不加 CAST。参数是 TEXT,Postgres 不会隐式把 text 转成 enum,于是插入报 42804、比较报 42883;读的时候结果列是 ticket_status,sqlx 的类型检查拒绝把它读成 String。

B 和 D:

=== B. enum_name = "status"
 INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\") VALUES ($1::\"status\") RETURNING \"id\", CAST(\"status\" AS \"text\")\n" rows_affected=0 rows_returned=0 elapsed=579.833µs elapsed_secs=0.000579833
insert: Query Error: error returned from database: type "status" does not exist at line 270
 INFO sqlx::query: summary="SELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS …" db.statement="\n\nSELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS \"text\") FROM \"ticket\" WHERE \"ticket\".\"id\" = $1 LIMIT $2\n" rows_affected=1 rows_returned=1 elapsed=1.064708ms elapsed_secs=0.001064708
select: Ok(Some(Model { id: 1, status: Open }))
...
=== D. a label Rust does not know
 INFO sqlx::query: summary="INSERT INTO ticket (status) …" db.statement="\n\nINSERT INTO ticket (status) VALUES ('archived')\n" rows_affected=1 rows_returned=0 elapsed=849.208µs elapsed_secs=0.000849208
 INFO sqlx::query: summary="SELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS …" db.statement="\n\nSELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS \"text\") FROM \"ticket\"\n" rows_affected=3 rows_returned=3 elapsed=1.3165ms elapsed_secs=0.0013165
all   : Type Error: unexpected value for Status enum: archived
  • B:写入用 enum_name 生成 $1::"status",Postgres 找不到这个类型,报 42704。读取只写 CAST(... AS "text"),不提类型名,照常成功。
  • D:映射正确,但库里的枚举比 Rust 多一个 archived。all() 返回三行,只要有一行是 archived,整个结果就是一个错误,前两行也拿不到。

出处

写入一侧,2.0 写法的值是 Value::Enum,带着 enum_name,sea-query 的 PostgresQueryBuilder 把它写成 $N::"类型名";声明成 db_type = "Text" 时值是普通的 Value::String,列的 save_enum_as 也只在列类型是 Enum 时才加转型,于是什么都不加。读取一侧,select_enum_as 对 enum 列一律 CAST 成 text,用不到类型名:

pub(crate) fn select_enum_as(col: Expr, _: DynIden, col_type: &ColumnType) -> Expr {
    let type_name = match col_type {
        ColumnType::Array(_) => TextArray.into_iden(),
        _ => Text.into_iden(),
    };
    col.as_enum(type_name)
}

第二个参数(enum 名)是 _,这就是 B 读得出来的原因。读回来的文本交给宏生成的 try_from_value 逐个比较 string_value,没有匹配的分支就是 D 的那句:

                fn try_from_value(v: &<Self as sea_orm::ActiveEnum>::Value) -> std::result::Result<Self, sea_orm::DbErr> {
                    match #val {
                        #( #variant_values => Ok(Self::#variant_idents), )*
                        _ => Err(sea_orm::DbErr::Type(format!(
                            "unexpected value for {} enum: {}",
                            stringify!(#ident),
                            #val
                        ))),
                    }
                }

Selector::all 把每一行 map 成 S::from_raw_query_result(row) 再 try_collect(),任何一行出错整个结果就是那个错误,没有「跳过这一行」的选项。

📝NOTE
没有蠢问题

问: 线上一直在读这张表,从来没报过错,是不是说明 DeriveActiveEnum 写对了?

答: 不能说明。读取只用 CAST(... AS "text") 和 string_value,enum_name 写错(B)、大小写和库里不一致,读都照样成功;只有写入和按这一列过滤会用到类型名。只读的服务可以一直带着这个错,直到第一次写入。反过来,db_type = "Text"(A)读也会失败,上线前就能发现。要确认,得像下面那样直接和库里的类型比。

本站用的 1.1.20 结果一样,只是 2.0 写法在 1.1.20 里还要求 rs_type = "String",发出的转型是 CAST($1 AS "...") 而不是 $1::"..."(exp-seaorm-pit1-old/src/bin/enum_old.rs,只留报错和 B 的插入语句):

=== A. db_type = "Text"
...
insert: Query Error: error returned from database: column "status" is of type ticket_status but expression is of type text
...
filter: Query Error: error returned from database: operator does not exist: ticket_status = text
...
select: Query Error: error occurred while decoding column "status": mismatched types; Rust type `core::option::Option<alloc::string::String>` (as SQL type `TEXT`) is not compatible with SQL type `ticket_status`
=== B. enum_name = "status"
 INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\") VALUES (CAST($1 AS \"status\")) RETURNING \"id\", CAST(\"status\" AS \"text\")\n" rows_affected=0 rows_returned=0 elapsed=556.166µs elapsed_secs=0.000556166
insert: Query Error: error returned from database: type "status" does not exist
...
select: Ok(Some(Model { id: 1, status: Open }))
...
all   : Type Error: unexpected value for Status enum: archived

在 SeaQL/sea-orm 的 issue 里按 but expression is of type text、unexpected value for enum、enum_name type does not exist 搜,没有找到报这几种错配的(有一个 #2829 报的是生成实体时多了 column_type = "Text",报告人后来说是用了旧版 sea-orm-cli)。这是声明和库对不上,不是 sea-orm 的 bug。

修法

db_type = "Enum",enum_name 写库里的类型名(pg_type.typname,区分大小写),每个 string_value 对应一个标签。同一个程序的 C 段:

=== C. db_type = "Enum", enum_name = "ticket_status"
 INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\") VALUES ($1::\"ticket_status\") RETURNING \"id\", CAST(\"status\" AS \"text\")\n" rows_affected=1 rows_returned=1 elapsed=1.710667ms elapsed_secs=0.001710667
insert: Ok(Model { id: 2, status: Closed })
 INFO sqlx::query: summary="SELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS …" db.statement="\n\nSELECT \"ticket\".\"id\", CAST(\"ticket\".\"status\" AS \"text\") FROM \"ticket\" WHERE \"ticket\".\"status\" = $1::\"ticket_status\"\n" rows_affected=1 rows_returned=1 elapsed=927.834µs elapsed_secs=0.000927834
filter: Ok([Model { id: 1, status: Open }])

D 没有声明上的修法:库里加了标签(ALTER TYPE ... ADD VALUE),Rust 枚举就要先加上对应的分支,而且要在新标签第一次写进库之前部署。反过来删标签前,先确认库里已经没有这个值。

怎么确认没中招

拿实体的声明和库里的列直接比。src/bin/enum_check.rs 查 ticket.status 的类型名和标签,和 ActiveEnum::name()、Status::iter() 的值对照:

  let rust: Vec<String> = Status::iter().map(|v| v.to_value().value.into_owned()).collect();
  println!("column type : {udt}  enum_name: {}", Status::name().to_string());
  println!("db labels   : {labels:?}");
  println!("rust values : {rust:?}");
  println!("db only     : {:?}", labels.iter().filter(|l| !rust.contains(l)).collect::<Vec<_>>());
column type : ticket_status  enum_name: ticket_status
db labels   : ["open", "closed", "archived"]
rust values : ["open", "closed"]
db only     : ["archived"]

库里的类型名来自 information_schema.columns.udt_name,标签来自 pg_enum(按列所在的 schema 解析类型,否则别的 schema 里的同名类型会混进来)。类型名不同就是 B,udt_name 不是枚举类型而 Rust 用了 ActiveEnum、或者反过来,就是 A 一类;db only 不为空就是 D 的前兆。把它写成集成测试,对每个 enum 列跑一遍。

本站按固定提交 89a61b1 在 crates/ 下没有 DeriveActiveEnum,状态类的列(例如 moderation_queue.status)在实体里是 String、按字符串比较,不受这一页影响。

小结

  • DeriveActiveEnum 不连库,db_type / enum_name 写错编译照样通过。
  • 库里是 enum、声明成 Text:插入 42804 column "status" is of type ticket_status but expression is of type text,过滤 42883 operator does not exist: ticket_status = text,读取 mismatched types。
  • enum_name 写错:写入 42704 type "status" does not exist,读取正常,因为读只用 CAST(... AS "text")。
  • 库里多一个 Rust 不认识的标签:unexpected value for Status enum: archived,整个 all() 失败。
  • 2.0.4 和 1.1.20 一样;修法是照库里的类型名和标签写声明,并用 udt_name / pg_enum 做一次对照测试。

💬 关联讨论

发起讨论
加载中...