fix(db): expand migration error display to walk source chain

tokio_postgres::Error's Display prints only the placeholder "db error"
for DB-side failures; the actual message (e.g. column already exists,
SQLSTATE, constraint name) lives in source() as DbError. Without
traversing the chain, startup logs showed the useless
"migration 001 failed: db error", making failures impossible to
diagnose.

MigrateError::Display now appends each source layer with ": " and
exposes source() so error-reporting layers that walk the chain
themselves also benefit.
This commit is contained in:
xfy 2026-07-04 21:47:24 +08:00
parent f3eb93f320
commit e9f0b032b7

View File

@ -92,16 +92,51 @@ impl From<tokio_postgres::Error> for MigrateError {
impl std::fmt::Display for MigrateError { impl std::fmt::Display for MigrateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
MigrateError::Pool(e) => write!(f, "database pool error: {}", e), MigrateError::Pool(e) => {
MigrateError::Query(e) => write!(f, "database query error: {}", e), write!(f, "database pool error: {}", e)?;
fmt_source_chain(f, e)
}
MigrateError::Query(e) => {
write!(f, "database query error: {}", e)?;
fmt_source_chain(f, e)
}
MigrateError::Apply { version, source } => { MigrateError::Apply { version, source } => {
write!(f, "migration {} failed: {}", version, source) write!(f, "migration {} failed: {}", version, source)?;
fmt_source_chain(f, source)
} }
} }
} }
} }
impl std::error::Error for MigrateError {} impl std::error::Error for MigrateError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MigrateError::Pool(e) => Some(e),
MigrateError::Query(e) => Some(e),
MigrateError::Apply { source, .. } => Some(source),
}
}
}
/// 把错误的 `source()` 链逐层拼到 Display 输出里,用 `: ` 分隔。
///
/// 存在的原因:`tokio_postgres::Error` 的 `Display` 对 DB 侧错误只会打印
/// 无信息量的 `db error`,真正的消息文本(如
/// `column "x" of relation "y" already exists`、SQLSTATE、约束名藏在
/// `source()` 链里的 `postgres::error::DbError`。不主动遍历链就会丢失全部上下文,
/// 日志只显示 `migration 001 failed: db error`,难以定位。
fn fmt_source_chain(
f: &mut std::fmt::Formatter<'_>,
mut source: &(dyn std::error::Error + 'static),
) -> std::fmt::Result {
while let Some(next) = source.source() {
// `DbError` 的 Display 已经包含 message必要时还有 detail/hint
// 直接拼上即可;避免逐层重复打印 `db error` 这类占位串。
write!(f, ": {next}")?;
source = next;
}
Ok(())
}
/// 在**已获取**的连接上执行迁移主体逻辑(咨询锁 + 建表 + 应用迁移 + 解锁)。 /// 在**已获取**的连接上执行迁移主体逻辑(咨询锁 + 建表 + 应用迁移 + 解锁)。
/// ///