noxu_persist/error.rs
1//! Error types for the persistence layer.
2//!
3//! # v1.5.1 cleanup (Wave 1C, persist-xa Low audit)
4//!
5//! Four `PersistError` variants used to live here that no production
6//! call site ever returned: `EntityNotFound`, `DuplicateKey`,
7//! `InvalidEntity`, `StoreAlreadyOpen`. They were removed because
8//! the public surface advertises an error variant the engine cannot
9//! actually emit. Migration:
10//!
11//! * `EntityNotFound` — `PrimaryIndex::get` returns `Result<Option<E>>`
12//! already; absence is `Ok(None)`, never an error.
13//! * `DuplicateKey` — `PrimaryIndex::put` is overwrite-by-default; if
14//! the user wants "insert or fail", they should use the
15//! `Database::put_no_overwrite` shape and check the
16//! `OperationStatus::KeyExist` status bit instead.
17//! * `InvalidEntity` — entity validation is the application's
18//! responsibility; raise an application-level error type or
19//! `PersistError::SerializationError` instead.
20//! * `StoreAlreadyOpen` — `EntityStore::open` cannot fail with this:
21//! each call constructs a fresh handle.
22
23use thiserror::Error;
24
25/// Errors that can occur in the persistence layer.
26///
27#[derive(Debug, Error)]
28pub enum PersistError {
29 /// An error from the underlying database layer.
30 #[error("database error: {0}")]
31 DatabaseError(#[from] noxu_db::NoxuError),
32
33 /// An error occurred during serialization or deserialization.
34 #[error("serialization error: {0}")]
35 SerializationError(String),
36
37 /// The entity store is not open.
38 #[error("store not open")]
39 StoreNotOpen,
40
41 /// The requested index is not available.
42 #[error("index not available: {0}")]
43 IndexNotAvailable(String),
44
45 /// A primary write was performed inside a user transaction while the
46 /// `PrimaryIndex` had registered (in-memory) secondary indexes.
47 ///
48 /// In v1.5 DPL secondary indexes are in-memory only and their updates
49 /// are NOT atomic with the surrounding transaction: secondary mutations
50 /// are applied immediately on the primary `put` / `delete_with_entity`
51 /// call regardless of whether the user later commits or aborts the
52 /// transaction. This variant is **not** returned from those methods —
53 /// they continue to succeed — but it is constructed and emitted as a
54 /// `log::warn!` (one-shot per `PrimaryIndex`) so operators have a
55 /// machine-greppable signal that the limitation applies to them.
56 ///
57 /// Tracking issue: persistent secondaries are scoped for v1.6.
58 #[error(
59 "DPL secondary indexes are in-memory only in v1.5; secondary \
60 updates are not atomic with the user transaction (see \
61 docs/src/collections/entity-persistence.md, v1.5 limitations)"
62 )]
63 SecondariesNotTransactional,
64}
65
66/// Result type for persistence layer operations.
67pub type Result<T> = std::result::Result<T, PersistError>;
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_serialization_error_display() {
75 let err = PersistError::SerializationError("bad format".to_string());
76 assert_eq!(err.to_string(), "serialization error: bad format");
77 }
78
79 #[test]
80 fn test_store_not_open_display() {
81 let err = PersistError::StoreNotOpen;
82 assert_eq!(err.to_string(), "store not open");
83 }
84
85 #[test]
86 fn test_index_not_available_display() {
87 let err = PersistError::IndexNotAvailable("email_idx".to_string());
88 assert_eq!(err.to_string(), "index not available: email_idx");
89 }
90
91 #[test]
92 fn test_secondaries_not_transactional_display() {
93 let err = PersistError::SecondariesNotTransactional;
94 let msg = err.to_string();
95 assert!(msg.contains("secondary indexes"));
96 assert!(msg.contains("in-memory"));
97 }
98
99 #[test]
100 fn test_database_error_from() {
101 let db_err = noxu_db::NoxuError::DatabaseClosed;
102 let err: PersistError = db_err.into();
103 assert!(matches!(err, PersistError::DatabaseError(_)));
104 assert!(err.to_string().contains("database error"));
105 }
106}