Rust in Everything
基于 sqlx 0.9.0

unprintable error: (unprintable type)

主线位置:查询主线的最后一步,FromRow 把 PgRow 变成 Rust 值的时候。#[derive(FromRow)] 怎么展开、__spec_error! 的五层实现,见Row 与 FromRow;本页讲怎么认出它、怎么绕开。

flowchart TB
  get["try_get 中间类型"] --> conv["TryFrom 转换"]
  conv -->|失败| spec["__spec_error!"]
  spec --> lost["固定字符串,原因丢失"]
  fix1["修法 A:字段用中间类型"] --> own["自己转换,错误原样"]
  fix2["修法 B:newtype 实现 Decode"] --> cd["ColumnDecode 带原错误"]

本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0";换成 =0.8.6 输出逐字相同。

现象

字段上写了 #[sqlx(try_from = "...")],数据库里的值转不过来时,错误只说哪一列,原因永远是同一句话:

error occurred while decoding column qty: unprintable error: (unprintable type)

最小复现

src/bin/try_from.rs,一个用标准库的 TryFromIntError,一个用自己写的、Display 和 Error 都实现了的错误类型:

#[derive(Debug, FromRow)]
struct Item {
  #[sqlx(try_from = "i64")]
  qty: u8,
}

#[derive(Debug)]
struct PercentError(i64);

impl std::fmt::Display for PercentError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    write!(f, "{} is not a percentage (0..=100)", self.0)
  }
}

impl std::error::Error for PercentError {}

impl TryFrom<i32> for Percent {
  type Error = PercentError;
  // 0..=100 之外返回 Err(PercentError(v))
}

#[derive(Debug, FromRow)]
struct Score {
  #[sqlx(try_from = "i32")]
  pct: Percent,
}

show("Item  300", sqlx::query_as::<_, Item>("select 300::int8 as qty").fetch_one(&mut conn).await);
show("Score 250", sqlx::query_as::<_, Score>("select 250::int4 as pct").fetch_one(&mut conn).await);

show 打印错误的 Display,是 ColumnDecode 时再打印 source 的 Debug:

Item  300: error occurred while decoding column qty: unprintable error: (unprintable type)
Item  300: source = "unprintable error: (unprintable type)"
Score 250: error occurred while decoding column pct: unprintable error: (unprintable type)
Score 250: source = "unprintable error: (unprintable type)"

source 里就只是这个字符串,TryFromIntError 和 PercentError 都没有被保留。

原因

try_from 字段展开成先 try_get 中间类型、再 TryFrom,失败时包成 ColumnDecode,source 由 sqlx::__spec_error!(e) 生成(row.rs)。__spec_error! 本该按「实现了 Error 就装箱、否则 Display、否则 Debug、否则类型名」挑一个实现,可它在 SpecErrorWrapper(e) 本身上调方法,自动引用一层就匹配上了没有任何约束的那个实现:

impl<E> SpecError<E> for SpecErrorWrapper<E> {
    fn __sqlx_spec_error(
        &self,
    ) -> fn(SpecErrorWrapper<E>) -> Box<dyn Error + Send + Sync + 'static> {
        |_e| "unprintable error: (unprintable type)".into()
    }
}

所以不管错误类型实现了什么,结果都一样。逐层的分析和「从 &&&& 开始调就对了」的对照实验在Row 与 FromRow。flatten 加 try_from 的组合走同一个宏,同样受影响。

这是 sqlx 的 bug,0.8.6 和 0.9.0 都能复现。用 gh search issues -R transact-rs/sqlx 搜 unprintable error、unprintable type、spec_error、FromRow try_from、try_from error message,都没有结果,未找到上游 issue;本地仓库的 origin/main(到 2026-10-23)里 src/spec_error.rs 在 v0.9.0 之后没有改动。

修法

修法 A:字段用数据库那一侧的类型,FromRow 之后自己转。 错误在自己手里,想怎么报就怎么报:

#[derive(Debug, FromRow)]
struct RawItem {
  qty: i64,
}

let raw = sqlx::query_as::<_, RawItem>("select 300::int8 as qty").fetch_one(&mut conn).await?;
match u8::try_from(raw.qty) {
  Ok(q) => println!("修法 A: Ok({q})"),
  Err(e) => println!("修法 A: qty = {}: {e}", raw.qty),
}

修法 B:newtype 自己实现 Type 和 Decode。 Decode::decode 返回的 BoxDynError 会原样成为 ColumnDecode 的 source:

struct Qty(u8);

impl Type<Postgres> for Qty {
  fn type_info() -> PgTypeInfo {
    <i64 as Type<Postgres>>::type_info()
  }
}

impl<'r> Decode<'r, Postgres> for Qty {
  fn decode(value: PgValueRef<'r>) -> Result<Self, BoxDynError> {
    let v = <i64 as Decode<Postgres>>::decode(value)?;
    Ok(Qty(u8::try_from(v)?))
  }
}

#[derive(Debug, FromRow)]
struct ItemB {
  qty: Qty,
}
修法 A: qty = 300: number too large to fit in target type
修法 B 300: error occurred while decoding column "qty": number too large to fit in target type
修法 B 300: source = TryFromIntError(PosOverflow)
修法 B  42: Ok(ItemB { qty: Qty(42) })

修法 B 的 source 就是原来的 TryFromIntError。列名这次带了引号(column "qty"),因为这条错误走的是 try_get 的 {index:?},不是 try_from 分支里的 #id_s.to_string()。Type 这里只实现了 type_info;想和 i64 接受完全一样的列,把 compatible 也转发给 <i64 as Type<Postgres>>::compatible。Type / Decode 各管什么见类型:Type、Encode、Decode。

怎么确认自己没中招

  • grep -rn 'try_from *=' src/:每一处 #[sqlx(try_from = ...)] 失败时都只会报这句话。转换几乎不会失败的(i64 转 u64 的主键)可以留着;会失败、需要知道原因的,换成上面两种写法。
  • 日志或告警里出现 unprintable error: (unprintable type),原因不在错误里,只能按列名回去查数据。

小结

  • #[sqlx(try_from)] 转换失败时报 ColumnDecode,source 永远是字符串 unprintable error: (unprintable type),原始错误被丢掉。
  • 原因是 __spec_error! 在 SpecErrorWrapper 本身上调方法,自动引用落到了无约束的兜底实现;0.8.6 和 0.9.0 都是这样,未找到上游 issue。
  • 修法:字段用中间类型、FromRow 之后自己转;或者给 newtype 实现 Type + Decode,错误原样进 source。

💬 关联讨论

发起讨论
加载中...