1use std::path::PathBuf;
21use thiserror::Error;
22
23#[cfg(feature = "sync")]
24use crate::sync::SyncError;
25
26pub type Result<T> = std::result::Result<T, PulseDBError>;
28
29#[derive(Debug, Error)]
34pub enum PulseDBError {
35 #[error("Storage error: {0}")]
37 Storage(#[from] StorageError),
38
39 #[error("Validation error: {0}")]
41 Validation(#[from] ValidationError),
42
43 #[error("Configuration error: {reason}")]
45 Config {
46 reason: String,
48 },
49
50 #[error("{0}")]
52 NotFound(#[from] NotFoundError),
53
54 #[error("I/O error: {0}")]
56 Io(#[from] std::io::Error),
57
58 #[error("Embedding error: {0}")]
60 Embedding(String),
61
62 #[error("Vector index error: {0}")]
64 Vector(String),
65
66 #[error("Watch error: {0}")]
68 Watch(String),
69
70 #[error("Internal error: {0}")]
72 Internal(String),
73
74 #[cfg(feature = "sync")]
78 #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
79 #[error("Sync error: {0}")]
80 Sync(#[from] SyncError),
81}
82
83impl PulseDBError {
84 pub fn config(reason: impl Into<String>) -> Self {
86 Self::Config {
87 reason: reason.into(),
88 }
89 }
90
91 pub fn embedding(msg: impl Into<String>) -> Self {
93 Self::Embedding(msg.into())
94 }
95
96 pub fn vector(msg: impl Into<String>) -> Self {
98 Self::Vector(msg.into())
99 }
100
101 pub fn watch(msg: impl Into<String>) -> Self {
103 Self::Watch(msg.into())
104 }
105
106 pub fn internal(msg: impl Into<String>) -> Self {
108 Self::Internal(msg.into())
109 }
110
111 pub fn is_not_found(&self) -> bool {
113 matches!(self, Self::NotFound(_))
114 }
115
116 pub fn is_validation(&self) -> bool {
118 matches!(self, Self::Validation(_))
119 }
120
121 pub fn is_storage(&self) -> bool {
123 matches!(self, Self::Storage(_))
124 }
125
126 pub fn is_vector(&self) -> bool {
128 matches!(self, Self::Vector(_))
129 }
130
131 pub fn is_watch(&self) -> bool {
133 matches!(self, Self::Watch(_))
134 }
135
136 pub fn is_embedding(&self) -> bool {
138 matches!(self, Self::Embedding(_))
139 }
140
141 pub fn is_internal(&self) -> bool {
143 matches!(self, Self::Internal(_))
144 }
145
146 pub fn is_config(&self) -> bool {
148 matches!(self, Self::Config { .. })
149 }
150
151 pub fn is_io(&self) -> bool {
153 matches!(self, Self::Io(_))
154 }
155
156 #[cfg(feature = "sync")]
160 #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
161 pub fn is_sync(&self) -> bool {
162 matches!(self, Self::Sync(_))
163 }
164}
165
166#[derive(Debug, Error)]
170pub enum StorageError {
171 #[error("Database corrupted: {0}")]
173 Corrupted(String),
174
175 #[error("Database not found: {0}")]
177 DatabaseNotFound(PathBuf),
178
179 #[error("Database is locked by another writer")]
181 DatabaseLocked,
182
183 #[error("Transaction failed: {0}")]
185 Transaction(String),
186
187 #[error("Serialization error: {0}")]
189 Serialization(String),
190
191 #[error("Storage engine error: {0}")]
193 Redb(String),
194
195 #[error("Schema version mismatch: expected {expected}, found {found}")]
197 SchemaVersionMismatch {
198 expected: u32,
200 found: u32,
202 },
203
204 #[error("Table not found: {0}")]
206 TableNotFound(String),
207}
208
209impl StorageError {
210 pub fn corrupted(msg: impl Into<String>) -> Self {
212 Self::Corrupted(msg.into())
213 }
214
215 pub fn transaction(msg: impl Into<String>) -> Self {
217 Self::Transaction(msg.into())
218 }
219
220 pub fn serialization(msg: impl Into<String>) -> Self {
222 Self::Serialization(msg.into())
223 }
224
225 pub fn redb(msg: impl Into<String>) -> Self {
227 Self::Redb(msg.into())
228 }
229}
230
231impl From<redb::Error> for StorageError {
233 fn from(err: redb::Error) -> Self {
234 StorageError::Redb(err.to_string())
235 }
236}
237
238impl From<redb::DatabaseError> for StorageError {
239 fn from(err: redb::DatabaseError) -> Self {
240 StorageError::Redb(err.to_string())
241 }
242}
243
244impl From<redb::TransactionError> for StorageError {
245 fn from(err: redb::TransactionError) -> Self {
246 StorageError::Transaction(err.to_string())
247 }
248}
249
250impl From<redb::CommitError> for StorageError {
251 fn from(err: redb::CommitError) -> Self {
252 StorageError::Transaction(format!("Commit failed: {}", err))
253 }
254}
255
256impl From<redb::TableError> for StorageError {
257 fn from(err: redb::TableError) -> Self {
258 StorageError::Redb(format!("Table error: {}", err))
259 }
260}
261
262impl From<redb::StorageError> for StorageError {
263 fn from(err: redb::StorageError) -> Self {
264 StorageError::Redb(format!("Storage error: {}", err))
265 }
266}
267
268impl From<bincode::Error> for StorageError {
270 fn from(err: bincode::Error) -> Self {
271 StorageError::Serialization(err.to_string())
272 }
273}
274
275impl From<redb::Error> for PulseDBError {
277 fn from(err: redb::Error) -> Self {
278 PulseDBError::Storage(StorageError::from(err))
279 }
280}
281
282impl From<redb::DatabaseError> for PulseDBError {
283 fn from(err: redb::DatabaseError) -> Self {
284 PulseDBError::Storage(StorageError::from(err))
285 }
286}
287
288impl From<redb::TransactionError> for PulseDBError {
289 fn from(err: redb::TransactionError) -> Self {
290 PulseDBError::Storage(StorageError::from(err))
291 }
292}
293
294impl From<redb::CommitError> for PulseDBError {
295 fn from(err: redb::CommitError) -> Self {
296 PulseDBError::Storage(StorageError::from(err))
297 }
298}
299
300impl From<redb::TableError> for PulseDBError {
301 fn from(err: redb::TableError) -> Self {
302 PulseDBError::Storage(StorageError::from(err))
303 }
304}
305
306impl From<redb::StorageError> for PulseDBError {
307 fn from(err: redb::StorageError) -> Self {
308 PulseDBError::Storage(StorageError::from(err))
309 }
310}
311
312impl From<bincode::Error> for PulseDBError {
313 fn from(err: bincode::Error) -> Self {
314 PulseDBError::Storage(StorageError::from(err))
315 }
316}
317
318#[derive(Debug, Error)]
322pub enum ValidationError {
323 #[error("Embedding dimension mismatch: expected {expected}, got {got}")]
325 DimensionMismatch {
326 expected: usize,
328 got: usize,
330 },
331
332 #[error("Invalid field '{field}': {reason}")]
334 InvalidField {
335 field: String,
337 reason: String,
339 },
340
341 #[error("Content too large: {size} bytes (max: {max} bytes)")]
343 ContentTooLarge {
344 size: usize,
346 max: usize,
348 },
349
350 #[error("Required field missing: {field}")]
352 RequiredField {
353 field: String,
355 },
356
357 #[error("Too many items in '{field}': {count} (max: {max})")]
359 TooManyItems {
360 field: String,
362 count: usize,
364 max: usize,
366 },
367}
368
369impl ValidationError {
370 pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
372 Self::DimensionMismatch { expected, got }
373 }
374
375 pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
377 Self::InvalidField {
378 field: field.into(),
379 reason: reason.into(),
380 }
381 }
382
383 pub fn content_too_large(size: usize, max: usize) -> Self {
385 Self::ContentTooLarge { size, max }
386 }
387
388 pub fn required_field(field: impl Into<String>) -> Self {
390 Self::RequiredField {
391 field: field.into(),
392 }
393 }
394
395 pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
397 Self::TooManyItems {
398 field: field.into(),
399 count,
400 max,
401 }
402 }
403}
404
405#[derive(Debug, Error)]
407pub enum NotFoundError {
408 #[error("Collective not found: {0}")]
410 Collective(String),
411
412 #[error("Experience not found: {0}")]
414 Experience(String),
415
416 #[error("Relation not found: {0}")]
418 Relation(String),
419
420 #[error("Insight not found: {0}")]
422 Insight(String),
423
424 #[error("Activity not found: {0}")]
426 Activity(String),
427}
428
429impl NotFoundError {
430 pub fn collective(id: impl ToString) -> Self {
432 Self::Collective(id.to_string())
433 }
434
435 pub fn experience(id: impl ToString) -> Self {
437 Self::Experience(id.to_string())
438 }
439
440 pub fn relation(id: impl ToString) -> Self {
442 Self::Relation(id.to_string())
443 }
444
445 pub fn insight(id: impl ToString) -> Self {
447 Self::Insight(id.to_string())
448 }
449
450 pub fn activity(id: impl ToString) -> Self {
452 Self::Activity(id.to_string())
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn test_error_display() {
462 let err = PulseDBError::config("Invalid dimension");
463 assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
464 }
465
466 #[test]
467 fn test_storage_error_display() {
468 let err = StorageError::SchemaVersionMismatch {
469 expected: 2,
470 found: 1,
471 };
472 assert_eq!(
473 err.to_string(),
474 "Schema version mismatch: expected 2, found 1"
475 );
476 }
477
478 #[test]
479 fn test_validation_error_display() {
480 let err = ValidationError::dimension_mismatch(384, 768);
481 assert_eq!(
482 err.to_string(),
483 "Embedding dimension mismatch: expected 384, got 768"
484 );
485 }
486
487 #[test]
488 fn test_not_found_error_display() {
489 let err = NotFoundError::collective("abc-123");
490 assert_eq!(err.to_string(), "Collective not found: abc-123");
491 }
492
493 #[test]
494 fn test_is_not_found() {
495 let err: PulseDBError = NotFoundError::collective("test").into();
496 assert!(err.is_not_found());
497 assert!(!err.is_validation());
498 }
499
500 #[test]
501 fn test_is_validation() {
502 let err: PulseDBError = ValidationError::required_field("content").into();
503 assert!(err.is_validation());
504 assert!(!err.is_not_found());
505 }
506
507 #[test]
508 fn test_vector_error_display() {
509 let err = PulseDBError::vector("HNSW insert failed");
510 assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
511 assert!(err.is_vector());
512 assert!(!err.is_storage());
513 }
514
515 #[test]
516 fn test_error_conversion_chain() {
517 fn inner() -> Result<()> {
519 Err(StorageError::corrupted("test corruption"))?
520 }
521
522 let result = inner();
523 assert!(result.is_err());
524 assert!(result.unwrap_err().is_storage());
525 }
526
527 #[test]
528 fn test_watch_error_display() {
529 let err = PulseDBError::watch("subscribers lock poisoned");
530 assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
531 }
532
533 #[test]
534 fn test_watch_constructor() {
535 let err = PulseDBError::watch("test");
536 assert!(err.is_watch());
537 assert!(!err.is_storage());
538 }
539
540 #[test]
541 fn test_is_watch() {
542 let err = PulseDBError::watch("test");
543 assert!(err.is_watch());
544 assert!(!err.is_not_found());
545 }
546
547 #[test]
548 fn test_is_embedding() {
549 let err = PulseDBError::embedding("model load failed");
550 assert!(err.is_embedding());
551 assert!(!err.is_vector());
552 }
553
554 #[test]
555 fn test_is_internal() {
556 let err = PulseDBError::internal("task join failed");
557 assert!(err.is_internal());
558 assert!(!err.is_storage());
559 }
560
561 #[test]
562 fn test_is_config() {
563 let err = PulseDBError::config("invalid dimension");
564 assert!(err.is_config());
565 assert!(!err.is_validation());
566 }
567
568 #[test]
569 fn test_is_io() {
570 let err = PulseDBError::Io(std::io::Error::new(
571 std::io::ErrorKind::NotFound,
572 "file missing",
573 ));
574 assert!(err.is_io());
575 assert!(!err.is_storage());
576 }
577}