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),
#[error(
"Database substrate format {found} is older than the current format {current}; \
open the database writable once to migrate it (read-only opens of an \
un-migrated store cannot upgrade)"
)]
SubstrateUpgradeRequired {
found: u8,
current: u8,
},
#[error(
"Database substrate format {found} is newer than this build's format {current}; \
upgrade PulseDB to open this database (do not modify it with an older build)"
)]
SubstrateFormatTooNew {
found: u8,
current: u8,
},
#[error(
"store too large for a single-transaction codec migration: store size {store_size} bytes \
(projected peak ~{projected_peak} bytes) exceeds the single-txn budget of {budget} bytes; \
declare available memory via Config to raise the ceiling, or run the offline migration tool"
)]
SubstrateMigrationTooLarge {
store_size: u64,
projected_peak: u64,
budget: u64,
},
#[error(
"insufficient free disk space for the codec migration: store size {store_size} bytes \
needs ~{required} bytes free (pristine backup + migrated file + transaction margin) \
but only {available} bytes are available; free up disk or run the offline migration tool"
)]
SubstrateMigrationInsufficientDisk {
store_size: u64,
required: u64,
available: u64,
},
#[error(
"cannot migrate this database's sync state without the `sync` feature: it \
contains sync_cursors rows that require a sync-enabled PulseDB build to \
re-encode; rebuild or run PulseDB with the `sync` feature to migrate it"
)]
SubstrateMigrationRequiresSync,
}
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())
}
pub fn substrate_upgrade_required(found: u8, current: u8) -> Self {
Self::SubstrateUpgradeRequired { found, current }
}
pub fn substrate_format_too_new(found: u8, current: u8) -> Self {
Self::SubstrateFormatTooNew { found, current }
}
pub fn substrate_migration_too_large(
store_size: u64,
projected_peak: u64,
budget: u64,
) -> Self {
Self::SubstrateMigrationTooLarge {
store_size,
projected_peak,
budget,
}
}
pub fn substrate_migration_insufficient_disk(
store_size: u64,
required: u64,
available: u64,
) -> Self {
Self::SubstrateMigrationInsufficientDisk {
store_size,
required,
available,
}
}
}
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<postcard::Error> for StorageError {
fn from(err: postcard::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<postcard::Error> for PulseDBError {
fn from(err: postcard::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());
}
#[test]
fn test_substrate_upgrade_required_display_is_actionable() {
let err = StorageError::substrate_upgrade_required(0, 1);
assert!(matches!(
err,
StorageError::SubstrateUpgradeRequired {
found: 0,
current: 1
}
));
let msg = err.to_string();
assert!(msg.contains("substrate format 0"), "msg: {msg}");
assert!(msg.contains("current format 1"), "msg: {msg}");
assert!(msg.contains("writable"), "msg: {msg}");
}
#[test]
fn test_substrate_format_too_new_display_is_actionable() {
let err = StorageError::substrate_format_too_new(7, 1);
assert!(matches!(
err,
StorageError::SubstrateFormatTooNew {
found: 7,
current: 1
}
));
let msg = err.to_string();
assert!(msg.contains("substrate format 7"), "msg: {msg}");
assert!(msg.contains("format 1"), "msg: {msg}");
assert!(msg.contains("upgrade PulseDB"), "msg: {msg}");
}
#[test]
fn test_substrate_errors_propagate_as_storage() {
let err: PulseDBError = StorageError::substrate_upgrade_required(0, 1).into();
assert!(err.is_storage());
let err: PulseDBError = StorageError::substrate_format_too_new(2, 1).into();
assert!(err.is_storage());
}
}