use crate::{
storage::StorageError,
superfile::{BuildError as SuperfileBuildError, ReadError as SuperfileReadError},
supertable::{
error::{
BuildError as SupertableBuildError, CommitError as SupertableCommitError, OpenError,
QueryError,
},
manifest::ManifestLoadError,
mutations::{CommitError as MutationCommitError, MutationError},
},
};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum InfinoError {
#[error("not found: {0}")]
NotFound(String),
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("schema: {0}")]
Schema(String),
#[error("cardinality: {0}")]
Cardinality(String),
#[error("io: {0}")]
Io(String),
#[error("query: {0}")]
Query(String),
#[error("over budget: {0}")]
OverBudget(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("backend: {0}")]
Backend(String),
#[error("config: {0}")]
Config(String),
}
impl InfinoError {
pub(crate) fn with_context(self, operation: &'static str, table: Option<&str>) -> Self {
let prefix = match table {
Some(t) => format!("{operation}({t})"),
None => operation.to_string(),
};
match self {
Self::NotFound(m) => Self::NotFound(format!("{prefix}: {m}")),
Self::AlreadyExists(m) => Self::AlreadyExists(format!("{prefix}: {m}")),
Self::Schema(m) => Self::Schema(format!("{prefix}: {m}")),
Self::Cardinality(m) => Self::Cardinality(format!("{prefix}: {m}")),
Self::Io(m) => Self::Io(format!("{prefix}: {m}")),
Self::Query(m) => Self::Query(format!("{prefix}: {m}")),
Self::OverBudget(m) => Self::OverBudget(format!("{prefix}: {m}")),
Self::Conflict(m) => Self::Conflict(format!("{prefix}: {m}")),
Self::Backend(m) => Self::Backend(format!("{prefix}: {m}")),
Self::Config(m) => Self::Config(format!("{prefix}: {m}")),
}
}
}
impl From<StorageError> for InfinoError {
fn from(e: StorageError) -> Self {
let msg = e.to_string();
match e {
StorageError::NotFound { .. } => InfinoError::NotFound(msg),
StorageError::PreconditionFailed { .. } => InfinoError::Conflict(msg),
StorageError::TransientExhausted { .. } | StorageError::Permanent { .. } => {
InfinoError::Io(msg)
}
}
}
}
impl From<QueryError> for InfinoError {
fn from(e: QueryError) -> Self {
if let Some(msg) = e.over_budget() {
return InfinoError::OverBudget(msg.to_string());
}
InfinoError::Query(e.to_string())
}
}
impl From<ManifestLoadError> for InfinoError {
fn from(e: ManifestLoadError) -> Self {
let msg = e.to_string();
match e {
ManifestLoadError::PointerVanished => InfinoError::NotFound(msg),
ManifestLoadError::Storage(_) => InfinoError::Io(msg),
_ => InfinoError::Backend(msg),
}
}
}
impl From<SuperfileReadError> for InfinoError {
fn from(e: SuperfileReadError) -> Self {
if let Some(msg) = e.over_budget() {
return InfinoError::OverBudget(msg.to_string());
}
InfinoError::Query(e.to_string())
}
}
impl From<SuperfileBuildError> for InfinoError {
fn from(e: SuperfileBuildError) -> Self {
InfinoError::Schema(e.to_string())
}
}
impl From<SupertableBuildError> for InfinoError {
fn from(e: SupertableBuildError) -> Self {
if let Some(msg) = e.over_budget() {
return InfinoError::OverBudget(msg.to_string());
}
if e.is_conflict() {
return InfinoError::Conflict(e.to_string());
}
if matches!(e, SupertableBuildError::TableGone) {
return InfinoError::NotFound(e.to_string());
}
InfinoError::Schema(e.to_string())
}
}
impl From<SupertableCommitError> for InfinoError {
fn from(e: SupertableCommitError) -> Self {
let msg = e.to_string();
match e {
SupertableCommitError::PointerVanished => InfinoError::NotFound(msg),
e if e.is_conflict() => InfinoError::Conflict(msg),
_ => InfinoError::Backend(msg),
}
}
}
impl From<OpenError> for InfinoError {
fn from(e: OpenError) -> Self {
if e.is_conflict() {
return InfinoError::Conflict(e.to_string());
}
InfinoError::Backend(e.to_string())
}
}
impl From<MutationError> for InfinoError {
fn from(e: MutationError) -> Self {
let msg = e.to_string();
if e.is_conflict() {
return InfinoError::Conflict(msg);
}
match e {
MutationError::PredicateEval(q) => InfinoError::from(q),
MutationError::Storage(s) => InfinoError::from(s),
MutationError::CardinalityMismatch { .. }
| MutationError::MatchCountExceedsCap { .. } => InfinoError::Cardinality(msg),
MutationError::SchemaMismatch(_) => InfinoError::Schema(msg),
MutationError::TableGone => InfinoError::NotFound(msg),
_ => InfinoError::Backend(msg),
}
}
}
impl From<MutationCommitError> for InfinoError {
fn from(e: MutationCommitError) -> Self {
if let Some(msg) = e.over_budget() {
return InfinoError::OverBudget(msg.to_string());
}
if e.is_conflict() {
return InfinoError::Conflict(e.to_string());
}
if matches!(
&e,
MutationCommitError::AppendFlush(SupertableBuildError::TableGone)
) {
return InfinoError::NotFound(e.to_string());
}
InfinoError::Backend(e.to_string())
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::*;
use crate::{
storage::StorageError,
supertable::wal::{
WalStoreError,
pipeline::{AppendPhaseError, TombstonePhaseError},
},
};
#[test]
fn display_messages_are_prefixed() {
assert_eq!(
InfinoError::NotFound("t".into()).to_string(),
"not found: t"
);
assert_eq!(
InfinoError::AlreadyExists("t".into()).to_string(),
"already exists: t"
);
assert_eq!(InfinoError::Schema("t".into()).to_string(), "schema: t");
assert_eq!(
InfinoError::Cardinality("t".into()).to_string(),
"cardinality: t"
);
assert_eq!(InfinoError::Io("t".into()).to_string(), "io: t");
assert_eq!(InfinoError::Query("t".into()).to_string(), "query: t");
assert_eq!(InfinoError::Conflict("t".into()).to_string(), "conflict: t");
assert_eq!(InfinoError::Backend("t".into()).to_string(), "backend: t");
assert_eq!(InfinoError::Config("t".into()).to_string(), "config: t");
}
#[test]
fn with_context_prefixes_operation_and_table() {
let err = InfinoError::NotFound("posts".into()).with_context("open_table", Some("posts"));
assert_eq!(err.to_string(), "not found: open_table(posts): posts");
let err = InfinoError::Cardinality("mismatch".into()).with_context("update", None);
assert_eq!(err.to_string(), "cardinality: update: mismatch");
let err = InfinoError::Conflict("lost the CAS".into()).with_context("delete", None);
assert_eq!(err.to_string(), "conflict: delete: lost the CAS");
}
#[test]
fn from_storage_error_maps_each_variant() {
assert!(matches!(
InfinoError::from(StorageError::NotFound { uri: "u".into() }),
InfinoError::NotFound(_)
));
assert!(matches!(
InfinoError::from(StorageError::PreconditionFailed { uri: "u".into() }),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(StorageError::TransientExhausted {
uri: "u".into(),
source: "x".into()
}),
InfinoError::Io(_)
));
assert!(matches!(
InfinoError::from(StorageError::Permanent {
uri: "u".into(),
source: "x".into()
}),
InfinoError::Io(_)
));
}
#[test]
fn from_query_read_and_build_errors() {
assert!(matches!(
InfinoError::from(QueryError::Plan("p".into())),
InfinoError::Query(_)
));
assert!(matches!(
InfinoError::from(QueryError::OverBudget("b".into())),
InfinoError::OverBudget(_)
));
assert!(matches!(
InfinoError::from(SuperfileReadError::MissingKv("k")),
InfinoError::Query(_)
));
assert!(matches!(
InfinoError::from(SuperfileBuildError::MissingIdColumn("c".into())),
InfinoError::Schema(_)
));
assert!(matches!(
InfinoError::from(SupertableBuildError::NoDocsToBuild),
InfinoError::Schema(_)
));
}
#[test]
fn from_commit_and_open_errors_are_backend() {
assert!(matches!(
InfinoError::from(SupertableCommitError::Encode("e".into())),
InfinoError::Backend(_)
));
assert!(matches!(
InfinoError::from(OpenError::ManifestListParse("m".into())),
InfinoError::Backend(_)
));
}
#[test]
fn manifest_pointer_vanished_is_not_found_but_a_storage_fault_is_retryable_io() {
assert!(matches!(
InfinoError::from(ManifestLoadError::PointerVanished),
InfinoError::NotFound(_)
));
assert!(matches!(
InfinoError::from(ManifestLoadError::Storage(
StorageError::TransientExhausted {
uri: "p".into(),
source: "blip".into(),
}
)),
InfinoError::Io(_)
));
}
#[test]
fn over_budget_routes_through_wrappers() {
let nested =
MutationCommitError::AppendFlush(SupertableBuildError::OverBudget("deep".into()));
assert!(matches!(
InfinoError::from(nested),
InfinoError::OverBudget(_)
));
assert!(matches!(
InfinoError::from(MutationCommitError::AppendFlush(
SupertableBuildError::NoDocsToBuild
)),
InfinoError::Backend(_)
));
}
#[test]
fn cas_loss_maps_to_conflict_on_every_mutation_path() {
assert!(matches!(
InfinoError::from(SupertableCommitError::WriteContentionExhausted),
InfinoError::Conflict(_)
));
assert!(matches!(
SupertableBuildError::from(SupertableCommitError::WriteContentionExhausted),
SupertableBuildError::WriteContention
));
assert!(matches!(
InfinoError::from(MutationCommitError::AppendFlush(
SupertableBuildError::WriteContention
)),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(MutationCommitError::PartialCommit {
committed_wal_ids: Vec::new(),
committed: 0,
total: 1,
cause: Box::new(MutationError::WalStore(WalStoreError::CasFailed {
path: "wal/mutations/1.json".into()
})),
}),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(MutationError::TombstonePhase(
TombstonePhaseError::CasRetryExhausted {
superfile_id: Uuid::nil(),
attempts: 8,
}
)),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(MutationError::AppendPhase(
AppendPhaseError::ManifestCommit(Box::new(
SupertableCommitError::WriteContentionExhausted
))
)),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(MutationError::Storage(StorageError::PreconditionFailed {
uri: "u".into()
})),
InfinoError::Conflict(_)
));
assert!(matches!(
InfinoError::from(OpenError::Commit(
SupertableCommitError::WriteContentionExhausted
)),
InfinoError::Conflict(_)
));
}
#[test]
fn non_cas_failures_are_not_conflicts() {
assert!(matches!(
InfinoError::from(MutationError::WalStore(WalStoreError::AlreadyExists {
path: "wal/mutations/1.json".into()
})),
InfinoError::Backend(_)
));
assert!(matches!(
InfinoError::from(MutationError::TombstonePhase(
TombstonePhaseError::IdLookupFailed {
target_id: "7".into(),
message: "boom".into(),
}
)),
InfinoError::Backend(_)
));
assert!(matches!(
InfinoError::from(SupertableCommitError::Encode("e".into())),
InfinoError::Backend(_)
));
assert!(matches!(
InfinoError::from(SupertableCommitError::PointerVanished),
InfinoError::NotFound(_)
));
}
#[test]
fn from_mutation_error_maps_each_arm() {
assert!(matches!(
InfinoError::from(MutationError::PredicateEval(QueryError::Plan("p".into()))),
InfinoError::Query(_)
));
assert!(matches!(
InfinoError::from(MutationError::Storage(StorageError::NotFound {
uri: "u".into()
})),
InfinoError::NotFound(_)
));
assert!(matches!(
InfinoError::from(MutationError::CardinalityMismatch {
matched: 1,
new_rows: 2
}),
InfinoError::Cardinality(_)
));
assert!(matches!(
InfinoError::from(MutationError::MatchCountExceedsCap { matched: 9, cap: 5 }),
InfinoError::Cardinality(_)
));
assert!(matches!(
InfinoError::from(MutationError::SchemaMismatch("s".into())),
InfinoError::Schema(_)
));
assert!(matches!(
InfinoError::from(MutationError::NoStorageAttached),
InfinoError::Backend(_)
));
}
}