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 #[error(
232 "Database substrate format {found} is older than the current format {current}; \
233 open the database writable once to migrate it (read-only opens of an \
234 un-migrated store cannot upgrade)"
235 )]
236 SubstrateUpgradeRequired {
237 found: u8,
239 current: u8,
241 },
242
243 #[error(
249 "Database substrate format {found} is newer than this build's format {current}; \
250 upgrade PulseDB to open this database (do not modify it with an older build)"
251 )]
252 SubstrateFormatTooNew {
253 found: u8,
255 current: u8,
257 },
258
259 #[error(
274 "store too large for a single-transaction codec migration: store size {store_size} bytes \
275 (projected peak ~{projected_peak} bytes) exceeds the single-txn budget of {budget} bytes; \
276 declare available memory via Config to raise the ceiling, or run the offline migration tool"
277 )]
278 SubstrateMigrationTooLarge {
279 store_size: u64,
281 projected_peak: u64,
283 budget: u64,
285 },
286
287 #[error(
307 "insufficient free disk space for the codec migration: store size {store_size} bytes \
308 needs ~{required} bytes free (pristine backup + migrated file + transaction margin) \
309 but only {available} bytes are available; free up disk or run the offline migration tool"
310 )]
311 SubstrateMigrationInsufficientDisk {
312 store_size: u64,
314 required: u64,
316 available: u64,
318 },
319
320 #[error(
328 "cannot migrate this database's sync state without the `sync` feature: it \
329 contains sync_cursors rows that require a sync-enabled PulseDB build to \
330 re-encode; rebuild or run PulseDB with the `sync` feature to migrate it"
331 )]
332 SubstrateMigrationRequiresSync,
333}
334
335impl StorageError {
336 pub fn corrupted(msg: impl Into<String>) -> Self {
338 Self::Corrupted(msg.into())
339 }
340
341 pub fn transaction(msg: impl Into<String>) -> Self {
343 Self::Transaction(msg.into())
344 }
345
346 pub fn serialization(msg: impl Into<String>) -> Self {
348 Self::Serialization(msg.into())
349 }
350
351 pub fn redb(msg: impl Into<String>) -> Self {
353 Self::Redb(msg.into())
354 }
355
356 pub fn substrate_upgrade_required(found: u8, current: u8) -> Self {
361 Self::SubstrateUpgradeRequired { found, current }
362 }
363
364 pub fn substrate_format_too_new(found: u8, current: u8) -> Self {
366 Self::SubstrateFormatTooNew { found, current }
367 }
368
369 pub fn substrate_migration_too_large(
371 store_size: u64,
372 projected_peak: u64,
373 budget: u64,
374 ) -> Self {
375 Self::SubstrateMigrationTooLarge {
376 store_size,
377 projected_peak,
378 budget,
379 }
380 }
381
382 pub fn substrate_migration_insufficient_disk(
384 store_size: u64,
385 required: u64,
386 available: u64,
387 ) -> Self {
388 Self::SubstrateMigrationInsufficientDisk {
389 store_size,
390 required,
391 available,
392 }
393 }
394}
395
396impl From<redb::Error> for StorageError {
398 fn from(err: redb::Error) -> Self {
399 StorageError::Redb(err.to_string())
400 }
401}
402
403impl From<redb::DatabaseError> for StorageError {
404 fn from(err: redb::DatabaseError) -> Self {
405 StorageError::Redb(err.to_string())
406 }
407}
408
409impl From<redb::TransactionError> for StorageError {
410 fn from(err: redb::TransactionError) -> Self {
411 StorageError::Transaction(err.to_string())
412 }
413}
414
415impl From<redb::CommitError> for StorageError {
416 fn from(err: redb::CommitError) -> Self {
417 StorageError::Transaction(format!("Commit failed: {}", err))
418 }
419}
420
421impl From<redb::TableError> for StorageError {
422 fn from(err: redb::TableError) -> Self {
423 StorageError::Redb(format!("Table error: {}", err))
424 }
425}
426
427impl From<redb::StorageError> for StorageError {
428 fn from(err: redb::StorageError) -> Self {
429 StorageError::Redb(format!("Storage error: {}", err))
430 }
431}
432
433impl From<postcard::Error> for StorageError {
435 fn from(err: postcard::Error) -> Self {
436 StorageError::Serialization(err.to_string())
437 }
438}
439
440impl From<redb::Error> for PulseDBError {
442 fn from(err: redb::Error) -> Self {
443 PulseDBError::Storage(StorageError::from(err))
444 }
445}
446
447impl From<redb::DatabaseError> for PulseDBError {
448 fn from(err: redb::DatabaseError) -> Self {
449 PulseDBError::Storage(StorageError::from(err))
450 }
451}
452
453impl From<redb::TransactionError> for PulseDBError {
454 fn from(err: redb::TransactionError) -> Self {
455 PulseDBError::Storage(StorageError::from(err))
456 }
457}
458
459impl From<redb::CommitError> for PulseDBError {
460 fn from(err: redb::CommitError) -> Self {
461 PulseDBError::Storage(StorageError::from(err))
462 }
463}
464
465impl From<redb::TableError> for PulseDBError {
466 fn from(err: redb::TableError) -> Self {
467 PulseDBError::Storage(StorageError::from(err))
468 }
469}
470
471impl From<redb::StorageError> for PulseDBError {
472 fn from(err: redb::StorageError) -> Self {
473 PulseDBError::Storage(StorageError::from(err))
474 }
475}
476
477impl From<postcard::Error> for PulseDBError {
478 fn from(err: postcard::Error) -> Self {
479 PulseDBError::Storage(StorageError::from(err))
480 }
481}
482
483#[derive(Debug, Error)]
487pub enum ValidationError {
488 #[error("Embedding dimension mismatch: expected {expected}, got {got}")]
490 DimensionMismatch {
491 expected: usize,
493 got: usize,
495 },
496
497 #[error("Invalid field '{field}': {reason}")]
499 InvalidField {
500 field: String,
502 reason: String,
504 },
505
506 #[error("Content too large: {size} bytes (max: {max} bytes)")]
508 ContentTooLarge {
509 size: usize,
511 max: usize,
513 },
514
515 #[error("Required field missing: {field}")]
517 RequiredField {
518 field: String,
520 },
521
522 #[error("Too many items in '{field}': {count} (max: {max})")]
524 TooManyItems {
525 field: String,
527 count: usize,
529 max: usize,
531 },
532}
533
534impl ValidationError {
535 pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
537 Self::DimensionMismatch { expected, got }
538 }
539
540 pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
542 Self::InvalidField {
543 field: field.into(),
544 reason: reason.into(),
545 }
546 }
547
548 pub fn content_too_large(size: usize, max: usize) -> Self {
550 Self::ContentTooLarge { size, max }
551 }
552
553 pub fn required_field(field: impl Into<String>) -> Self {
555 Self::RequiredField {
556 field: field.into(),
557 }
558 }
559
560 pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
562 Self::TooManyItems {
563 field: field.into(),
564 count,
565 max,
566 }
567 }
568}
569
570#[derive(Debug, Error)]
572pub enum NotFoundError {
573 #[error("Collective not found: {0}")]
575 Collective(String),
576
577 #[error("Experience not found: {0}")]
579 Experience(String),
580
581 #[error("Relation not found: {0}")]
583 Relation(String),
584
585 #[error("Insight not found: {0}")]
587 Insight(String),
588
589 #[error("Activity not found: {0}")]
591 Activity(String),
592}
593
594impl NotFoundError {
595 pub fn collective(id: impl ToString) -> Self {
597 Self::Collective(id.to_string())
598 }
599
600 pub fn experience(id: impl ToString) -> Self {
602 Self::Experience(id.to_string())
603 }
604
605 pub fn relation(id: impl ToString) -> Self {
607 Self::Relation(id.to_string())
608 }
609
610 pub fn insight(id: impl ToString) -> Self {
612 Self::Insight(id.to_string())
613 }
614
615 pub fn activity(id: impl ToString) -> Self {
617 Self::Activity(id.to_string())
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use super::*;
624
625 #[test]
626 fn test_error_display() {
627 let err = PulseDBError::config("Invalid dimension");
628 assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
629 }
630
631 #[test]
632 fn test_storage_error_display() {
633 let err = StorageError::SchemaVersionMismatch {
634 expected: 2,
635 found: 1,
636 };
637 assert_eq!(
638 err.to_string(),
639 "Schema version mismatch: expected 2, found 1"
640 );
641 }
642
643 #[test]
644 fn test_validation_error_display() {
645 let err = ValidationError::dimension_mismatch(384, 768);
646 assert_eq!(
647 err.to_string(),
648 "Embedding dimension mismatch: expected 384, got 768"
649 );
650 }
651
652 #[test]
653 fn test_not_found_error_display() {
654 let err = NotFoundError::collective("abc-123");
655 assert_eq!(err.to_string(), "Collective not found: abc-123");
656 }
657
658 #[test]
659 fn test_is_not_found() {
660 let err: PulseDBError = NotFoundError::collective("test").into();
661 assert!(err.is_not_found());
662 assert!(!err.is_validation());
663 }
664
665 #[test]
666 fn test_is_validation() {
667 let err: PulseDBError = ValidationError::required_field("content").into();
668 assert!(err.is_validation());
669 assert!(!err.is_not_found());
670 }
671
672 #[test]
673 fn test_vector_error_display() {
674 let err = PulseDBError::vector("HNSW insert failed");
675 assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
676 assert!(err.is_vector());
677 assert!(!err.is_storage());
678 }
679
680 #[test]
681 fn test_error_conversion_chain() {
682 fn inner() -> Result<()> {
684 Err(StorageError::corrupted("test corruption"))?
685 }
686
687 let result = inner();
688 assert!(result.is_err());
689 assert!(result.unwrap_err().is_storage());
690 }
691
692 #[test]
693 fn test_watch_error_display() {
694 let err = PulseDBError::watch("subscribers lock poisoned");
695 assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
696 }
697
698 #[test]
699 fn test_watch_constructor() {
700 let err = PulseDBError::watch("test");
701 assert!(err.is_watch());
702 assert!(!err.is_storage());
703 }
704
705 #[test]
706 fn test_is_watch() {
707 let err = PulseDBError::watch("test");
708 assert!(err.is_watch());
709 assert!(!err.is_not_found());
710 }
711
712 #[test]
713 fn test_is_embedding() {
714 let err = PulseDBError::embedding("model load failed");
715 assert!(err.is_embedding());
716 assert!(!err.is_vector());
717 }
718
719 #[test]
720 fn test_is_internal() {
721 let err = PulseDBError::internal("task join failed");
722 assert!(err.is_internal());
723 assert!(!err.is_storage());
724 }
725
726 #[test]
727 fn test_is_config() {
728 let err = PulseDBError::config("invalid dimension");
729 assert!(err.is_config());
730 assert!(!err.is_validation());
731 }
732
733 #[test]
734 fn test_is_io() {
735 let err = PulseDBError::Io(std::io::Error::new(
736 std::io::ErrorKind::NotFound,
737 "file missing",
738 ));
739 assert!(err.is_io());
740 assert!(!err.is_storage());
741 }
742
743 #[test]
744 fn test_substrate_upgrade_required_display_is_actionable() {
745 let err = StorageError::substrate_upgrade_required(0, 1);
746 assert!(matches!(
747 err,
748 StorageError::SubstrateUpgradeRequired {
749 found: 0,
750 current: 1
751 }
752 ));
753 let msg = err.to_string();
754 assert!(msg.contains("substrate format 0"), "msg: {msg}");
756 assert!(msg.contains("current format 1"), "msg: {msg}");
757 assert!(msg.contains("writable"), "msg: {msg}");
758 }
759
760 #[test]
761 fn test_substrate_format_too_new_display_is_actionable() {
762 let err = StorageError::substrate_format_too_new(7, 1);
763 assert!(matches!(
764 err,
765 StorageError::SubstrateFormatTooNew {
766 found: 7,
767 current: 1
768 }
769 ));
770 let msg = err.to_string();
771 assert!(msg.contains("substrate format 7"), "msg: {msg}");
772 assert!(msg.contains("format 1"), "msg: {msg}");
773 assert!(msg.contains("upgrade PulseDB"), "msg: {msg}");
775 }
776
777 #[test]
778 fn test_substrate_errors_propagate_as_storage() {
779 let err: PulseDBError = StorageError::substrate_upgrade_required(0, 1).into();
780 assert!(err.is_storage());
781 let err: PulseDBError = StorageError::substrate_format_too_new(2, 1).into();
782 assert!(err.is_storage());
783 }
784}