#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Storage error: {0}")]
Storage(#[from] redb::Error),
#[error("Database error: {0}")]
Database(String),
#[error("Table error: {0}")]
Table(String),
#[error("Transaction error: {0}")]
Transaction(String),
#[error("Commit error: {0}")]
Commit(String),
#[error("Vector index error: {0}")]
VectorIndex(String),
#[error("Invalid file format: {0}")]
InvalidFormat(&'static str),
#[error("Unsupported version: {0} (current version: {1})")]
UnsupportedVersion(u32, u32),
#[error("Memory not found: {0}")]
MemoryNotFound(String),
#[error("Entity not found: {0}")]
EntityNotFound(String),
#[error("Invalid embedding dimension: expected {expected}, got {got}")]
InvalidEmbeddingDimension { expected: usize, got: usize },
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid memory ID: {0}")]
InvalidMemoryId(String),
#[error("Invalid timestamp: {0}")]
InvalidTimestamp(String),
#[error("Invalid source: {0}")]
InvalidSource(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Invalid parameter: {0}")]
InvalidParameter(String),
#[error("Database already exists at path: {0}")]
DatabaseExists(String),
#[error("Database not found at path: {0}")]
DatabaseNotFound(String),
#[error("Namespace mismatch: expected '{expected}', found '{found}'")]
NamespaceMismatch { expected: String, found: String },
#[error("Database corruption detected: {0}")]
DatabaseCorruption(String),
#[error("File truncated or incomplete: {0}")]
FileTruncated(String),
#[error("SLM feature not available - compile with 'slm' feature to enable")]
SlmNotAvailable,
#[error("SLM initialization error: {0}")]
SlmInitialization(String),
#[error("SLM inference error: {0}")]
SlmInference(String),
#[error("SLM inference timeout after {0}ms")]
SlmTimeout(u64),
#[error("Model not found: {0}")]
ModelNotFound(String),
#[error("Inference error: {0}")]
InferenceError(String),
#[error("Entity extraction feature not available - compile with 'entity-extraction' feature")]
EntityExtractionNotAvailable,
#[error(
"No embedding engine configured. Either pass an explicit embedding vector \
or set 'embedding_model' in Config to enable automatic embedding."
)]
NoEmbeddingEngine,
#[error("Embedding feature not available - compile with 'embedding-onnx' feature")]
EmbeddingNotAvailable,
}
pub type Result<T> = std::result::Result<T, Error>;
impl Error {
pub fn is_recoverable(&self) -> bool {
match self {
Error::MemoryNotFound(_) => true,
Error::EntityNotFound(_) => true,
Error::NamespaceMismatch { .. } => true,
Error::InvalidParameter(_) => true,
Error::InvalidEmbeddingDimension { .. } => true,
Error::DatabaseCorruption(_) => false,
Error::FileTruncated(_) => false,
Error::InvalidFormat(_) => false,
Error::UnsupportedVersion(..) => false,
Error::Storage(_) => true,
Error::Io(_) => true,
_ => false,
}
}
pub fn user_message(&self) -> String {
match self {
Error::InvalidEmbeddingDimension { expected, got } => {
format!(
"Embedding dimension mismatch: expected {} dimensions, but got {}.\n\
Hint: Ensure all embeddings use the same model and dimension size.\n\
You can set the dimension in Config with .with_embedding_dim({})",
expected, got, expected
)
}
Error::DatabaseCorruption(msg) => {
format!(
"Database corruption detected: {}\n\
The database file may be corrupted or incomplete.\n\
Hint: Try restoring from a backup if available, or create a new database.",
msg
)
}
Error::FileTruncated(msg) => {
format!(
"Database file is truncated: {}\n\
The file may have been corrupted during a previous operation.\n\
Hint: Restore from backup or delete the file to start fresh.",
msg
)
}
Error::UnsupportedVersion(found, current) => {
if found > current {
format!(
"Database version {} is newer than supported version {}.\n\
Hint: Update MnemeFusion to the latest version.",
found, current
)
} else {
format!(
"Database version {} is older than current version {}.\n\
Migration may be required.",
found, current
)
}
}
Error::NamespaceMismatch { expected, found } => {
format!(
"Namespace mismatch: operation expected '{}' but memory is in '{}'.\n\
Hint: Verify you're using the correct namespace for this operation.",
expected, found
)
}
Error::VectorIndex(msg) => {
format!(
"Vector index error: {}\n\
Hint: This may indicate corrupted index data. Try reopening the database.",
msg
)
}
_ => self.to_string(),
}
}
pub fn is_corruption(&self) -> bool {
matches!(
self,
Error::DatabaseCorruption(_) | Error::FileTruncated(_) | Error::InvalidFormat(_)
)
}
pub fn is_version_error(&self) -> bool {
matches!(self, Error::UnsupportedVersion(..))
}
}
impl From<redb::DatabaseError> for Error {
fn from(err: redb::DatabaseError) -> Self {
Error::Database(err.to_string())
}
}
impl From<redb::TransactionError> for Error {
fn from(err: redb::TransactionError) -> Self {
Error::Transaction(err.to_string())
}
}
impl From<redb::TableError> for Error {
fn from(err: redb::TableError) -> Self {
Error::Table(err.to_string())
}
}
impl From<redb::CommitError> for Error {
fn from(err: redb::CommitError) -> Self {
Error::Commit(err.to_string())
}
}
impl From<redb::StorageError> for Error {
fn from(err: redb::StorageError) -> Self {
Error::Storage(redb::Error::from(err))
}
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
if err.is_data() {
Error::Deserialization(err.to_string())
} else {
Error::Serialization(err.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::InvalidFormat("bad magic number");
assert_eq!(err.to_string(), "Invalid file format: bad magic number");
let err = Error::InvalidEmbeddingDimension {
expected: 384,
got: 512,
};
assert_eq!(
err.to_string(),
"Invalid embedding dimension: expected 384, got 512"
);
}
#[test]
fn test_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
}
#[test]
fn test_is_recoverable() {
assert!(Error::MemoryNotFound("id".to_string()).is_recoverable());
assert!(Error::EntityNotFound("id".to_string()).is_recoverable());
assert!(Error::InvalidParameter("test".to_string()).is_recoverable());
assert!(Error::InvalidEmbeddingDimension {
expected: 384,
got: 512
}
.is_recoverable());
assert!(!Error::DatabaseCorruption("test".to_string()).is_recoverable());
assert!(!Error::FileTruncated("test".to_string()).is_recoverable());
assert!(!Error::InvalidFormat("test").is_recoverable());
assert!(!Error::UnsupportedVersion(2, 1).is_recoverable());
}
#[test]
fn test_is_corruption() {
assert!(Error::DatabaseCorruption("test".to_string()).is_corruption());
assert!(Error::FileTruncated("test".to_string()).is_corruption());
assert!(Error::InvalidFormat("test").is_corruption());
assert!(!Error::MemoryNotFound("id".to_string()).is_corruption());
assert!(!Error::InvalidParameter("test".to_string()).is_corruption());
}
#[test]
fn test_is_version_error() {
assert!(Error::UnsupportedVersion(2, 1).is_version_error());
assert!(!Error::DatabaseCorruption("test".to_string()).is_version_error());
}
#[test]
fn test_user_message() {
let err = Error::InvalidEmbeddingDimension {
expected: 384,
got: 512,
};
let msg = err.user_message();
assert!(msg.contains("expected 384"));
assert!(msg.contains("got 512"));
assert!(msg.contains("Hint"));
let err = Error::DatabaseCorruption("bad data".to_string());
let msg = err.user_message();
assert!(msg.contains("corruption"));
assert!(msg.contains("backup"));
let err = Error::UnsupportedVersion(5, 1);
let msg = err.user_message();
assert!(msg.contains("newer"));
assert!(msg.contains("Update"));
}
#[test]
fn test_unsupported_version_messages() {
let err = Error::UnsupportedVersion(5, 1);
let msg = err.user_message();
assert!(msg.contains("newer"));
let err = Error::UnsupportedVersion(1, 5);
let msg = err.user_message();
assert!(msg.contains("older") || msg.contains("Migration"));
}
}