#[derive(Debug, thiserror::Error)]
pub enum StorageError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("corrupt data: {0}")]
CorruptData(String),
#[error("CRC32 mismatch: expected {expected:#010x}, got {actual:#010x}")]
CorruptCrc { expected: u32, actual: u32 },
#[error("WAL replay error: {0}")]
WalReplay(String),
#[error("catalog corrupt: {0}")]
CatalogCorrupt(String),
#[error("page corrupt: {0}")]
PageCorrupt(String),
#[error("invalid identifier: {0}")]
InvalidIdentifier(String),
#[error("row too large: {size} bytes exceeds max {max} bytes")]
RowTooLarge { size: usize, max: usize },
#[error("value too large: {size} bytes exceeds max {max} bytes")]
ValueTooLarge { size: usize, max: usize },
#[error("overflow chain corrupt: {0}")]
OverflowCorrupt(String),
#[error(
"cannot run {verb} inside an explicit transaction: DDL is not transactional in PowDB, commit or roll back first"
)]
DdlInTransaction { verb: &'static str },
#[error(
"cannot buffer more of this transaction: {pages} unflushed pages exceed the {limit_bytes} byte dirty-page budget, commit or roll back"
)]
TransactionTooLarge { pages: usize, limit_bytes: usize },
#[error("unique constraint violation on {table}.{column}")]
UniqueConstraintViolation { table: String, column: String },
#[error("unique expression index violation on {table} ({expression})")]
UniqueExpressionIndexViolation { table: String, expression: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageErrorKind {
Io,
CorruptData,
CorruptCrc,
WalReplay,
CatalogCorrupt,
PageCorrupt,
InvalidIdentifier,
RowTooLarge,
ValueTooLarge,
OverflowCorrupt,
DdlInTransaction,
TransactionTooLarge,
UniqueConstraintViolation,
UniqueExpressionIndexViolation,
}
impl StorageError {
pub fn kind(&self) -> StorageErrorKind {
match self {
Self::Io(_) => StorageErrorKind::Io,
Self::CorruptData(_) => StorageErrorKind::CorruptData,
Self::CorruptCrc { .. } => StorageErrorKind::CorruptCrc,
Self::WalReplay(_) => StorageErrorKind::WalReplay,
Self::CatalogCorrupt(_) => StorageErrorKind::CatalogCorrupt,
Self::PageCorrupt(_) => StorageErrorKind::PageCorrupt,
Self::InvalidIdentifier(_) => StorageErrorKind::InvalidIdentifier,
Self::RowTooLarge { .. } => StorageErrorKind::RowTooLarge,
Self::ValueTooLarge { .. } => StorageErrorKind::ValueTooLarge,
Self::OverflowCorrupt(_) => StorageErrorKind::OverflowCorrupt,
Self::DdlInTransaction { .. } => StorageErrorKind::DdlInTransaction,
Self::TransactionTooLarge { .. } => StorageErrorKind::TransactionTooLarge,
Self::UniqueConstraintViolation { .. } => StorageErrorKind::UniqueConstraintViolation,
Self::UniqueExpressionIndexViolation { .. } => {
StorageErrorKind::UniqueExpressionIndexViolation
}
}
}
pub fn kind_of_io_error(error: &std::io::Error) -> Option<StorageErrorKind> {
error
.get_ref()?
.downcast_ref::<StorageError>()
.map(StorageError::kind)
}
}
pub type Result<T> = std::result::Result<T, StorageError>;
impl From<StorageError> for std::io::Error {
fn from(e: StorageError) -> Self {
match e {
StorageError::Io(io) => io,
other => std::io::Error::other(other),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn one_of_every_variant() -> Vec<StorageError> {
vec![
StorageError::Io(std::io::Error::other("disk gone")),
StorageError::CorruptData("row 3".into()),
StorageError::CorruptCrc {
expected: 1,
actual: 2,
},
StorageError::WalReplay("truncated record".into()),
StorageError::CatalogCorrupt("bad magic".into()),
StorageError::PageCorrupt("slot past end".into()),
StorageError::InvalidIdentifier("a b".into()),
StorageError::RowTooLarge {
size: 8192,
max: 4070,
},
StorageError::ValueTooLarge { size: 1, max: 0 },
StorageError::OverflowCorrupt("chain length".into()),
StorageError::DdlInTransaction { verb: "drop" },
StorageError::TransactionTooLarge {
pages: 65_536,
limit_bytes: 268_435_456,
},
StorageError::UniqueConstraintViolation {
table: "User".into(),
column: "email".into(),
},
StorageError::UniqueExpressionIndexViolation {
table: "Doc".into(),
expression: ".data->code".into(),
},
]
}
#[test]
fn io_error_from_conversion_preserves_kind_and_text() {
for err in one_of_every_variant() {
let kind = err.kind();
let rendered = err.to_string();
let io: std::io::Error = err.into();
assert_eq!(
io.to_string(),
rendered,
"conversion must not change the rendered message"
);
if matches!(kind, StorageErrorKind::Io) {
continue; }
assert_eq!(
StorageError::kind_of_io_error(&io),
Some(kind),
"kind lost crossing the io::Error boundary for {rendered:?}"
);
}
}
#[test]
fn every_variant_has_a_distinct_kind() {
let mut seen: Vec<StorageErrorKind> = Vec::new();
for err in one_of_every_variant() {
let kind = err.kind();
assert!(
!seen.contains(&kind),
"{err:?} reuses the kind {kind:?} of an earlier variant"
);
seen.push(kind);
}
}
#[test]
fn kinds_survive_the_io_error_round_trip() {
for err in one_of_every_variant() {
let expected = err.kind();
let wrapped = std::io::Error::new(std::io::ErrorKind::InvalidInput, err);
assert_eq!(
StorageError::kind_of_io_error(&wrapped),
Some(expected),
"the kind was lost crossing an io::Error boundary"
);
}
}
#[test]
fn a_plain_io_error_has_no_storage_kind() {
let bare = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
assert_eq!(StorageError::kind_of_io_error(&bare), None);
let no_source = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
assert_eq!(StorageError::kind_of_io_error(&no_source), None);
}
#[test]
fn kinds_survive_the_io_error_the_engine_raises_them_through() {
let ddl = std::io::Error::new(
std::io::ErrorKind::InvalidInput,
StorageError::DdlInTransaction { verb: "drop" },
);
assert_eq!(
StorageError::kind_of_io_error(&ddl),
Some(StorageErrorKind::DdlInTransaction)
);
let too_large = std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
StorageError::TransactionTooLarge {
pages: 65_536,
limit_bytes: 268_435_456,
},
);
assert_eq!(
StorageError::kind_of_io_error(&too_large),
Some(StorageErrorKind::TransactionTooLarge)
);
}
}