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 #[error("Database is in read-only mode")]
79 ReadOnly,
80
81 #[cfg(feature = "sync")]
85 #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
86 #[error("Sync error: {0}")]
87 Sync(#[from] SyncError),
88}
89
90impl PulseDBError {
91 pub fn config(reason: impl Into<String>) -> Self {
93 Self::Config {
94 reason: reason.into(),
95 }
96 }
97
98 pub fn embedding(msg: impl Into<String>) -> Self {
100 Self::Embedding(msg.into())
101 }
102
103 pub fn vector(msg: impl Into<String>) -> Self {
105 Self::Vector(msg.into())
106 }
107
108 pub fn watch(msg: impl Into<String>) -> Self {
110 Self::Watch(msg.into())
111 }
112
113 pub fn internal(msg: impl Into<String>) -> Self {
115 Self::Internal(msg.into())
116 }
117
118 pub fn is_not_found(&self) -> bool {
120 matches!(self, Self::NotFound(_))
121 }
122
123 pub fn is_validation(&self) -> bool {
125 matches!(self, Self::Validation(_))
126 }
127
128 pub fn is_storage(&self) -> bool {
130 matches!(self, Self::Storage(_))
131 }
132
133 pub fn is_vector(&self) -> bool {
135 matches!(self, Self::Vector(_))
136 }
137
138 pub fn is_watch(&self) -> bool {
140 matches!(self, Self::Watch(_))
141 }
142
143 pub fn is_embedding(&self) -> bool {
145 matches!(self, Self::Embedding(_))
146 }
147
148 pub fn is_internal(&self) -> bool {
150 matches!(self, Self::Internal(_))
151 }
152
153 pub fn is_config(&self) -> bool {
155 matches!(self, Self::Config { .. })
156 }
157
158 pub fn is_io(&self) -> bool {
160 matches!(self, Self::Io(_))
161 }
162
163 pub fn is_read_only(&self) -> bool {
165 matches!(self, Self::ReadOnly)
166 }
167
168 #[cfg(feature = "sync")]
172 #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
173 pub fn is_sync(&self) -> bool {
174 matches!(self, Self::Sync(_))
175 }
176}
177
178#[derive(Debug, Error)]
182pub enum StorageError {
183 #[error("Database corrupted: {0}")]
185 Corrupted(String),
186
187 #[error("Database not found: {0}")]
189 DatabaseNotFound(PathBuf),
190
191 #[error("Database is locked by another writer")]
193 DatabaseLocked,
194
195 #[error("Transaction failed: {0}")]
197 Transaction(String),
198
199 #[error("Serialization error: {0}")]
201 Serialization(String),
202
203 #[error("Storage engine error: {0}")]
205 Redb(String),
206
207 #[error("Schema version mismatch: expected {expected}, found {found}")]
209 SchemaVersionMismatch {
210 expected: u32,
212 found: u32,
214 },
215
216 #[error("Table not found: {0}")]
218 TableNotFound(String),
219}
220
221impl StorageError {
222 pub fn corrupted(msg: impl Into<String>) -> Self {
224 Self::Corrupted(msg.into())
225 }
226
227 pub fn transaction(msg: impl Into<String>) -> Self {
229 Self::Transaction(msg.into())
230 }
231
232 pub fn serialization(msg: impl Into<String>) -> Self {
234 Self::Serialization(msg.into())
235 }
236
237 pub fn redb(msg: impl Into<String>) -> Self {
239 Self::Redb(msg.into())
240 }
241}
242
243impl From<redb::Error> for StorageError {
245 fn from(err: redb::Error) -> Self {
246 StorageError::Redb(err.to_string())
247 }
248}
249
250impl From<redb::DatabaseError> for StorageError {
251 fn from(err: redb::DatabaseError) -> Self {
252 StorageError::Redb(err.to_string())
253 }
254}
255
256impl From<redb::TransactionError> for StorageError {
257 fn from(err: redb::TransactionError) -> Self {
258 StorageError::Transaction(err.to_string())
259 }
260}
261
262impl From<redb::CommitError> for StorageError {
263 fn from(err: redb::CommitError) -> Self {
264 StorageError::Transaction(format!("Commit failed: {}", err))
265 }
266}
267
268impl From<redb::TableError> for StorageError {
269 fn from(err: redb::TableError) -> Self {
270 StorageError::Redb(format!("Table error: {}", err))
271 }
272}
273
274impl From<redb::StorageError> for StorageError {
275 fn from(err: redb::StorageError) -> Self {
276 StorageError::Redb(format!("Storage error: {}", err))
277 }
278}
279
280impl From<bincode::Error> for StorageError {
282 fn from(err: bincode::Error) -> Self {
283 StorageError::Serialization(err.to_string())
284 }
285}
286
287impl From<redb::Error> for PulseDBError {
289 fn from(err: redb::Error) -> Self {
290 PulseDBError::Storage(StorageError::from(err))
291 }
292}
293
294impl From<redb::DatabaseError> for PulseDBError {
295 fn from(err: redb::DatabaseError) -> Self {
296 PulseDBError::Storage(StorageError::from(err))
297 }
298}
299
300impl From<redb::TransactionError> for PulseDBError {
301 fn from(err: redb::TransactionError) -> Self {
302 PulseDBError::Storage(StorageError::from(err))
303 }
304}
305
306impl From<redb::CommitError> for PulseDBError {
307 fn from(err: redb::CommitError) -> Self {
308 PulseDBError::Storage(StorageError::from(err))
309 }
310}
311
312impl From<redb::TableError> for PulseDBError {
313 fn from(err: redb::TableError) -> Self {
314 PulseDBError::Storage(StorageError::from(err))
315 }
316}
317
318impl From<redb::StorageError> for PulseDBError {
319 fn from(err: redb::StorageError) -> Self {
320 PulseDBError::Storage(StorageError::from(err))
321 }
322}
323
324impl From<bincode::Error> for PulseDBError {
325 fn from(err: bincode::Error) -> Self {
326 PulseDBError::Storage(StorageError::from(err))
327 }
328}
329
330#[derive(Debug, Error)]
334pub enum ValidationError {
335 #[error("Embedding dimension mismatch: expected {expected}, got {got}")]
337 DimensionMismatch {
338 expected: usize,
340 got: usize,
342 },
343
344 #[error("Invalid field '{field}': {reason}")]
346 InvalidField {
347 field: String,
349 reason: String,
351 },
352
353 #[error("Content too large: {size} bytes (max: {max} bytes)")]
355 ContentTooLarge {
356 size: usize,
358 max: usize,
360 },
361
362 #[error("Required field missing: {field}")]
364 RequiredField {
365 field: String,
367 },
368
369 #[error("Too many items in '{field}': {count} (max: {max})")]
371 TooManyItems {
372 field: String,
374 count: usize,
376 max: usize,
378 },
379}
380
381impl ValidationError {
382 pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
384 Self::DimensionMismatch { expected, got }
385 }
386
387 pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
389 Self::InvalidField {
390 field: field.into(),
391 reason: reason.into(),
392 }
393 }
394
395 pub fn content_too_large(size: usize, max: usize) -> Self {
397 Self::ContentTooLarge { size, max }
398 }
399
400 pub fn required_field(field: impl Into<String>) -> Self {
402 Self::RequiredField {
403 field: field.into(),
404 }
405 }
406
407 pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
409 Self::TooManyItems {
410 field: field.into(),
411 count,
412 max,
413 }
414 }
415}
416
417#[derive(Debug, Error)]
419pub enum NotFoundError {
420 #[error("Collective not found: {0}")]
422 Collective(String),
423
424 #[error("Experience not found: {0}")]
426 Experience(String),
427
428 #[error("Relation not found: {0}")]
430 Relation(String),
431
432 #[error("Insight not found: {0}")]
434 Insight(String),
435
436 #[error("Activity not found: {0}")]
438 Activity(String),
439}
440
441impl NotFoundError {
442 pub fn collective(id: impl ToString) -> Self {
444 Self::Collective(id.to_string())
445 }
446
447 pub fn experience(id: impl ToString) -> Self {
449 Self::Experience(id.to_string())
450 }
451
452 pub fn relation(id: impl ToString) -> Self {
454 Self::Relation(id.to_string())
455 }
456
457 pub fn insight(id: impl ToString) -> Self {
459 Self::Insight(id.to_string())
460 }
461
462 pub fn activity(id: impl ToString) -> Self {
464 Self::Activity(id.to_string())
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_error_display() {
474 let err = PulseDBError::config("Invalid dimension");
475 assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
476 }
477
478 #[test]
479 fn test_storage_error_display() {
480 let err = StorageError::SchemaVersionMismatch {
481 expected: 2,
482 found: 1,
483 };
484 assert_eq!(
485 err.to_string(),
486 "Schema version mismatch: expected 2, found 1"
487 );
488 }
489
490 #[test]
491 fn test_validation_error_display() {
492 let err = ValidationError::dimension_mismatch(384, 768);
493 assert_eq!(
494 err.to_string(),
495 "Embedding dimension mismatch: expected 384, got 768"
496 );
497 }
498
499 #[test]
500 fn test_not_found_error_display() {
501 let err = NotFoundError::collective("abc-123");
502 assert_eq!(err.to_string(), "Collective not found: abc-123");
503 }
504
505 #[test]
506 fn test_is_not_found() {
507 let err: PulseDBError = NotFoundError::collective("test").into();
508 assert!(err.is_not_found());
509 assert!(!err.is_validation());
510 }
511
512 #[test]
513 fn test_is_validation() {
514 let err: PulseDBError = ValidationError::required_field("content").into();
515 assert!(err.is_validation());
516 assert!(!err.is_not_found());
517 }
518
519 #[test]
520 fn test_vector_error_display() {
521 let err = PulseDBError::vector("HNSW insert failed");
522 assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
523 assert!(err.is_vector());
524 assert!(!err.is_storage());
525 }
526
527 #[test]
528 fn test_error_conversion_chain() {
529 fn inner() -> Result<()> {
531 Err(StorageError::corrupted("test corruption"))?
532 }
533
534 let result = inner();
535 assert!(result.is_err());
536 assert!(result.unwrap_err().is_storage());
537 }
538
539 #[test]
540 fn test_watch_error_display() {
541 let err = PulseDBError::watch("subscribers lock poisoned");
542 assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
543 }
544
545 #[test]
546 fn test_watch_constructor() {
547 let err = PulseDBError::watch("test");
548 assert!(err.is_watch());
549 assert!(!err.is_storage());
550 }
551
552 #[test]
553 fn test_is_watch() {
554 let err = PulseDBError::watch("test");
555 assert!(err.is_watch());
556 assert!(!err.is_not_found());
557 }
558
559 #[test]
560 fn test_is_embedding() {
561 let err = PulseDBError::embedding("model load failed");
562 assert!(err.is_embedding());
563 assert!(!err.is_vector());
564 }
565
566 #[test]
567 fn test_is_internal() {
568 let err = PulseDBError::internal("task join failed");
569 assert!(err.is_internal());
570 assert!(!err.is_storage());
571 }
572
573 #[test]
574 fn test_is_config() {
575 let err = PulseDBError::config("invalid dimension");
576 assert!(err.is_config());
577 assert!(!err.is_validation());
578 }
579
580 #[test]
581 fn test_is_io() {
582 let err = PulseDBError::Io(std::io::Error::new(
583 std::io::ErrorKind::NotFound,
584 "file missing",
585 ));
586 assert!(err.is_io());
587 assert!(!err.is_storage());
588 }
589}