Skip to main content

linkrs_core/error/
storage.rs

1//! Storage layer error type
2//!
3//! Errors related to the underlying storage operations of the database.
4//!
5//! ## Design
6//!
7//! `StorageError` is a struct with boxed source error to keep size small (~24 bytes).
8//! This follows the same pattern as `DBError` and `QueryError` for consistency.
9
10use std::error::Error;
11
12use super::BoxedError;
13use crate::error::codes::{ErrorCode, PublicError, ToPublicError};
14
15/// Storage layer result type
16pub type StorageResult<T> = Result<T, StorageError>;
17
18/// Storage error kind enumeration
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum StorageErrorKind {
21    DbError,
22    StorageError,
23    SerializeError,
24    DeserializeError,
25    NodeNotFound,
26    EdgeNotFound,
27    NotSupported,
28    Conflict,
29    LockTimeout,
30    Deadlock,
31    IOError,
32    NotFound,
33    AlreadyExists,
34    InvalidInput,
35    ParseError,
36    VertexNotFound,
37    VertexAlreadyExists,
38    EdgeAlreadyExists,
39    LabelNotFound,
40    LabelAlreadyExists,
41    PropertyNotFound,
42    ColumnNotFound,
43    ColumnAlreadyExists,
44    StorageNotOpen,
45    CapacityExceeded,
46    NullValueNotAllowed,
47    TypeMismatch,
48    InvalidOffset,
49    InvalidOperation,
50    WalError,
51    CompressError,
52    DecompressError,
53    DataCorruption,
54    HistoryBeforeFloor,
55}
56
57impl StorageErrorKind {
58    pub fn as_str(&self) -> &'static str {
59        match self {
60            StorageErrorKind::DbError => "db_error",
61            StorageErrorKind::StorageError => "storage_error",
62            StorageErrorKind::SerializeError => "serialize_error",
63            StorageErrorKind::DeserializeError => "deserialize_error",
64            StorageErrorKind::NodeNotFound => "node_not_found",
65            StorageErrorKind::EdgeNotFound => "edge_not_found",
66            StorageErrorKind::NotSupported => "not_supported",
67            StorageErrorKind::Conflict => "conflict",
68            StorageErrorKind::LockTimeout => "lock_timeout",
69            StorageErrorKind::Deadlock => "deadlock",
70            StorageErrorKind::IOError => "io_error",
71            StorageErrorKind::NotFound => "not_found",
72            StorageErrorKind::AlreadyExists => "already_exists",
73            StorageErrorKind::InvalidInput => "invalid_input",
74            StorageErrorKind::ParseError => "parse_error",
75            StorageErrorKind::VertexNotFound => "vertex_not_found",
76            StorageErrorKind::VertexAlreadyExists => "vertex_already_exists",
77            StorageErrorKind::EdgeAlreadyExists => "edge_already_exists",
78            StorageErrorKind::LabelNotFound => "label_not_found",
79            StorageErrorKind::LabelAlreadyExists => "label_already_exists",
80            StorageErrorKind::PropertyNotFound => "property_not_found",
81            StorageErrorKind::ColumnNotFound => "column_not_found",
82            StorageErrorKind::ColumnAlreadyExists => "column_already_exists",
83            StorageErrorKind::StorageNotOpen => "storage_not_open",
84            StorageErrorKind::CapacityExceeded => "capacity_exceeded",
85            StorageErrorKind::NullValueNotAllowed => "null_value_not_allowed",
86            StorageErrorKind::TypeMismatch => "type_mismatch",
87            StorageErrorKind::InvalidOffset => "invalid_offset",
88            StorageErrorKind::InvalidOperation => "invalid_operation",
89            StorageErrorKind::WalError => "wal_error",
90            StorageErrorKind::CompressError => "compress_error",
91            StorageErrorKind::DecompressError => "decompress_error",
92            StorageErrorKind::DataCorruption => "data_corruption",
93            StorageErrorKind::HistoryBeforeFloor => "history_before_floor",
94        }
95    }
96}
97
98/// Storage layer error type
99///
100/// Design principles:
101/// 1. Small size: Uses boxed errors to keep struct size minimal (~24 bytes)
102/// 2. Full context: Preserves error chain
103/// 3. Clone support: Can be cloned for logging/propagation
104#[derive(Debug)]
105pub struct StorageError {
106    kind: StorageErrorKind,
107    message: String,
108    source: Option<BoxedError>,
109}
110
111impl StorageError {
112    pub fn new(kind: StorageErrorKind, message: impl Into<String>) -> Self {
113        Self {
114            kind,
115            message: message.into(),
116            source: None,
117        }
118    }
119
120    pub fn with_source(mut self, source: BoxedError) -> Self {
121        self.source = Some(source);
122        self
123    }
124
125    pub fn kind(&self) -> StorageErrorKind {
126        self.kind
127    }
128
129    pub fn message(&self) -> &str {
130        &self.message
131    }
132
133    pub fn source(&self) -> &Option<BoxedError> {
134        &self.source
135    }
136
137    fn from_boxed<E: Error + Send + Sync + 'static>(kind: StorageErrorKind, error: E) -> Self {
138        Self {
139            kind,
140            message: error.to_string(),
141            source: Some(Box::new(error)),
142        }
143    }
144
145    pub fn is_retryable(&self) -> bool {
146        matches!(
147            self.kind,
148            StorageErrorKind::LockTimeout | StorageErrorKind::Deadlock
149        )
150    }
151
152    // Convenience constructors
153    pub fn db_error(message: impl Into<String>) -> Self {
154        Self::new(StorageErrorKind::DbError, message)
155    }
156
157    #[allow(clippy::self_named_constructors)]
158    pub fn storage_error(message: impl Into<String>) -> Self {
159        Self::new(StorageErrorKind::StorageError, message)
160    }
161
162    pub fn serialize_error(message: impl Into<String>) -> Self {
163        Self::new(StorageErrorKind::SerializeError, message)
164    }
165
166    pub fn deserialize_error(message: impl Into<String>) -> Self {
167        Self::new(StorageErrorKind::DeserializeError, message)
168    }
169
170    pub fn node_not_found(value: crate::types::VertexId) -> Self {
171        Self::new(StorageErrorKind::NodeNotFound, format!("{}", value))
172    }
173
174    pub fn edge_not_found(value: crate::types::VertexId) -> Self {
175        Self::new(StorageErrorKind::EdgeNotFound, format!("{}", value))
176    }
177
178    pub fn not_supported(message: impl Into<String>) -> Self {
179        Self::new(StorageErrorKind::NotSupported, message)
180    }
181
182    pub fn conflict(message: impl Into<String>) -> Self {
183        Self::new(StorageErrorKind::Conflict, message)
184    }
185
186    /// A write-write conflict detected at the storage layer write path.
187    ///
188    /// Two writes touched the same entity with incompatible timestamps (e.g.
189    /// deleting an edge that another write already deleted at a different
190    /// timestamp). This is the first line of defense; the transaction layer's
191    /// commit-time WriteSet certification remains the fallback.
192    pub fn write_write_conflict(entity: impl Into<String>) -> Self {
193        Self::new(
194            StorageErrorKind::Conflict,
195            format!("Write-write conflict: {}", entity.into()),
196        )
197    }
198
199    pub fn lock_timeout(message: impl Into<String>) -> Self {
200        Self::new(StorageErrorKind::LockTimeout, message)
201    }
202
203    pub fn deadlock() -> Self {
204        Self::new(StorageErrorKind::Deadlock, "Deadlock detected")
205    }
206
207    pub fn io_error(message: impl Into<String>) -> Self {
208        Self::new(StorageErrorKind::IOError, message)
209    }
210
211    pub fn not_found(message: impl Into<String>) -> Self {
212        Self::new(StorageErrorKind::NotFound, message)
213    }
214
215    pub fn already_exists(message: impl Into<String>) -> Self {
216        Self::new(StorageErrorKind::AlreadyExists, message)
217    }
218
219    pub fn invalid_input(message: impl Into<String>) -> Self {
220        Self::new(StorageErrorKind::InvalidInput, message)
221    }
222
223    pub fn parse_error(message: impl Into<String>) -> Self {
224        Self::new(StorageErrorKind::ParseError, message)
225    }
226
227    pub fn vertex_not_found() -> Self {
228        Self::new(StorageErrorKind::VertexNotFound, "Vertex not found")
229    }
230
231    pub fn vertex_already_exists(message: impl Into<String>) -> Self {
232        Self::new(StorageErrorKind::VertexAlreadyExists, message)
233    }
234
235    pub fn edge_already_exists(message: impl Into<String>) -> Self {
236        Self::new(StorageErrorKind::EdgeAlreadyExists, message)
237    }
238
239    pub fn label_not_found(message: impl Into<String>) -> Self {
240        Self::new(StorageErrorKind::LabelNotFound, message)
241    }
242
243    pub fn label_already_exists(message: impl Into<String>) -> Self {
244        Self::new(StorageErrorKind::LabelAlreadyExists, message)
245    }
246
247    pub fn property_not_found(message: impl Into<String>) -> Self {
248        Self::new(StorageErrorKind::PropertyNotFound, message)
249    }
250
251    pub fn column_not_found(message: impl Into<String>) -> Self {
252        Self::new(StorageErrorKind::ColumnNotFound, message)
253    }
254
255    pub fn column_already_exists(message: impl Into<String>) -> Self {
256        Self::new(StorageErrorKind::ColumnAlreadyExists, message)
257    }
258
259    pub fn storage_not_open() -> Self {
260        Self::new(StorageErrorKind::StorageNotOpen, "Storage not open")
261    }
262
263    pub fn capacity_exceeded() -> Self {
264        Self::new(StorageErrorKind::CapacityExceeded, "Capacity exceeded")
265    }
266
267    pub fn null_value_not_allowed(column: impl Into<String>) -> Self {
268        Self::new(
269            StorageErrorKind::NullValueNotAllowed,
270            format!("Null value not allowed for column: {}", column.into()),
271        )
272    }
273
274    pub fn type_mismatch(expected: crate::DataType, actual: crate::DataType) -> Self {
275        Self::new(
276            StorageErrorKind::TypeMismatch,
277            format!("Type mismatch: expected {:?}, got {:?}", expected, actual),
278        )
279    }
280
281    pub fn invalid_offset(offset: u32) -> Self {
282        Self::new(
283            StorageErrorKind::InvalidOffset,
284            format!("Invalid offset: {}", offset),
285        )
286    }
287
288    pub fn invalid_operation(message: impl Into<String>) -> Self {
289        Self::new(StorageErrorKind::InvalidOperation, message)
290    }
291
292    pub fn wal_error(message: impl Into<String>) -> Self {
293        Self::new(StorageErrorKind::WalError, message)
294    }
295
296    pub fn compress_error(message: impl Into<String>) -> Self {
297        Self::new(StorageErrorKind::CompressError, message)
298    }
299
300    pub fn decompress_error(message: impl Into<String>) -> Self {
301        Self::new(StorageErrorKind::DecompressError, message)
302    }
303
304    pub fn data_corruption(message: impl Into<String>) -> Self {
305        Self::new(StorageErrorKind::DataCorruption, message)
306    }
307
308    pub fn history_before_floor(row: u32, ts: u64, floor: u64) -> Self {
309        Self::new(
310            StorageErrorKind::HistoryBeforeFloor,
311            format!(
312                "vertex row {} history at {} before epoch floor {} is not retained; version chains do not survive checkpoints",
313                row, ts, floor
314            ),
315        )
316    }
317
318    pub fn history_before_floor_edge(edge: impl Into<String>, ts: u64, floor: u64) -> Self {
319        Self::new(
320            StorageErrorKind::HistoryBeforeFloor,
321            format!(
322                "edge {} history at {} before epoch floor {} is not retained; version chains do not survive checkpoints",
323                edge.into(),
324                ts,
325                floor
326            ),
327        )
328    }
329}
330
331impl std::fmt::Display for StorageError {
332    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333        write!(f, "[{}] {}", self.kind.as_str(), self.message)?;
334        if let Some(ref source) = self.source {
335            write!(f, "\n  Caused by: {}", source)?;
336        }
337        Ok(())
338    }
339}
340
341impl Clone for StorageError {
342    fn clone(&self) -> Self {
343        Self {
344            kind: self.kind,
345            message: self.message.clone(),
346            source: None,
347        }
348    }
349}
350
351impl std::error::Error for StorageError {
352    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
353        self.source
354            .as_ref()
355            .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
356    }
357}
358
359impl From<std::io::Error> for StorageError {
360    fn from(e: std::io::Error) -> Self {
361        Self::from_boxed(StorageErrorKind::IOError, e)
362    }
363}
364
365impl From<String> for StorageError {
366    fn from(s: String) -> Self {
367        Self::db_error(s)
368    }
369}
370
371impl From<&str> for StorageError {
372    fn from(s: &str) -> Self {
373        Self::db_error(s)
374    }
375}
376
377impl<T> From<std::sync::PoisonError<T>> for StorageError {
378    fn from(e: std::sync::PoisonError<T>) -> Self {
379        Self::db_error(e.to_string())
380    }
381}
382
383impl From<postcard::Error> for StorageError {
384    fn from(e: postcard::Error) -> Self {
385        Self::from_boxed(StorageErrorKind::SerializeError, e)
386    }
387}
388
389impl From<crate::types::UndoLogError> for StorageError {
390    fn from(e: crate::types::UndoLogError) -> Self {
391        Self::db_error(e.to_string())
392    }
393}
394
395impl ToPublicError for StorageError {
396    fn to_public_error(&self) -> PublicError {
397        PublicError::new(self.to_error_code(), self.to_public_message())
398    }
399
400    fn to_error_code(&self) -> ErrorCode {
401        match self.kind {
402            StorageErrorKind::NodeNotFound
403            | StorageErrorKind::EdgeNotFound
404            | StorageErrorKind::NotFound => ErrorCode::ResourceNotFound,
405            StorageErrorKind::AlreadyExists => ErrorCode::ResourceAlreadyExists,
406            StorageErrorKind::InvalidInput
407            | StorageErrorKind::InvalidOperation
408            | StorageErrorKind::HistoryBeforeFloor => ErrorCode::InvalidInput,
409            StorageErrorKind::LockTimeout => ErrorCode::Timeout,
410            StorageErrorKind::Deadlock => ErrorCode::Deadlock,
411            StorageErrorKind::Conflict => ErrorCode::Conflict,
412            StorageErrorKind::NotSupported => ErrorCode::InvalidStatement,
413            _ => ErrorCode::InternalError,
414        }
415    }
416
417    fn to_public_message(&self) -> String {
418        match self.kind {
419            StorageErrorKind::NodeNotFound => "Node does not exist".to_string(),
420            StorageErrorKind::EdgeNotFound => "Edge does not exist".to_string(),
421            StorageErrorKind::NotFound => format!("Resource not found: {}", self.message),
422            StorageErrorKind::AlreadyExists => format!("Resource already exists: {}", self.message),
423            _ => "Storage operation failed".to_string(),
424        }
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_storage_error_size() {
434        assert!(
435            std::mem::size_of::<StorageError>() <= 64,
436            "StorageError should be small, got {} bytes",
437            std::mem::size_of::<StorageError>()
438        );
439    }
440
441    #[test]
442    fn test_storage_error_creation() {
443        let err = StorageError::not_found("test resource");
444        assert_eq!(err.kind(), StorageErrorKind::NotFound);
445        assert!(err.message().contains("test resource"));
446    }
447
448    #[test]
449    fn test_retryable_error() {
450        let err = StorageError::lock_timeout("test");
451        assert!(err.is_retryable());
452
453        let err = StorageError::deadlock();
454        assert!(err.is_retryable());
455
456        let err = StorageError::not_found("test");
457        assert!(!err.is_retryable());
458    }
459}