ActiveEnum 与 ValueType
主线位置:定义主线上「列的类型」这一格。
Model的每个字段要能变成 sea-query 的Value(写入主线绑参数时用),也要能从QueryResult取出来(查询主线FromQueryResult逐列TryGetable::try_get时用)。整数、字符串这些内建类型 sea-orm 自带实现(见 列与值);这一页讲三个 derive,它们给自己的类型补上这几个 trait:DeriveActiveEnum(Rust 枚举)、DeriveValueType(newtype 和能与字符串互转的结构体)、FromJsonQueryResult(存成 JSON 的结构体)。
flowchart TB
variant["Status::Open"] -->|to_value| kind{db_type}
kind -->|Enum| ev["Value::Enum"]
kind -->|"Text 等"| sv["Value::String"]
kind -->|Integer| iv["Value::Int"]
ev --> hint["$1::类型名"]
sv --> plain["$1"]
iv --> plain
col["enum 列"] -->|"SELECT 时"| cast["CAST 成 text"]
cast --> str[String]
str -->|try_from_value| back["Status 或 DbErr::Type"]
实验 crate 依赖 sea-orm =2.0.4(sqlx-postgres、runtime-tokio-rustls、macros、with-json、postgres-array、serde),平台 M2 Max、macOS、rustc 1.99.0、Postgres 16.14。看 SQL 用两种方式:QueryTrait::build(DbBackend::Postgres) 得到的 Statement(sql 带 $N,values 单独列出),以及开了 sqlx_logging 后 sqlx 每条语句一个的 sqlx::query 事件(实际发出的 SQL,不带参数值)。
ActiveEnum:一个 trait,三种存法
ActiveEnum 描述「这个 Rust 枚举在数据库里是什么」:
pub trait ActiveEnum: Sized + Iterable {
/// Define the Rust type that each enum variant corresponds.
type Value: ActiveEnumValue;
// ...
/// Get the name of enum
fn name() -> DynIden;
/// Convert enum variant into the corresponding value.
fn to_value(&self) -> Self::Value;
/// Try to convert the corresponding value into enum variant.
fn try_from_value(v: &Self::Value) -> Result<Self, DbErr>;
/// Get the database column definition of this active enum.
fn db_type() -> ColumnDef;
// ...
}
Value 是每个分支对应的那个值的类型,to_value / try_from_value 是一对双向映射,db_type 是列定义(建表、类型转换时用),name 是 Postgres enum 的类型名。DeriveActiveEnum 根据两个属性决定 Value 是什么(RsType::from_attr):
| 属性 | type Value | 数据库里 | 每个分支写 |
|---|---|---|---|
db_type = "Enum",不写 rs_type | sea_query::Enum(类型名 + 字符串) | Postgres enum | string_value / rename |
db_type = "Enum",rs_type = "String" | String | Postgres enum | 同上 |
rs_type = "String",db_type = "Text" 等 | String | 字符串列 | 同上 |
rs_type = "i32",db_type = "Integer" 等 | i32 | 整数列 | num_value 或 = 3 判别值 |
第二行是 1.x 的写法:1.1.20 的宏里 rs_type 是必填的(缺了报 Missing macro attribute \rs_type`),Postgres enum 写成 rs_type = "String", db_type = "Enum"。第一行是 2.0 新加的,rs_type可省,省了就生成sea_query::Enum值,宏在db_type是Enum时只接受rs_type为"String"、"Enum" 或不写。db_type = "Enum"不能配num_value(编译期报 `` db_type = "Enum"does not supportnum_value` or numeric discriminants ``)。
enum_name 不写时取 Rust 类型名的 UpperCamelCase(ActiveEnum::new 第一行 ident.to_string().to_upper_camel_case())。生成的 SQL 会给它加双引号,所以 enum Status 对应的是区分大小写的 "Status";用 CREATE TYPE status 建的类型名是小写的,必须显式写 enum_name = "status"。
实验用一张表,四个 Rust 类型(src/bin/enum_db.rs):
#[derive(Clone, Debug, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)]
#[sea_orm(db_type = "Enum", enum_name = "ticket_status")]
pub enum Status {
#[sea_orm(string_value = "open")]
Open,
#[sea_orm(string_value = "closed")]
Closed,
}
// 同一个 Postgres enum,1.x 写法
#[derive(Clone, Debug, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "ticket_status")]
pub enum StatusV1 { /* 同上 */ }
#[derive(Clone, Debug, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "Text")]
pub enum Kind {
#[sea_orm(string_value = "bug")]
Bug,
#[sea_orm(string_value = "feature")]
Feature,
}
#[derive(Clone, Debug, PartialEq, Eq, sea_orm::EnumIter, sea_orm::DeriveActiveEnum)]
#[sea_orm(rs_type = "i32", db_type = "Integer")]
pub enum Prio {
Low = 1,
High = 3,
}
CREATE TYPE ticket_status AS ENUM ('open', 'closed', 'archived');
CREATE TABLE ticket (id serial PRIMARY KEY, status ticket_status NOT NULL,
kind text NOT NULL DEFAULT 'bug', prio int NOT NULL DEFAULT 1);
ticket::Model 的 status、kind、prio 三列分别用 Status、Kind、Prio;ticket_v1::Model 映射同一张表,只有 id 和 status: StatusV1。
写出去:类型提示还是 CAST
先只构造不执行:
let am = ticket::ActiveModel { status: Set(Status::Open), kind: Set(Kind::Bug), prio: Set(Prio::High), ..Default::default() };
let stmt = ticket::Entity::insert(am.clone()).build(DbBackend::Postgres);
println!("--- built insert (2.0 Enum)\nsql = {}\nvalues = {:?}", stmt.sql, stmt.values);
// ticket_v1 的 insert、两个实体按 status 过滤的 select 同理
--- built insert (2.0 Enum)
sql = INSERT INTO "ticket" ("status", "kind", "prio") VALUES ($1::"ticket_status", $2, $3)
values = Some(Values([String(Some("open")), String(Some("bug")), Int(Some(3))]))
--- built insert (rs_type = String)
sql = INSERT INTO "ticket" ("status") VALUES (CAST($1 AS "ticket_status"))
values = Some(Values([String(Some("closed"))]))
--- built select
sql = SELECT "ticket"."id", CAST("ticket"."status" AS "text"), "ticket"."kind", "ticket"."prio" FROM "ticket" WHERE "ticket"."status" = $1::"ticket_status"
values = Some(Values([String(Some("open"))]))
--- built select (rs_type = String)
sql = SELECT "ticket"."id", CAST("ticket"."status" AS "text") FROM "ticket" WHERE "ticket"."status" = (CAST($1 AS "ticket_status"))
values = Some(Values([String(Some("open"))]))
两种 Postgres enum 写法发出去的参数都是一个字符串,区别只在 SQL 里怎么转型。
2.0 写法的 to_value 产出 sea_query::Enum { type_name: "ticket_status", value: "open" },From<Status> for Value 再包成 Value::Enum。到 sea-query 写 SQL 时,PostgresQueryBuilder 的 prepare_value 把它拆开:
fn prepare_value(&self, value: Value, sql: &mut impl SqlWriter) {
match value {
Value::Enum(value) => match value {
OptionEnum::Some(value) => {
let Enum { type_name, value } = *value;
sql.push_param(Value::String(Some(value.into_owned())), self as _);
self.write_enum_type_hint(type_name, sql);
}
OptionEnum::None(type_name) => {
sql.push_param(Value::String(None), self as _);
self.write_enum_type_hint(type_name, sql);
}
},
参数换成普通的 Value::String,类型名由 write_enum_type_hint 写成紧跟在占位符后面的 ::"ticket_status"。NULL 也带类型名(OptionEnum::None(type_name)),所以可空的 enum 列写 None 时,SQL 里同样是 $1::"ticket_status"。
1.x 写法的值是 String,本身不知道自己是 enum,转型由列来加:ColumnTrait::save_as 默认调 save_enum_as,列定义是 ColumnType::Enum 时把值包成 CAST(... AS "ticket_status")。save_enum_as 开头那个判断让两种写法不会叠加:
pub(crate) fn save_enum_as(col: Expr, enum_name: DynIden, col_type: &ColumnType) -> Expr {
if matches!(col, Expr::Value(Value::Enum(_))) {
return col;
}
值已经是 Value::Enum 就不再包 CAST。
不论哪种写法,参数在 sqlx 那里都是 Option<String>,类型是 TEXT,由 Postgres 在 SQL 里转成 enum。sea-orm 不靠 sqlx 的自定义类型绑定,也就没有 sqlx 绑定自定义 enum 时按名字查 OID 的那次来回(见 Postgres 类型)。实际发出的 insert 和结果:
INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\", …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\", \"kind\", \"prio\") VALUES ($1::\"ticket_status\", $2, $3) RETURNING \"id\", CAST(\"status\" AS \"text\"), \"kind\", \"prio\"\n" rows_affected=1 rows_returned=1 elapsed=3.731958ms elapsed_secs=0.003731958
Model { id: 1, status: Open, kind: Bug, prio: High }
INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"status\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"status\") VALUES (CAST($1 AS \"ticket_status\")) RETURNING \"id\", CAST(\"status\" AS \"text\")\n" rows_affected=1 rows_returned=1 elapsed=1.204041ms elapsed_secs=0.001204041
Model { id: 2, status: Closed }
读回来:CAST 成 text 再匹配
上面每条 SELECT 和 RETURNING 里 enum 列都写成了 CAST("status" AS "text")。这是 ColumnTrait::select_as 的默认实现,对 ColumnType::Enum 的列调 select_enum_as,数组列转成 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)
}
于是结果列是 text,宏生成的 TryGetable 先取一个 String,再交给 try_from_value(try_getable_impl 生成的代码)。try_from_value 是一个 match,匹配不上就报错(active_enum_impl):
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
))),
}
}
数据库里出现 Rust 枚举没有的值时会怎样:Postgres enum 加了一个 Rust 端还没有的 'archived',或者别的程序往字符串列、整数列写了别的值。enum_db.rs 的后半段直接 UPDATE 成这些值再读:
2.0 Enum : Err(Type("unexpected value for Status enum: archived"))
rs String: Err(Type("unexpected value for StatusV1 enum: archived"))
text col : Err(Type("unexpected value for Kind enum: question"))
int col : Err(Type("unexpected value for Prio enum: 2"))
四种存法报的是同一个 DbErr::Type。读一批行时也不会跳过坏的那一行:同一段程序里 ticket_v1::Entity::find().all(&db) 读 id 1(archived)和 id 2(closed),结果是整个查询失败:
all() over ids 1 (archived) and 2 (closed): Err(Type("unexpected value for StatusV1 enum: archived"))
所以给 Postgres enum 加值要先发 Rust 端。
问: 宏已经给 Status 实现了 sqlx 的 Type 和 Decode(sqlx_postgres_impl 里的 PgTypeInfo::with_name("ticket_status")),为什么 SELECT 还要多此一举 CAST 成 text?
答: 因为 CAST 是给所有存法共用的路径。rs_type = "String" 的 enum 取值时用的是 String 的 TryGetable,而 sqlx 的 String 只认 TEXT 这类内建类型,不认自定义 enum 的 OID,不 CAST 就解不出来(下一节 A 段就是这个错误:String (as SQL type TEXT) is not compatible with SQL type ticket_status)。2.0 为 db_type = "Enum" 生成的 TryGetable 会先试 QueryResult::try_get_from_sqlx_postgres:列类型与 Status 不兼容时返回 None,再退回 String 那条路。列被 CAST 成了 text,所以经由实体查询走的总是后一条。
前一条路要在列没有被 CAST 时才用得上,比如手写 SQL 再 try_get(src/bin/enum_mismatch.rs 的 D 段,id 1 是 open,id 2 是 archived):
let row = db
.query_one_raw(Statement::from_sql_and_values(DbBackend::Postgres, "SELECT status FROM ticket WHERE id = $1", [id.into()]))
.await?
.unwrap();
println!("id {id}: {:?}", row.try_get::<Status>("", "status"));
id 1: Ok(Open)
id 2: Err(Query(SqlxError(ColumnDecode { index: "\"status\"", source: "invalid value \"archived\" for enum Status" })))
同一个未知值,走 sqlx 的 Decode 报的是 ColumnDecode 和另一句话(invalid value ... for enum Status),不是 DbErr::Type。按错误类型处理时要两种都考虑到。这两个 sqlx impl 和 Value::Enum 都是 #3043「Support Value::Enum」加的,首次发布于 2.0.0-rc.38;1.1.20 的宏里没有任何 sqlx 相关的代码。
db_type 和真实的列对不上
DeriveActiveEnum 不连数据库,db_type、enum_name 写错了,编译照样通过,错误要到 SQL 发出去才出现,而且同一个错配在读和写上表现不一样。enum_mismatch.rs 用一个宏批量定义只有一列的实体,故意配错:
ticket_entity!(as_text, status, super::StatusAsText); // enum 列,声明成 db_type = "Text"
ticket_entity!(wrong_name, status, super::StatusWrongName); // enum 列,enum_name = "status"
ticket_entity!(enum_on_text, kind, super::Status); // text 列,声明成 db_type = "Enum"
A. enum 列声明成字符串(rs_type = "String", db_type = "Text")。sea-orm 认为这是普通字符串列:写不加转型,读不加 CAST。三种操作全失败,而且是三个不同的错误:
=== A. enum column declared 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=1.23475ms elapsed_secs=0.00123475
insert: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42804", message: "column \"status\" is of type ticket_status but expression is of type text", detail: None, hint: Some("You will need to rewrite or cast the expression."), position: Some(Original(41)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_target.c"), line: Some(586), routine: Some("transformAssignedExpr") }))))
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.716ms elapsed_secs=0.001716
filter: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42883", message: "operator does not exist: ticket_status = text", detail: None, hint: Some("No operator matches the given name and argument types. You might need to add explicit type casts."), position: Some(Original(79)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_oper.c"), line: Some(647), routine: Some("op_error") }))))
INFO sqlx::query: summary="SELECT pg_type.oid, pg_type.oid::regtype::text pretty_name, …" ...
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.193958ms elapsed_secs=0.006193958
select: Err(Query(SqlxError(ColumnDecode { index: "\"status\"", source: "mismatched types; Rust type `core::option::Option<alloc::string::String>` (as SQL type `TEXT`) is not compatible with SQL type `ticket_status`" })))
参数是 TEXT,Postgres 不会把 text 隐式转成 enum,所以插入报 42804、比较报 42883。读取时结果列是 ticket_status,sqlx 先发了一条 pg_type 查询把这个陌生的 OID 查成类型名(日志里被省略的那条,机制见 Postgres 类型 的「结果列:OID 反查」),再报类型不兼容。
B. enum 列,enum_name 写错(写成 status,真名是 ticket_status)。写和读不对称:
=== B. enum column, wrong enum_name
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=255.625µs elapsed_secs=0.000255625
insert: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42704", message: "type \"status\" does not exist", detail: None, hint: None, position: Some(Original(45)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_type.c"), line: Some(270), routine: Some("typenameType") }))))
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=511.542µs elapsed_secs=0.000511542
select: Ok(Some(Model { id: 1, status: Open }))
读只用 CAST(... AS "text"),不提类型名,所以照常工作;只有写入和按这一列过滤才会用到 enum_name。只读的服务里这种错可以一直潜伏到第一次写入。省略 enum_name、Rust 类型名和 Postgres 类型名大小写不一致时,也是这个 42704。
C. text 列声明成 Postgres enum,留给动脑筋。
kind 是 text 列,实体却把它声明成 Status(db_type = "Enum", enum_name = "ticket_status")。insert 一行 Status::Closed 和 filter(Column::Kind.eq(Status::Open)),各自成功还是失败?平台:M2 Max、macOS、rustc 1.99.0、Postgres 16.14。答案在页末。DeriveValueType:自己的类型当列用
newtype 是给一列加上业务含义最便宜的办法(UserId(i32) 不会和 PostId(i32) 混用)。DeriveValueType 有两种展开,DeriveValueType::new 按有没有 value_type 属性分:
| 写法 | 展开(impl_value_type / 字符串版) | 列类型 |
|---|---|---|
struct UserId(pub i32),无 value_type | 每个 trait 都转发给内层类型:Value::from(source.0)、<i32 as TryGetable>::try_get_by(..).map(UserId) | 内层类型的 column_type() |
#[sea_orm(value_type = "String")],任意结构体 | 写:to_str(&source)(默认 ToString::to_string);读:取 String 再 from_str(默认 FromStr::from_str) | String(StringLen::None),可用 column_type 改 |
两种都实现 From<T> for Value、TryGetable、ValueType、Nullable,2.0.0-rc.23 起还有 IntoActiveValue。不带 value_type 时只接受只有一个字段的元组结构体,否则报 You can only derive \DeriveValueType` on a struct with a single unnamed field, unless `value_type` is set.`。
src/bin/value_type_db.rs 把三种自定义类型放进一张表:
#[derive(Clone, Copy, Debug, PartialEq, Eq, DeriveValueType)]
pub struct UserId(pub i32);
#[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)]
#[sea_orm(value_type = "String")]
pub struct Email {
pub local: String,
pub domain: String,
}
// impl Display("local@domain")、impl FromStr(按 '@' 拆开,失败返回 BadEmail)
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct Prefs {
pub theme: String,
pub langs: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "app_user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: UserId,
pub email: Email,
#[sea_orm(column_type = "JsonBinary")]
pub prefs: Prefs,
}
Value::from(UserId(7)) = Int(Some(7))
Value::from(Email) = String(Some("a@b.io"))
Value::from(Prefs) = Json(Some(Object {"langs": Array [String("zh")], "theme": String("dark")}))
<user::PrimaryKey as PrimaryKeyTrait>::auto_increment() = true
find_by_id(UserId(1)): SELECT "app_user"."id", "app_user"."email", "app_user"."prefs" FROM "app_user" WHERE "app_user"."id" = $1
values = Some(Values([Int(Some(1))]))
INFO sqlx::query: summary="INSERT INTO \"app_user\" (\"email\", …" db.statement="\n\nINSERT INTO \"app_user\" (\"email\", \"prefs\") VALUES ($1, $2) RETURNING \"id\", \"email\", \"prefs\"\n" rows_affected=1 rows_returned=1 elapsed=3.418041ms elapsed_secs=0.003418041
inserted: Model { id: UserId(1), email: Email { local: "alice", domain: "example.com" }, prefs: Prefs { theme: "dark", langs: ["zh", "en"] } }
newtype 在 SQL 层完全看不见:UserId(7) 就是 Int(Some(7)),Email 就是一个字符串。主键 id: UserId 仍然是自增的,insert 时 NotSet,由 RETURNING "id" 拿回来再包成 UserId(1)。
当主键:TryFromU64
newtype 能当主键,靠的是 PrimaryKeyTrait::ValueType 的一串约束里的 TryFromU64,文档说它用来「Build a value of Self from a u64 last-insert-id returned by the database after an auto-increment INSERT」。宏只在内层类型是 i8 … u64 这八种整数之一时生成它(can_try_from_u64),实现是 n.try_into() 再包一层,溢出报 DbErr::TryIntoErr。
内层是 String 时没有这个实现,哪怕主键写了 auto_increment = false,也编译不过(src/bin/vt_pk_string.rs,pub struct Sku(pub String) 当主键):
error[E0277]: the trait bound `Sku: TryFromU64` is not satisfied
--> src/bin/vt_pk_string.rs:14:14
|
14 | pub sku: Sku,
| ^^^ unsatisfied trait bound
|
help: the trait `sea_orm::ActiveEnum` is not implemented for `Sku`
--> src/bin/vt_pk_string.rs:4:1
|
4 | pub struct Sku(pub String);
| ^^^^^^^^^^^^^^
= help: the following other types implement trait `TryFromU64`:
(T0, T1)
(T0, T1, T2)
(T0, T1, T2, T3)
(T0, T1, T2, T3, T4)
(T0, T1, T2, T3, T4, T5)
(T0, T1, T2, T3, T4, T5, T6)
(T0, T1, T2, T3, T4, T5, T6, T7)
(T0, T1, T2, T3, T4, T5, T6, T7, T8)
and 30 others
= note: required for `Sku` to implement `TryFromU64`
note: required by a bound in `sea_orm::PrimaryKeyTrait::ValueType`
...
help 里提到 ActiveEnum 是因为 impl<T: ActiveEnum> TryFromU64 for T 这个通用实现(它总是返回 ConvertFromU64 错误,让 enum 也能当非自增主键),rustc 把它当成了候选,和这里无关。TryFromU64 是和 auto_increment 无关的类型约束,要自己写一个 impl TryFromU64 for Sku(照 ActiveEnum 的做法返回 Err(DbErr::ConvertFromU64(..)))。#[sea_orm(try_from_u64)] 属性帮不了这里:它生成的同样是 n.try_into(),而 String 没有 TryFrom<u64>。
字符串版:Display / FromStr 的约束
value_type = "String" 版读取时把 from_str 的错误放进 DbErr::TryIntoErr 的 source: Arc<dyn Error + Send + Sync>,所以 FromStr::Err 必须实现 std::error::Error。写成最顺手的 type Err = String;(src/bin/vt_err_string.rs):
error[E0277]: the trait bound `std::string::String: std::error::Error` is not satisfied
--> src/bin/vt_err_string.rs:5:39
|
5 | #[derive(Clone, Debug, PartialEq, Eq, DeriveValueType)]
| ^^^^^^^^^^^^^^^ the trait `std::error::Error` is not implemented for `std::string::String`
|
= note: required for the cast from `Arc<std::string::String>` to `Arc<(dyn std::error::Error + Send + Sync + 'static)>`
= note: this error originates in the derive macro `DeriveValueType` (in Nightly builds, run with -Z macro-backtrace for more info)
错误指向 derive 那一行,不指向 FromStr。换成一个实现了 Error 的 BadEmail 之后,库里存了解析不了的字符串时:
email: Err(TryIntoErr { from: "String", into: "Email", source: BadEmail("not-an-email") })
写入方向没有校验:Value::from 只调 to_string,往库里写什么完全取决于你的 Display。
#2831(首次发布于 2.0.0-rc.20,release notes 记为「Allow more stringy newtypes in DeriveValueType」)之前,DeriveValueType::new 按结构体的形状分派:元组结构体一律走转发版,命名字段结构体和 enum 走字符串版,value_type = "String" 写了也不起作用。PR 起因是 #2830:struct NewType(Utf8PathBuf) 加了 value_type = "String" 仍然编译失败,报 the trait `From<Utf8PathBuf>` is not implemented for `sea_orm::Value`,因为转发版要求内层类型本身是值类型。issue 里的说法是「DeriveValueTypeStruct has different behavior than DeriveValueTypeString」,建议「if value_type = "String"」就不管形状都走字符串版。现在的 new 先读属性:「If either value_type is unset, or no attributes were passed, assume DeriveValueTypeStruct」,其余走字符串版。2.0.4 上同样的写法(struct FilePath(pub PathBuf),value_type = "String")能编译,Value::from(FilePath(PathBuf::from("/tmp/a.txt"))) 是 String(Some("/tmp/a.txt"))。
TryGetableFromJson:存成 JSON 的结构体
结构体整个存进一个 json / jsonb 列,用 FromJsonQueryResult。它的展开(expand_derive_from_json_query_result)只有几样:一个空的 impl TryGetableFromJson,From<T> for Value(serde_json::to_value 后包成 Value::Json,序列化失败直接 expect panic),ValueType(column_type() 是 Json)和 Nullable。读取不在宏里,在 sea-orm 的通用实现(impl<T: TryGetableFromJson> TryGetable for T,需要 with-json feature)。真正干活的是 TryGetableFromJson::try_get_from_json 的默认方法,Postgres 分支:
#[cfg(feature = "sqlx-postgres")]
QueryResultRow::SqlxPostgres(row) => row
.try_get::<Option<sqlx::types::Json<Self>>, _>(idx.as_sqlx_postgres_index())
.map_err(|e| sqlx_error_to_query_err(e).into())
.and_then(|opt| opt.ok_or_else(|| err_null_idx_col(idx)).map(|json| json.0)),
直接用 sqlx 的 Json<T> 解码成 Self,不经过中间的 serde_json::Value(sqlx 的 Json 怎么编解码见 类型的三个 trait 的「Json:用 serde 编解码」,serde 的反序列化过程见 serde 卷)。所以结构不对时,错误是 serde_json 的原话,带行列号,包在 sqlx 的 ColumnDecode 里:
prefs: Err(Query(SqlxError(ColumnDecode { index: "\"prefs\"", source: Error("invalid type: integer `1`, expected a string", line: 1, column: 11) })))
(库里的值改成了 {"theme": 1}。)一个类型不能同时是 ActiveEnum 和 TryGetableFromJson:TryGetable 对 TryGetableFromJson 有通用实现,DeriveActiveEnum 又生成自己的 TryGetable,源码注释的说法是「A type can only implement ActiveEnum or TryGetableFromJson, but not both」。2.0 起 HashMap<K, V> 和 BTreeMap<K, V> 自带 TryGetableFromJson(#3009),可以直接当 JSON 列的字段类型。
动脑筋答案
enum_mismatch.rs 的 C 段:
let r = enum_on_text::ActiveModel { kind: Set(Status::Closed), ..Default::default() }.insert(&db).await;
println!("insert: {r:?}");
let r = enum_on_text::Entity::find().filter(enum_on_text::Column::Kind.eq(Status::Open)).all(&db).await;
println!("filter: {r:?}");
=== C. text column declared db_type = Enum
INFO sqlx::query: summary="INSERT INTO \"ticket\" (\"kind\") …" db.statement="\n\nINSERT INTO \"ticket\" (\"kind\") VALUES ($1::\"ticket_status\") RETURNING \"id\", CAST(\"kind\" AS \"text\")\n" rows_affected=1 rows_returned=1 elapsed=1.123958ms elapsed_secs=0.001123958
insert: Ok(Model { id: 3, kind: Closed })
INFO sqlx::query: summary="SELECT \"ticket\".\"id\", CAST(\"ticket\".\"kind\" AS …" db.statement="\n\nSELECT \"ticket\".\"id\", CAST(\"ticket\".\"kind\" AS \"text\") FROM \"ticket\" WHERE \"ticket\".\"kind\" = $1::\"ticket_status\"\n" rows_affected=0 rows_returned=0 elapsed=716.334µs elapsed_secs=0.000716334
filter: Err(Query(SqlxError(Database(PgDatabaseError { severity: Error, code: "42883", message: "operator does not exist: text = ticket_status", detail: None, hint: Some("No operator matches the given name and argument types. You might need to add explicit type casts."), position: Some(Original(91)), where: None, schema: None, table: None, column: None, data_type: None, constraint: None, file: Some("parse_oper.c"), line: Some(647), routine: Some("op_error") }))))
插入成功,过滤失败。插入时 $1::"ticket_status" 先把参数转成 enum,再赋给 text 列:Postgres 允许任何类型经由 I/O 转换在赋值时转成字符串类型,所以写进去的是 'closed'。RETURNING 里的 CAST("kind" AS "text") 对 text 列是空操作,读回来也正常。比较不是赋值,没有这条转换,text = ticket_status 找不到运算符,报 42883。这个错配在写入和读取时都不报错,第一次按这一列过滤才暴露;如果那个 enum 类型根本不存在,插入也会像 B 段一样报 42704。
小结
DeriveActiveEnum有三种存法:Postgres enum(2.0 默认值类型是sea_query::Enum,也可以照 1.x 写rs_type = "String")、字符串列、整数列。enum_name默认取 Rust 类型名的 UpperCamelCase,SQL 里带双引号,区分大小写。- 发出去的参数永远是普通的
TEXT/ 整数:Value::Enum写成$1::"类型名",String值靠列的save_as写成CAST($1 AS "类型名")。实体查询读 enum 列时一律CAST(... AS "text")。 - 读到 Rust 枚举没有的值,实体查询报
DbErr::Type("unexpected value for X enum: v"),整个查询失败;手写 SQL 经 sqlxDecode时报ColumnDecode,文字也不同。 db_type/enum_name和真实列不符时编译能过,错误在运行时、而且读写不对称:enum 列声明成字符串,读写过滤都失败(42804、42883、mismatched types);enum_name写错,读正常、写入报42704;text 列声明成 enum,读写正常、过滤报42883。DeriveValueType:不写value_type时转发给内层类型(只接受单字段元组结构体),value_type = "String"时经Display/FromStr存成字符串,FromStr::Err必须实现Error。- newtype 当主键需要
TryFromU64,宏只为八种整数内层类型生成;String内层要自己实现,与auto_increment无关。 FromJsonQueryResult把结构体存进 JSON 列,读取直接用 sqlx 的Json<T>解码,结构不对时报 serde_json 的原文。