1use std::path::PathBuf;
21use thiserror::Error;
22
23pub type Result<T> = std::result::Result<T, PulseDBError>;
25
26#[derive(Debug, Error)]
31pub enum PulseDBError {
32 #[error("Storage error: {0}")]
34 Storage(#[from] StorageError),
35
36 #[error("Validation error: {0}")]
38 Validation(#[from] ValidationError),
39
40 #[error("Configuration error: {reason}")]
42 Config {
43 reason: String,
45 },
46
47 #[error("{0}")]
49 NotFound(#[from] NotFoundError),
50
51 #[error("I/O error: {0}")]
53 Io(#[from] std::io::Error),
54
55 #[error("Embedding error: {0}")]
57 Embedding(String),
58
59 #[error("Vector index error: {0}")]
61 Vector(String),
62
63 #[error("Watch error: {0}")]
65 Watch(String),
66
67 #[error("Internal error: {0}")]
69 Internal(String),
70}
71
72impl PulseDBError {
73 pub fn config(reason: impl Into<String>) -> Self {
75 Self::Config {
76 reason: reason.into(),
77 }
78 }
79
80 pub fn embedding(msg: impl Into<String>) -> Self {
82 Self::Embedding(msg.into())
83 }
84
85 pub fn vector(msg: impl Into<String>) -> Self {
87 Self::Vector(msg.into())
88 }
89
90 pub fn watch(msg: impl Into<String>) -> Self {
92 Self::Watch(msg.into())
93 }
94
95 pub fn internal(msg: impl Into<String>) -> Self {
97 Self::Internal(msg.into())
98 }
99
100 pub fn is_not_found(&self) -> bool {
102 matches!(self, Self::NotFound(_))
103 }
104
105 pub fn is_validation(&self) -> bool {
107 matches!(self, Self::Validation(_))
108 }
109
110 pub fn is_storage(&self) -> bool {
112 matches!(self, Self::Storage(_))
113 }
114
115 pub fn is_vector(&self) -> bool {
117 matches!(self, Self::Vector(_))
118 }
119
120 pub fn is_watch(&self) -> bool {
122 matches!(self, Self::Watch(_))
123 }
124
125 pub fn is_embedding(&self) -> bool {
127 matches!(self, Self::Embedding(_))
128 }
129
130 pub fn is_internal(&self) -> bool {
132 matches!(self, Self::Internal(_))
133 }
134
135 pub fn is_config(&self) -> bool {
137 matches!(self, Self::Config { .. })
138 }
139
140 pub fn is_io(&self) -> bool {
142 matches!(self, Self::Io(_))
143 }
144}
145
146#[derive(Debug, Error)]
150pub enum StorageError {
151 #[error("Database corrupted: {0}")]
153 Corrupted(String),
154
155 #[error("Database not found: {0}")]
157 DatabaseNotFound(PathBuf),
158
159 #[error("Database is locked by another writer")]
161 DatabaseLocked,
162
163 #[error("Transaction failed: {0}")]
165 Transaction(String),
166
167 #[error("Serialization error: {0}")]
169 Serialization(String),
170
171 #[error("Storage engine error: {0}")]
173 Redb(String),
174
175 #[error("Schema version mismatch: expected {expected}, found {found}")]
177 SchemaVersionMismatch {
178 expected: u32,
180 found: u32,
182 },
183
184 #[error("Table not found: {0}")]
186 TableNotFound(String),
187}
188
189impl StorageError {
190 pub fn corrupted(msg: impl Into<String>) -> Self {
192 Self::Corrupted(msg.into())
193 }
194
195 pub fn transaction(msg: impl Into<String>) -> Self {
197 Self::Transaction(msg.into())
198 }
199
200 pub fn serialization(msg: impl Into<String>) -> Self {
202 Self::Serialization(msg.into())
203 }
204
205 pub fn redb(msg: impl Into<String>) -> Self {
207 Self::Redb(msg.into())
208 }
209}
210
211impl From<redb::Error> for StorageError {
213 fn from(err: redb::Error) -> Self {
214 StorageError::Redb(err.to_string())
215 }
216}
217
218impl From<redb::DatabaseError> for StorageError {
219 fn from(err: redb::DatabaseError) -> Self {
220 StorageError::Redb(err.to_string())
221 }
222}
223
224impl From<redb::TransactionError> for StorageError {
225 fn from(err: redb::TransactionError) -> Self {
226 StorageError::Transaction(err.to_string())
227 }
228}
229
230impl From<redb::CommitError> for StorageError {
231 fn from(err: redb::CommitError) -> Self {
232 StorageError::Transaction(format!("Commit failed: {}", err))
233 }
234}
235
236impl From<redb::TableError> for StorageError {
237 fn from(err: redb::TableError) -> Self {
238 StorageError::Redb(format!("Table error: {}", err))
239 }
240}
241
242impl From<redb::StorageError> for StorageError {
243 fn from(err: redb::StorageError) -> Self {
244 StorageError::Redb(format!("Storage error: {}", err))
245 }
246}
247
248impl From<bincode::Error> for StorageError {
250 fn from(err: bincode::Error) -> Self {
251 StorageError::Serialization(err.to_string())
252 }
253}
254
255impl From<redb::Error> for PulseDBError {
257 fn from(err: redb::Error) -> Self {
258 PulseDBError::Storage(StorageError::from(err))
259 }
260}
261
262impl From<redb::DatabaseError> for PulseDBError {
263 fn from(err: redb::DatabaseError) -> Self {
264 PulseDBError::Storage(StorageError::from(err))
265 }
266}
267
268impl From<redb::TransactionError> for PulseDBError {
269 fn from(err: redb::TransactionError) -> Self {
270 PulseDBError::Storage(StorageError::from(err))
271 }
272}
273
274impl From<redb::CommitError> for PulseDBError {
275 fn from(err: redb::CommitError) -> Self {
276 PulseDBError::Storage(StorageError::from(err))
277 }
278}
279
280impl From<redb::TableError> for PulseDBError {
281 fn from(err: redb::TableError) -> Self {
282 PulseDBError::Storage(StorageError::from(err))
283 }
284}
285
286impl From<redb::StorageError> for PulseDBError {
287 fn from(err: redb::StorageError) -> Self {
288 PulseDBError::Storage(StorageError::from(err))
289 }
290}
291
292impl From<bincode::Error> for PulseDBError {
293 fn from(err: bincode::Error) -> Self {
294 PulseDBError::Storage(StorageError::from(err))
295 }
296}
297
298#[derive(Debug, Error)]
302pub enum ValidationError {
303 #[error("Embedding dimension mismatch: expected {expected}, got {got}")]
305 DimensionMismatch {
306 expected: usize,
308 got: usize,
310 },
311
312 #[error("Invalid field '{field}': {reason}")]
314 InvalidField {
315 field: String,
317 reason: String,
319 },
320
321 #[error("Content too large: {size} bytes (max: {max} bytes)")]
323 ContentTooLarge {
324 size: usize,
326 max: usize,
328 },
329
330 #[error("Required field missing: {field}")]
332 RequiredField {
333 field: String,
335 },
336
337 #[error("Too many items in '{field}': {count} (max: {max})")]
339 TooManyItems {
340 field: String,
342 count: usize,
344 max: usize,
346 },
347}
348
349impl ValidationError {
350 pub fn dimension_mismatch(expected: usize, got: usize) -> Self {
352 Self::DimensionMismatch { expected, got }
353 }
354
355 pub fn invalid_field(field: impl Into<String>, reason: impl Into<String>) -> Self {
357 Self::InvalidField {
358 field: field.into(),
359 reason: reason.into(),
360 }
361 }
362
363 pub fn content_too_large(size: usize, max: usize) -> Self {
365 Self::ContentTooLarge { size, max }
366 }
367
368 pub fn required_field(field: impl Into<String>) -> Self {
370 Self::RequiredField {
371 field: field.into(),
372 }
373 }
374
375 pub fn too_many_items(field: impl Into<String>, count: usize, max: usize) -> Self {
377 Self::TooManyItems {
378 field: field.into(),
379 count,
380 max,
381 }
382 }
383}
384
385#[derive(Debug, Error)]
387pub enum NotFoundError {
388 #[error("Collective not found: {0}")]
390 Collective(String),
391
392 #[error("Experience not found: {0}")]
394 Experience(String),
395
396 #[error("Relation not found: {0}")]
398 Relation(String),
399
400 #[error("Insight not found: {0}")]
402 Insight(String),
403
404 #[error("Activity not found: {0}")]
406 Activity(String),
407}
408
409impl NotFoundError {
410 pub fn collective(id: impl ToString) -> Self {
412 Self::Collective(id.to_string())
413 }
414
415 pub fn experience(id: impl ToString) -> Self {
417 Self::Experience(id.to_string())
418 }
419
420 pub fn relation(id: impl ToString) -> Self {
422 Self::Relation(id.to_string())
423 }
424
425 pub fn insight(id: impl ToString) -> Self {
427 Self::Insight(id.to_string())
428 }
429
430 pub fn activity(id: impl ToString) -> Self {
432 Self::Activity(id.to_string())
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn test_error_display() {
442 let err = PulseDBError::config("Invalid dimension");
443 assert_eq!(err.to_string(), "Configuration error: Invalid dimension");
444 }
445
446 #[test]
447 fn test_storage_error_display() {
448 let err = StorageError::SchemaVersionMismatch {
449 expected: 2,
450 found: 1,
451 };
452 assert_eq!(
453 err.to_string(),
454 "Schema version mismatch: expected 2, found 1"
455 );
456 }
457
458 #[test]
459 fn test_validation_error_display() {
460 let err = ValidationError::dimension_mismatch(384, 768);
461 assert_eq!(
462 err.to_string(),
463 "Embedding dimension mismatch: expected 384, got 768"
464 );
465 }
466
467 #[test]
468 fn test_not_found_error_display() {
469 let err = NotFoundError::collective("abc-123");
470 assert_eq!(err.to_string(), "Collective not found: abc-123");
471 }
472
473 #[test]
474 fn test_is_not_found() {
475 let err: PulseDBError = NotFoundError::collective("test").into();
476 assert!(err.is_not_found());
477 assert!(!err.is_validation());
478 }
479
480 #[test]
481 fn test_is_validation() {
482 let err: PulseDBError = ValidationError::required_field("content").into();
483 assert!(err.is_validation());
484 assert!(!err.is_not_found());
485 }
486
487 #[test]
488 fn test_vector_error_display() {
489 let err = PulseDBError::vector("HNSW insert failed");
490 assert_eq!(err.to_string(), "Vector index error: HNSW insert failed");
491 assert!(err.is_vector());
492 assert!(!err.is_storage());
493 }
494
495 #[test]
496 fn test_error_conversion_chain() {
497 fn inner() -> Result<()> {
499 Err(StorageError::corrupted("test corruption"))?
500 }
501
502 let result = inner();
503 assert!(result.is_err());
504 assert!(result.unwrap_err().is_storage());
505 }
506
507 #[test]
508 fn test_watch_error_display() {
509 let err = PulseDBError::watch("subscribers lock poisoned");
510 assert_eq!(err.to_string(), "Watch error: subscribers lock poisoned");
511 }
512
513 #[test]
514 fn test_watch_constructor() {
515 let err = PulseDBError::watch("test");
516 assert!(err.is_watch());
517 assert!(!err.is_storage());
518 }
519
520 #[test]
521 fn test_is_watch() {
522 let err = PulseDBError::watch("test");
523 assert!(err.is_watch());
524 assert!(!err.is_not_found());
525 }
526
527 #[test]
528 fn test_is_embedding() {
529 let err = PulseDBError::embedding("model load failed");
530 assert!(err.is_embedding());
531 assert!(!err.is_vector());
532 }
533
534 #[test]
535 fn test_is_internal() {
536 let err = PulseDBError::internal("task join failed");
537 assert!(err.is_internal());
538 assert!(!err.is_storage());
539 }
540
541 #[test]
542 fn test_is_config() {
543 let err = PulseDBError::config("invalid dimension");
544 assert!(err.is_config());
545 assert!(!err.is_validation());
546 }
547
548 #[test]
549 fn test_is_io() {
550 let err = PulseDBError::Io(std::io::Error::new(
551 std::io::ErrorKind::NotFound,
552 "file missing",
553 ));
554 assert!(err.is_io());
555 assert!(!err.is_storage());
556 }
557}