Skip to main content

linkrs_core/
error.rs

1//! Unified Error Handling System for Linkrs
2//!
3//! ## Design concepts ##
4//!
5//! 1. **Boxed errors**: All sub-errors are boxed to keep DBError small (~24 bytes instead of ~160 bytes)
6//! 2. **Error classification**: Each error has a class for unified handling strategies
7//! 3. **Error chain**: Full error chain support with source tracking
8//! 4. **Public API**: Stable error codes for external API, hiding internal details
9//!
10//! ## Error Size Comparison ##
11//! - Old design: ~160 bytes (embedded sub-errors)
12//! - New design: ~24 bytes (boxed sub-errors)
13
14use std::error::Error;
15use std::sync::Arc;
16use thiserror::Error;
17
18// submodule
19pub mod codes;
20pub mod manager;
21pub mod query;
22pub mod storage;
23
24// Re-export the error code
25pub use codes::{ErrorCategory as CodeErrorCategory, ErrorCode, PublicError, ToPublicError};
26
27// Re-export all error types
28pub use manager::{ErrorCategory, ManagerError, ManagerResult};
29pub use query::PlanNodeVisitError;
30pub use query::QueryError;
31pub use query::QueryResult;
32pub use storage::StorageError;
33pub use storage::StorageResult;
34
35pub use crate::types::DataType;
36
37// ==================== Error Classification ====================
38
39/// Error classification for unified handling strategies
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum ErrorClass {
42    /// Transient errors that can be retried
43    Retryable,
44    /// User input errors (client should fix the request)
45    UserError,
46    /// System errors (server-side issues)
47    SystemError,
48    /// Fatal errors (service should stop)
49    Fatal,
50}
51
52impl ErrorClass {
53    pub fn is_retryable(&self) -> bool {
54        matches!(self, ErrorClass::Retryable)
55    }
56
57    pub fn is_user_error(&self) -> bool {
58        matches!(self, ErrorClass::UserError)
59    }
60
61    pub fn is_system_error(&self) -> bool {
62        matches!(self, ErrorClass::SystemError | ErrorClass::Fatal)
63    }
64}
65
66// ==================== Boxed Error Type ====================
67
68/// Thread-safe boxed error type
69pub type BoxedError = Box<dyn Error + Send + Sync>;
70
71/// Reference-counted error for cloning
72pub type SharedError = Arc<dyn Error + Send + Sync>;
73
74// ==================== Error Kind ====================
75
76/// Error kind enumeration for categorization
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum ErrorKind {
79    Storage,
80    Query,
81    Expression,
82    Plan,
83    Manager,
84    Validation,
85    Io,
86    TypeDeduction,
87    Serialization,
88    Index,
89    Transaction,
90    GraphService,
91    Internal,
92    Session,
93    Auth,
94    Permission,
95    MemoryLimitExceeded,
96    Fulltext,
97    Coordinator,
98    Vector,
99    VectorCoordinator,
100    Search,
101}
102
103impl ErrorKind {
104    pub fn as_str(&self) -> &'static str {
105        match self {
106            ErrorKind::Storage => "storage",
107            ErrorKind::Query => "query",
108            ErrorKind::Expression => "expression",
109            ErrorKind::Plan => "plan",
110            ErrorKind::Manager => "manager",
111            ErrorKind::Validation => "validation",
112            ErrorKind::Io => "io",
113            ErrorKind::TypeDeduction => "type_deduction",
114            ErrorKind::Serialization => "serialization",
115            ErrorKind::Index => "index",
116            ErrorKind::Transaction => "transaction",
117            ErrorKind::GraphService => "graph_service",
118            ErrorKind::Internal => "internal",
119            ErrorKind::Session => "session",
120            ErrorKind::Auth => "auth",
121            ErrorKind::Permission => "permission",
122            ErrorKind::MemoryLimitExceeded => "memory_limit_exceeded",
123            ErrorKind::Fulltext => "fulltext",
124            ErrorKind::Coordinator => "coordinator",
125            ErrorKind::Vector => "vector",
126            ErrorKind::VectorCoordinator => "vector_coordinator",
127            ErrorKind::Search => "search",
128        }
129    }
130}
131
132// ==================== Core DBError ====================
133
134/// Unified database error type
135///
136/// Design principles:
137/// 1. Small size: Uses boxed errors to keep enum size minimal (~24 bytes)
138/// 2. Full context: Preserves error chain and classification
139/// 3. Clone support: Can be cloned for logging/propagation
140#[derive(Error, Debug)]
141pub struct DBError {
142    kind: ErrorKind,
143    message: String,
144    #[source]
145    source: Option<BoxedError>,
146    class: ErrorClass,
147}
148
149impl DBError {
150    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
151        let class = kind.default_class();
152        Self {
153            kind,
154            message: message.into(),
155            source: None,
156            class,
157        }
158    }
159
160    pub fn with_source(mut self, source: BoxedError) -> Self {
161        self.source = Some(source);
162        self
163    }
164
165    pub fn with_class(mut self, class: ErrorClass) -> Self {
166        self.class = class;
167        self
168    }
169
170    pub fn kind(&self) -> ErrorKind {
171        self.kind
172    }
173
174    pub fn class(&self) -> ErrorClass {
175        self.class
176    }
177
178    pub fn message(&self) -> &str {
179        &self.message
180    }
181
182    pub fn is_retryable(&self) -> bool {
183        self.class.is_retryable()
184    }
185
186    pub fn is_user_error(&self) -> bool {
187        self.class.is_user_error()
188    }
189
190    pub fn is_system_error(&self) -> bool {
191        self.class.is_system_error()
192    }
193
194    pub fn source(&self) -> &Option<BoxedError> {
195        &self.source
196    }
197}
198
199impl std::fmt::Display for DBError {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        write!(f, "[{}] {}", self.kind.as_str(), self.message)?;
202        if let Some(ref source) = self.source {
203            write!(f, "\n  Caused by: {}", source)?;
204        }
205        Ok(())
206    }
207}
208
209impl Clone for DBError {
210    fn clone(&self) -> Self {
211        Self {
212            kind: self.kind,
213            message: self.message.clone(),
214            source: None,
215            class: self.class,
216        }
217    }
218}
219
220impl ErrorKind {
221    fn default_class(&self) -> ErrorClass {
222        match self {
223            ErrorKind::Storage => ErrorClass::SystemError,
224            ErrorKind::Query => ErrorClass::UserError,
225            ErrorKind::Expression => ErrorClass::UserError,
226            ErrorKind::Plan => ErrorClass::UserError,
227            ErrorKind::Manager => ErrorClass::SystemError,
228            ErrorKind::Validation => ErrorClass::UserError,
229            ErrorKind::Io => ErrorClass::SystemError,
230            ErrorKind::TypeDeduction => ErrorClass::UserError,
231            ErrorKind::Serialization => ErrorClass::SystemError,
232            ErrorKind::Index => ErrorClass::SystemError,
233            ErrorKind::Transaction => ErrorClass::SystemError,
234            ErrorKind::GraphService => ErrorClass::SystemError,
235            ErrorKind::Internal => ErrorClass::SystemError,
236            ErrorKind::Session => ErrorClass::UserError,
237            ErrorKind::Auth => ErrorClass::UserError,
238            ErrorKind::Permission => ErrorClass::UserError,
239            ErrorKind::MemoryLimitExceeded => ErrorClass::SystemError,
240            ErrorKind::Fulltext => ErrorClass::SystemError,
241            ErrorKind::Coordinator => ErrorClass::SystemError,
242            ErrorKind::Vector => ErrorClass::SystemError,
243            ErrorKind::VectorCoordinator => ErrorClass::SystemError,
244            ErrorKind::Search => ErrorClass::SystemError,
245        }
246    }
247}
248
249// ==================== Convenience Constructors ====================
250
251impl DBError {
252    pub fn storage(message: impl Into<String>) -> Self {
253        Self::new(ErrorKind::Storage, message)
254    }
255
256    pub fn query(message: impl Into<String>) -> Self {
257        Self::new(ErrorKind::Query, message)
258    }
259
260    pub fn validation(message: impl Into<String>) -> Self {
261        Self::new(ErrorKind::Validation, message)
262    }
263
264    pub fn io(message: impl Into<String>) -> Self {
265        Self::new(ErrorKind::Io, message)
266    }
267
268    pub fn internal(message: impl Into<String>) -> Self {
269        Self::new(ErrorKind::Internal, message)
270    }
271
272    pub fn transaction(message: impl Into<String>) -> Self {
273        Self::new(ErrorKind::Transaction, message)
274    }
275
276    pub fn search(message: impl Into<String>) -> Self {
277        Self::new(ErrorKind::Search, message)
278    }
279
280    pub fn expression(message: impl Into<String>) -> Self {
281        Self::new(ErrorKind::Expression, message)
282    }
283
284    pub fn plan(message: impl Into<String>) -> Self {
285        Self::new(ErrorKind::Plan, message)
286    }
287
288    pub fn manager(message: impl Into<String>) -> Self {
289        Self::new(ErrorKind::Manager, message)
290    }
291
292    pub fn session(message: impl Into<String>) -> Self {
293        Self::new(ErrorKind::Session, message)
294    }
295
296    pub fn auth(message: impl Into<String>) -> Self {
297        Self::new(ErrorKind::Auth, message)
298    }
299
300    pub fn permission(message: impl Into<String>) -> Self {
301        Self::new(ErrorKind::Permission, message)
302    }
303
304    pub fn fulltext(message: impl Into<String>) -> Self {
305        Self::new(ErrorKind::Fulltext, message)
306    }
307
308    pub fn vector(message: impl Into<String>) -> Self {
309        Self::new(ErrorKind::Vector, message)
310    }
311
312    pub fn index(message: impl Into<String>) -> Self {
313        Self::new(ErrorKind::Index, message)
314    }
315}
316
317// ==================== From Implementations ====================
318
319impl From<serde_json::Error> for DBError {
320    fn from(err: serde_json::Error) -> Self {
321        DBError::new(ErrorKind::Serialization, err.to_string())
322    }
323}
324
325impl From<std::io::Error> for DBError {
326    fn from(err: std::io::Error) -> Self {
327        DBError::io(err.to_string())
328    }
329}
330
331impl From<query::QueryError> for DBError {
332    fn from(err: query::QueryError) -> Self {
333        DBError::query(err.to_string()).with_source(Box::new(err))
334    }
335}
336
337impl From<storage::StorageError> for DBError {
338    fn from(err: storage::StorageError) -> Self {
339        let class = if err.is_retryable() {
340            ErrorClass::Retryable
341        } else {
342            ErrorClass::SystemError
343        };
344        DBError::storage(err.to_string())
345            .with_source(Box::new(err))
346            .with_class(class)
347    }
348}
349
350// ==================== ToPublicError Implementation ====================
351
352impl ToPublicError for DBError {
353    fn to_public_error(&self) -> PublicError {
354        PublicError::new(self.to_error_code(), self.to_public_message())
355    }
356
357    fn to_error_code(&self) -> ErrorCode {
358        match self.kind {
359            ErrorKind::Storage => {
360                if let Some(ref source) = self.source {
361                    if let Some(se) = source.downcast_ref::<StorageError>() {
362                        return se.to_error_code();
363                    }
364                }
365                ErrorCode::InternalError
366            }
367            ErrorKind::Query => {
368                if let Some(ref source) = self.source {
369                    if let Some(qe) = source.downcast_ref::<QueryError>() {
370                        return qe.to_error_code();
371                    }
372                }
373                ErrorCode::ExecutionError
374            }
375            ErrorKind::Expression | ErrorKind::Plan => ErrorCode::ExecutionError,
376            ErrorKind::Manager => {
377                if let Some(ref source) = self.source {
378                    if let Some(me) = source.downcast_ref::<ManagerError>() {
379                        return me.to_error_code();
380                    }
381                }
382                ErrorCode::InternalError
383            }
384            ErrorKind::Validation | ErrorKind::TypeDeduction => ErrorCode::ValidationError,
385            ErrorKind::Io | ErrorKind::Serialization | ErrorKind::Index => ErrorCode::InternalError,
386            ErrorKind::Transaction | ErrorKind::GraphService => ErrorCode::ExecutionError,
387            ErrorKind::Internal => ErrorCode::InternalError,
388            ErrorKind::Session => ErrorCode::Unauthorized,
389            ErrorKind::Auth => ErrorCode::Unauthorized,
390            ErrorKind::Permission => ErrorCode::PermissionDenied,
391            ErrorKind::MemoryLimitExceeded => ErrorCode::ResourceExhausted,
392            ErrorKind::Fulltext | ErrorKind::Coordinator => ErrorCode::ExecutionError,
393            ErrorKind::Vector | ErrorKind::VectorCoordinator => ErrorCode::ExecutionError,
394            ErrorKind::Search => ErrorCode::ExecutionError,
395        }
396    }
397
398    fn to_public_message(&self) -> String {
399        match self.kind {
400            ErrorKind::Internal => "Internal server error".to_string(),
401            ErrorKind::Io => "IO operation failed".to_string(),
402            ErrorKind::Serialization => "Data serialization failed".to_string(),
403            ErrorKind::Index => "Index operation failed".to_string(),
404            ErrorKind::GraphService => "Graph service error".to_string(),
405            _ => self.message.clone(),
406        }
407    }
408}
409
410// ==================== Result Type Aliases ====================
411
412/// Harmonized result types
413pub type DBResult<T> = Result<T, DBError>;
414
415// LinkrsResult was previously aliased here for backward compatibility.
416// Use DBResult directly for new code.
417
418// ==================== Tests ====================
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_dberror_size() {
426        assert!(
427            std::mem::size_of::<DBError>() <= 64,
428            "DBError should be small"
429        );
430    }
431
432    #[test]
433    fn test_dberror_creation() {
434        let storage_err = StorageError::node_not_found(
435            crate::types::VertexId::try_from_int64(42).expect("test vertex id"),
436        );
437        let db_err: DBError = storage_err.into();
438        assert_eq!(db_err.kind(), ErrorKind::Storage);
439        assert!(!db_err.is_retryable());
440    }
441
442    #[test]
443    fn test_error_conversion() {
444        let query_err = QueryError::parse_error("test error");
445        let db_err: DBError = query_err.into();
446        assert_eq!(db_err.kind(), ErrorKind::Query);
447        assert!(db_err.is_user_error());
448    }
449
450    #[test]
451    fn test_error_class() {
452        let err = DBError::validation("test");
453        assert!(err.is_user_error());
454        assert!(!err.is_retryable());
455    }
456
457    #[test]
458    fn test_retryable_error() {
459        let storage_err = StorageError::lock_timeout("test".to_string());
460        let db_err: DBError = storage_err.into();
461        assert!(db_err.is_retryable());
462    }
463
464    #[test]
465    fn test_error_display() {
466        let err = DBError::query("test query error");
467        let display = format!("{}", err);
468        assert!(display.contains("query"));
469        assert!(display.contains("test query error"));
470    }
471}