use std::path::PathBuf;
use thiserror::Error;
#[cfg(feature = "sync")]
use crate::sync::SyncError;
pub type Result<T> = std::result::Result<T, PulseDBError>;
#[derive(Debug, Error)]
pub enum PulseDBError {
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Configuration error: {reason}")]
Config {
reason: String,
},
#[error("{0}")]
NotFound(#[from] NotFoundError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Embedding error: {0}")]
Embedding(String),
#[error("Vector index error: {0}")]
Vector(String),
#[error("Watch error: {0}")]
Watch(String),
#[error("Internal error: {0}")]
Internal(String),
#[error("Database is in read-only mode")]
ReadOnly,
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
#[error("Sync error: {0}")]
Sync(#[from] SyncError),
}
impl PulseDBError {
pub fn config(reason: impl Into<String>) -> Self {
Self::Config {
reason: reason.into(),
}
}
pub fn embedding(msg: impl Into<String>) -> Self {
Self::Embedding(msg.into())
}
pub fn vector(msg: impl Into<String>) -> Self {
Self::Vector(msg.into())
}
pub fn watch(msg: impl Into<String>) -> Self {
Self::Watch(msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
pub fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound(_))
}
pub fn is_validation(&self) -> bool {
matches!(self, Self::Validation(_))
}
pub fn is_storage(&self) -> bool {
matches!(self, Self::Storage(_))
}
pub fn is_vector(&self) -> bool {
matches!(self, Self::Vector(_))
}
pub fn is_watch(&self) -> bool {
matches!(self, Self::Watch(_))
}
pub fn is_embedding(&self) -> bool {
matches!(self, Self::Embedding(_))
}
pub fn is_internal(&self) -> bool {
matches!(self, Self::Internal(_))
}
pub fn is_config(&self) -> bool {
matches!(self, Self::Config { .. })
}
pub fn is_io(&self) -> bool {
matches!(self, Self::Io(_))
}
pub fn is_read_only(&self) -> bool {
matches!(self, Self::ReadOnly)
}
#[cfg(feature = "sync")]
#[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
pub fn is_sync(&self) -> bool {
matches!(self, Self::Sync(_))
}
}
#[derive(Debug, Error)]
pub enum StorageError {
#[error("Database corrupted: {0}")]
Corrupted(String),
#[error("Database not found: {0}")]
DatabaseNotFound(PathBuf),
#[error("Database is locked by another writer")]
DatabaseLocked,
#[error("Transaction failed: {0}")]
Transaction(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Storage engine error: {0}")]
Redb(String),
#[error("Schema version mismatch: expected {expected}, found {found}")]
SchemaVersionMismatch {
expected: u32,
found: u32,
},
#[error("Table not found: {0}")]
TableNotFound(String),
}
impl StorageError {
pub fn corrupted(msg: impl Into<String>) -> Self {
Self::Corrupted(msg.into())
}
pub fn transaction(msg: impl Into<String>) -> Self {
Self::Transaction(msg.into())
}
pub fn serialization(msg: impl Into<String>) -> Self {
Self::Serialization(msg.into())
}
pub fn redb(msg: impl Into<String>) -> Self {
Self::Redb(msg.into())
}
}
impl From<redb::Error> for StorageError {
fn from(err: redb::Error) -> Self {
StorageError::Redb(err.to_string())
}
}
impl From<redb::DatabaseError> for StorageError {
fn from(err: redb::DatabaseError) -> Self {
StorageError::Redb(err.to_string())
}
}
impl From<redb::TransactionError> for StorageError {
fn from(err: redb::TransactionError) -> Self {
StorageError::Transaction(err.to_string())
}
}
impl From<redb::CommitError> for StorageError {
fn from(err: redb::CommitError) -> Self {
StorageError::Transaction(format!("Commit failed: {}", err))
}
}
impl From<redb::TableError> for StorageError {
fn from(err: redb::TableError) -> Self {
StorageError::Redb(format!("Table error: {}", err))
}
}
impl From<redb::StorageError> for StorageError {
fn from(err: redb::StorageError) -> Self {
StorageError::Redb(format!("Storage error: {}", err))
}
}
impl From<bincode::Error> for StorageError {
fn from(err: bincode::Error) -> Self {
StorageError::Serialization(err.to_string())
}
}
impl From<redb::Error> for PulseDBError {
fn from(err: redb::Error) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<redb::DatabaseError> for PulseDBError {
fn from(err: redb::DatabaseError) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<redb::TransactionError> for PulseDBError {
fn from(err: redb::TransactionError) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<redb::CommitError> for PulseDBError {
fn from(err: redb::CommitError) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<redb::TableError> for PulseDBError {
fn from(err: redb::TableError) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<redb::StorageError> for PulseDBError {
fn from(err: redb::StorageError) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
impl From<bincode::Error> for PulseDBError {
fn from(err: bincode::Error) -> Self {
PulseDBError::Storage(StorageError::from(err))
}
}
#[derive(Debug, Error)]
pub enum ValidationError {
#[error("Embedding dimension mismatch: expected {expected}, got {got}")]
DimensionMismatch {
expected: usize,
got: usize,
},
#[error("Invalid field '{field}': {reason}")]
InvalidField {
field: String,
reason: String,
},
#[error("Content too large: {size} bytes (max: {max} bytes)")]
ContentTooLarge {
size: usize,
max: usize,
},
#[error("Required field missing: {field}")]
RequiredField {
field: String,
},
#[error("Too many items in '{field}': {count} (max: {max})")]
TooManyItems {
field: String,
count: usize,
max: usize,
},
}
impl ValidationError {
pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
Self::DimensionMismatch { expected, got }
}
pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
Self::InvalidField {
field: field.into(),
reason: reason.into(),
}
}
pub fn content_too_large(size: usize, max: usize) -> Self {
Self::ContentTooLarge { size, max }
}
pub fn required_field(field: impl Into<String>) -> Self {
Self::RequiredField {
field: field.into(),
}
}
pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
Self::TooManyItems {
field: field.into(),
count,
max,
}
}
}
#[derive(Debug, Error)]
pub enum NotFoundError {
#[error("Collective not found: {0}")]
Collective(String),
#[error("Experience not found: {0}")]
Experience(String),
#[error("Relation not found: {0}")]
Relation(String),
#[error("Insight not found: {0}")]
Insight(String),
#[error("Activity not found: {0}")]
Activity(String),
}
impl NotFoundError {
pub fn collective(id: impl ToString) -> Self {
Self::Collective(id.to_string())
}
pub fn experience(id: impl ToString) -> Self {
Self::Experience(id.to_string())
}
pub fn relation(id: impl ToString) -> Self {
Self::Relation(id.to_string())
}
pub fn insight(id: impl ToString) -> Self {
Self::Insight(id.to_string())
}
pub fn activity(id: impl ToString) -> Self {
Self::Activity(id.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = PulseDBError::config("Invalid dimension");
assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
}
#[test]
fn test_storage_error_display() {
let err = StorageError::SchemaVersionMismatch {
expected: 2,
found: 1,
};
assert_eq!(
err.to_string(),
"Schema version mismatch: expected 2, found 1"
);
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::dimension_mismatch(384, 768);
assert_eq!(
err.to_string(),
"Embedding dimension mismatch: expected 384, got 768"
);
}
#[test]
fn test_not_found_error_display() {
let err = NotFoundError::collective("abc-123");
assert_eq!(err.to_string(), "Collective not found: abc-123");
}
#[test]
fn test_is_not_found() {
let err: PulseDBError = NotFoundError::collective("test").into();
assert!(err.is_not_found());
assert!(!err.is_validation());
}
#[test]
fn test_is_validation() {
let err: PulseDBError = ValidationError::required_field("content").into();
assert!(err.is_validation());
assert!(!err.is_not_found());
}
#[test]
fn test_vector_error_display() {
let err = PulseDBError::vector("HNSW insert failed");
assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
assert!(err.is_vector());
assert!(!err.is_storage());
}
#[test]
fn test_error_conversion_chain() {
fn inner() -> Result<()> {
Err(StorageError::corrupted("test corruption"))?
}
let result = inner();
assert!(result.is_err());
assert!(result.unwrap_err().is_storage());
}
#[test]
fn test_watch_error_display() {
let err = PulseDBError::watch("subscribers lock poisoned");
assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
}
#[test]
fn test_watch_constructor() {
let err = PulseDBError::watch("test");
assert!(err.is_watch());
assert!(!err.is_storage());
}
#[test]
fn test_is_watch() {
let err = PulseDBError::watch("test");
assert!(err.is_watch());
assert!(!err.is_not_found());
}
#[test]
fn test_is_embedding() {
let err = PulseDBError::embedding("model load failed");
assert!(err.is_embedding());
assert!(!err.is_vector());
}
#[test]
fn test_is_internal() {
let err = PulseDBError::internal("task join failed");
assert!(err.is_internal());
assert!(!err.is_storage());
}
#[test]
fn test_is_config() {
let err = PulseDBError::config("invalid dimension");
assert!(err.is_config());
assert!(!err.is_validation());
}
#[test]
fn test_is_io() {
let err = PulseDBError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file missing",
));
assert!(err.is_io());
assert!(!err.is_storage());
}
}