use thiserror::Error;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TalaDbError {
#[error("storage error: {0}")]
Storage(String),
#[error("storage error: {0}")]
StorageDatabase(#[from] redb::DatabaseError),
#[error("storage error: {0}")]
StorageTransaction(#[source] Box<redb::TransactionError>),
#[error("storage error: {0}")]
StorageTable(#[source] Box<redb::TableError>),
#[error("storage error: {0}")]
StorageIo(#[from] redb::StorageError),
#[error("storage error: {0}")]
StorageCommit(#[from] redb::CommitError),
#[error("storage error: {0}")]
StorageEngine(#[source] Box<redb::Error>),
#[error("serialization error: {0}")]
Serialization(String),
#[error("serialization error: {0}")]
Postcard(#[from] postcard::Error),
#[error("document not found")]
NotFound,
#[error("invalid filter: {0}")]
InvalidFilter(String),
#[error("index already exists: {0}")]
IndexExists(String),
#[error("index not found: {0}")]
IndexNotFound(String),
#[error("migration error: {0}")]
Migration(String),
#[error("type error: expected {expected}, got {got}")]
TypeError { expected: String, got: String },
#[error("encryption error: {0}")]
Encryption(String),
#[error("watch channel closed")]
WatchClosed,
#[error("watch subscriber dropped due to full channel")]
WatchBackpressure,
#[error("invalid or corrupt snapshot data")]
InvalidSnapshot,
#[error("vector index not found: {0}")]
VectorIndexNotFound(String),
#[error("vector dimension mismatch: index expects {expected}, got {got}")]
VectorDimensionMismatch { expected: usize, got: usize },
#[error(
"a compound index cannot cover more than one array field in the same document — \
the index entries would be the product of the two lists"
)]
CompoundIndexMultipleArrays,
#[error("duplicate document id: {0} already exists in this collection")]
DuplicateId(String),
#[error("invalid document id: {0}")]
InvalidDocumentId(String),
#[error("invalid operation: {0}")]
InvalidOperation(String),
#[error("config error: {0}")]
Config(String),
#[error("invalid name: {0}")]
InvalidName(String),
#[error("ANN traversal exceeded the shared search memory allowance")]
SearchMemoryLimit,
#[error("query exceeded the configured timeout")]
QueryTimeout,
#[error("changeset exceeds the maximum allowed entry count")]
ChangesetTooLarge,
}
macro_rules! boxed_from {
($($source:ty => $variant:ident),* $(,)?) => {
$(
impl From<$source> for TalaDbError {
fn from(e: $source) -> Self {
TalaDbError::$variant(Box::new(e))
}
}
)*
};
}
boxed_from! {
redb::TransactionError => StorageTransaction,
redb::TableError => StorageTable,
redb::Error => StorageEngine,
}
impl TalaDbError {
pub fn code(&self) -> &'static str {
match self {
Self::Storage(_)
| Self::StorageDatabase(_)
| Self::StorageTransaction(_)
| Self::StorageTable(_)
| Self::StorageIo(_)
| Self::StorageCommit(_)
| Self::StorageEngine(_) => "Storage",
Self::Serialization(_) | Self::Postcard(_) => "Serialization",
Self::NotFound => "NotFound",
Self::InvalidFilter(_) => "InvalidFilter",
Self::IndexExists(_) => "IndexExists",
Self::IndexNotFound(_) => "IndexNotFound",
Self::Migration(_) => "Migration",
Self::TypeError { .. } => "TypeError",
Self::Encryption(_) => "Encryption",
Self::WatchClosed => "WatchClosed",
Self::WatchBackpressure => "WatchBackpressure",
Self::InvalidSnapshot => "InvalidSnapshot",
Self::VectorIndexNotFound(_) => "VectorIndexNotFound",
Self::VectorDimensionMismatch { .. } => "VectorDimensionMismatch",
Self::CompoundIndexMultipleArrays => "CompoundIndexMultipleArrays",
Self::DuplicateId(_) => "DuplicateId",
Self::InvalidDocumentId(_) => "InvalidDocumentId",
Self::InvalidOperation(_) => "InvalidOperation",
Self::Config(_) => "Config",
Self::InvalidName(_) => "InvalidName",
Self::QueryTimeout => "QueryTimeout",
Self::SearchMemoryLimit => "SearchMemoryLimit",
Self::ChangesetTooLarge => "ChangesetTooLarge",
}
}
pub fn is_storage(&self) -> bool {
self.code() == "Storage"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn storage_errors_expose_their_source() {
let err = TalaDbError::from(redb::StorageError::Corrupted("bad page".into()));
let source = err.source().expect("storage errors must carry a source");
assert!(
source.downcast_ref::<redb::StorageError>().is_some(),
"source must downcast back to the originating redb error"
);
assert_eq!(err.code(), "Storage");
}
#[test]
fn storage_display_text_is_unchanged_by_the_split() {
let inner = redb::StorageError::Corrupted("bad page".into());
let expected = format!("storage error: {inner}");
assert_eq!(TalaDbError::from(inner).to_string(), expected);
}
#[test]
fn oversized_sources_downcast_through_their_box() {
let err = TalaDbError::from(redb::TableError::TableDoesNotExist("books".into()));
let source = err.source().expect("must carry a source");
assert!(
source.downcast_ref::<Box<redb::TableError>>().is_some(),
"boxed sources downcast to Box<T>"
);
assert_eq!(err.code(), "Storage");
}
#[test]
fn error_type_stays_small_enough_for_the_hot_path() {
let size = std::mem::size_of::<TalaDbError>();
assert!(
size <= 56,
"TalaDbError grew to {size} bytes — box any large source rather than \
storing it inline"
);
}
#[test]
fn hand_built_storage_errors_have_no_source() {
let err = TalaDbError::Storage("cannot write encryption salt".into());
assert!(err.source().is_none());
assert!(err.is_storage());
}
}