Skip to main content

grafeo_common/utils/
error.rs

1//! Error types for Grafeo operations.
2//!
3//! [`Error`] is the main error type you'll encounter. For query-specific errors,
4//! [`QueryError`] includes source location and hints to help users fix issues.
5//!
6//! Every error carries a machine-readable [`ErrorCode`] (e.g. `GRAFEO-Q001`)
7//! for programmatic handling across the ecosystem (core, server, web, bindings).
8
9use std::fmt;
10
11/// Machine-readable error code for programmatic error handling.
12///
13/// Error codes follow the pattern `GRAFEO-{category}{number}`:
14/// - **Q**: Query errors (parse, semantic, timeout)
15/// - **T**: Transaction errors (conflict, timeout, state)
16/// - **S**: Storage errors (full, corruption)
17/// - **V**: Validation errors (not found, type mismatch, invalid input)
18/// - **X**: Internal errors (should not happen)
19///
20/// Clients can match on these codes without parsing error messages.
21///
22/// # Examples
23///
24/// ```
25/// use grafeo_common::utils::error::{Error, ErrorCode};
26///
27/// let err = Error::Internal("something broke".into());
28/// assert_eq!(err.error_code().as_str(), "GRAFEO-X001");
29/// assert!(!err.error_code().is_retryable());
30/// ```
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ErrorCode {
33    // Query errors (Q)
34    /// Query failed to parse.
35    QuerySyntax,
36    /// Query parsed but is invalid (unknown label, type mismatch, etc.).
37    QuerySemantic,
38    /// Query exceeded timeout.
39    QueryTimeout,
40    /// Feature not supported for this query language.
41    QueryUnsupported,
42    /// Query optimization failed.
43    QueryOptimization,
44    /// Query execution failed.
45    QueryExecution,
46
47    // Transaction errors (T)
48    /// Write-write conflict (retry possible).
49    TransactionConflict,
50    /// Transaction exceeded TTL.
51    TransactionTimeout,
52    /// Transaction is read-only but attempted a write.
53    TransactionReadOnly,
54    /// Invalid transaction state.
55    TransactionInvalidState,
56    /// Serialization failure (SSI violation).
57    TransactionSerialization,
58    /// Deadlock detected.
59    TransactionDeadlock,
60
61    // Storage errors (S)
62    /// Memory or disk limit reached.
63    StorageFull,
64    /// WAL or data corruption detected.
65    StorageCorrupted,
66    /// Recovery from WAL failed.
67    StorageRecoveryFailed,
68
69    // Validation errors (V)
70    /// Request validation failed.
71    InvalidInput,
72    /// Node not found.
73    NodeNotFound,
74    /// Edge not found.
75    EdgeNotFound,
76    /// Property key not found.
77    PropertyNotFound,
78    /// Label not found.
79    LabelNotFound,
80    /// Type mismatch.
81    TypeMismatch,
82
83    // Internal errors (X)
84    /// Unexpected internal error.
85    Internal,
86    /// Serialization/deserialization error.
87    SerializationError,
88    /// I/O error.
89    IoError,
90}
91
92impl ErrorCode {
93    /// Returns the string code (e.g. `"GRAFEO-Q001"`).
94    #[must_use]
95    pub const fn as_str(&self) -> &'static str {
96        match self {
97            Self::QuerySyntax => "GRAFEO-Q001",
98            Self::QuerySemantic => "GRAFEO-Q002",
99            Self::QueryTimeout => "GRAFEO-Q003",
100            Self::QueryUnsupported => "GRAFEO-Q004",
101            Self::QueryOptimization => "GRAFEO-Q005",
102            Self::QueryExecution => "GRAFEO-Q006",
103
104            Self::TransactionConflict => "GRAFEO-T001",
105            Self::TransactionTimeout => "GRAFEO-T002",
106            Self::TransactionReadOnly => "GRAFEO-T003",
107            Self::TransactionInvalidState => "GRAFEO-T004",
108            Self::TransactionSerialization => "GRAFEO-T005",
109            Self::TransactionDeadlock => "GRAFEO-T006",
110
111            Self::StorageFull => "GRAFEO-S001",
112            Self::StorageCorrupted => "GRAFEO-S002",
113            Self::StorageRecoveryFailed => "GRAFEO-S003",
114
115            Self::InvalidInput => "GRAFEO-V001",
116            Self::NodeNotFound => "GRAFEO-V002",
117            Self::EdgeNotFound => "GRAFEO-V003",
118            Self::PropertyNotFound => "GRAFEO-V004",
119            Self::LabelNotFound => "GRAFEO-V005",
120            Self::TypeMismatch => "GRAFEO-V006",
121
122            Self::Internal => "GRAFEO-X001",
123            Self::SerializationError => "GRAFEO-X002",
124            Self::IoError => "GRAFEO-X003",
125        }
126    }
127
128    /// Whether this error is retryable (client should retry the operation).
129    #[must_use]
130    pub const fn is_retryable(&self) -> bool {
131        matches!(
132            self,
133            Self::TransactionConflict
134                | Self::TransactionTimeout
135                | Self::TransactionDeadlock
136                | Self::QueryTimeout
137        )
138    }
139}
140
141impl fmt::Display for ErrorCode {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(self.as_str())
144    }
145}
146
147/// The main error type - covers everything that can go wrong in Grafeo.
148///
149/// Most methods return `Result<T, Error>`. Use pattern matching to handle
150/// specific cases, or just propagate with `?`.
151#[derive(Debug)]
152pub enum Error {
153    /// A node was not found.
154    NodeNotFound(crate::types::NodeId),
155
156    /// An edge was not found.
157    EdgeNotFound(crate::types::EdgeId),
158
159    /// A property key was not found.
160    PropertyNotFound(String),
161
162    /// A label was not found.
163    LabelNotFound(String),
164
165    /// Type mismatch error.
166    TypeMismatch {
167        /// The expected type.
168        expected: String,
169        /// The actual type found.
170        found: String,
171    },
172
173    /// Invalid value error.
174    InvalidValue(String),
175
176    /// Transaction error.
177    Transaction(TransactionError),
178
179    /// Storage error.
180    Storage(StorageError),
181
182    /// Query error.
183    Query(QueryError),
184
185    /// Serialization error.
186    Serialization(String),
187
188    /// I/O error.
189    Io(std::io::Error),
190
191    /// Internal error (should not happen in normal operation).
192    Internal(String),
193}
194
195impl Error {
196    /// Returns the machine-readable error code for this error.
197    #[must_use]
198    pub fn error_code(&self) -> ErrorCode {
199        match self {
200            Error::NodeNotFound(_) => ErrorCode::NodeNotFound,
201            Error::EdgeNotFound(_) => ErrorCode::EdgeNotFound,
202            Error::PropertyNotFound(_) => ErrorCode::PropertyNotFound,
203            Error::LabelNotFound(_) => ErrorCode::LabelNotFound,
204            Error::TypeMismatch { .. } => ErrorCode::TypeMismatch,
205            Error::InvalidValue(_) => ErrorCode::InvalidInput,
206            Error::Transaction(e) => e.error_code(),
207            Error::Storage(e) => e.error_code(),
208            Error::Query(e) => e.error_code(),
209            Error::Serialization(_) => ErrorCode::SerializationError,
210            Error::Io(_) => ErrorCode::IoError,
211            Error::Internal(_) => ErrorCode::Internal,
212        }
213    }
214}
215
216impl fmt::Display for Error {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        let code = self.error_code();
219        match self {
220            Error::NodeNotFound(id) => write!(f, "{code}: Node not found: {id}"),
221            Error::EdgeNotFound(id) => write!(f, "{code}: Edge not found: {id}"),
222            Error::PropertyNotFound(key) => write!(f, "{code}: Property not found: {key}"),
223            Error::LabelNotFound(label) => write!(f, "{code}: Label not found: {label}"),
224            Error::TypeMismatch { expected, found } => {
225                write!(
226                    f,
227                    "{code}: Type mismatch: expected {expected}, found {found}"
228                )
229            }
230            Error::InvalidValue(msg) => write!(f, "{code}: Invalid value: {msg}"),
231            Error::Transaction(e) => write!(f, "{code}: {e}"),
232            Error::Storage(e) => write!(f, "{code}: {e}"),
233            Error::Query(e) => write!(f, "{e}"),
234            Error::Serialization(msg) => write!(f, "{code}: Serialization error: {msg}"),
235            Error::Io(e) => write!(f, "{code}: I/O error: {e}"),
236            Error::Internal(msg) => write!(f, "{code}: Internal error: {msg}"),
237        }
238    }
239}
240
241impl std::error::Error for Error {
242    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
243        match self {
244            Error::Io(e) => Some(e),
245            Error::Transaction(e) => Some(e),
246            Error::Storage(e) => Some(e),
247            Error::Query(e) => Some(e),
248            _ => None,
249        }
250    }
251}
252
253impl From<std::io::Error> for Error {
254    fn from(e: std::io::Error) -> Self {
255        Error::Io(e)
256    }
257}
258
259/// Transaction-specific errors.
260#[derive(Debug, Clone)]
261pub enum TransactionError {
262    /// Transaction was aborted.
263    Aborted,
264
265    /// Transaction commit failed due to conflict.
266    Conflict,
267
268    /// Write-write conflict with another transaction.
269    WriteConflict(String),
270
271    /// Serialization failure (SSI violation).
272    ///
273    /// Occurs when running at Serializable isolation level and a read-write
274    /// conflict is detected (we read data that another committed transaction wrote).
275    SerializationFailure(String),
276
277    /// Deadlock detected.
278    Deadlock,
279
280    /// Transaction timed out.
281    Timeout,
282
283    /// Transaction is read-only but attempted a write.
284    ReadOnly,
285
286    /// Invalid transaction state.
287    InvalidState(String),
288}
289
290impl TransactionError {
291    /// Returns the machine-readable error code for this transaction error.
292    #[must_use]
293    pub const fn error_code(&self) -> ErrorCode {
294        match self {
295            Self::Aborted | Self::Conflict | Self::WriteConflict(_) => {
296                ErrorCode::TransactionConflict
297            }
298            Self::SerializationFailure(_) => ErrorCode::TransactionSerialization,
299            Self::Deadlock => ErrorCode::TransactionDeadlock,
300            Self::Timeout => ErrorCode::TransactionTimeout,
301            Self::ReadOnly => ErrorCode::TransactionReadOnly,
302            Self::InvalidState(_) => ErrorCode::TransactionInvalidState,
303        }
304    }
305}
306
307impl fmt::Display for TransactionError {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self {
310            TransactionError::Aborted => write!(f, "Transaction aborted"),
311            TransactionError::Conflict => write!(f, "Transaction conflict"),
312            TransactionError::WriteConflict(msg) => write!(f, "Write conflict: {msg}"),
313            TransactionError::SerializationFailure(msg) => {
314                write!(f, "Serialization failure (SSI): {msg}")
315            }
316            TransactionError::Deadlock => write!(f, "Deadlock detected"),
317            TransactionError::Timeout => write!(f, "Transaction timeout"),
318            TransactionError::ReadOnly => write!(f, "Cannot write in read-only transaction"),
319            TransactionError::InvalidState(msg) => write!(f, "Invalid transaction state: {msg}"),
320        }
321    }
322}
323
324impl std::error::Error for TransactionError {}
325
326impl From<TransactionError> for Error {
327    fn from(e: TransactionError) -> Self {
328        Error::Transaction(e)
329    }
330}
331
332/// Storage-specific errors.
333#[derive(Debug, Clone)]
334pub enum StorageError {
335    /// Corruption detected in storage.
336    Corruption(String),
337
338    /// Storage is full.
339    Full,
340
341    /// Invalid WAL entry.
342    InvalidWalEntry(String),
343
344    /// Recovery failed.
345    RecoveryFailed(String),
346
347    /// Checkpoint failed.
348    CheckpointFailed(String),
349}
350
351impl StorageError {
352    /// Returns the machine-readable error code for this storage error.
353    #[must_use]
354    pub const fn error_code(&self) -> ErrorCode {
355        match self {
356            Self::Corruption(_) => ErrorCode::StorageCorrupted,
357            Self::Full => ErrorCode::StorageFull,
358            Self::InvalidWalEntry(_) | Self::CheckpointFailed(_) => ErrorCode::StorageCorrupted,
359            Self::RecoveryFailed(_) => ErrorCode::StorageRecoveryFailed,
360        }
361    }
362}
363
364impl fmt::Display for StorageError {
365    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366        match self {
367            StorageError::Corruption(msg) => write!(f, "Storage corruption: {msg}"),
368            StorageError::Full => write!(f, "Storage is full"),
369            StorageError::InvalidWalEntry(msg) => write!(f, "Invalid WAL entry: {msg}"),
370            StorageError::RecoveryFailed(msg) => write!(f, "Recovery failed: {msg}"),
371            StorageError::CheckpointFailed(msg) => write!(f, "Checkpoint failed: {msg}"),
372        }
373    }
374}
375
376impl std::error::Error for StorageError {}
377
378impl From<StorageError> for Error {
379    fn from(e: StorageError) -> Self {
380        Error::Storage(e)
381    }
382}
383
384/// A query error with source location and helpful hints.
385///
386/// When something goes wrong in a query (syntax error, unknown label, type
387/// mismatch), you get one of these. The error message includes the location
388/// in your query and often a suggestion for fixing it.
389#[derive(Debug, Clone)]
390pub struct QueryError {
391    /// What category of error (lexer, syntax, semantic, etc.)
392    pub kind: QueryErrorKind,
393    /// Human-readable explanation of what went wrong.
394    pub message: String,
395    /// Where in the query the error occurred.
396    pub span: Option<SourceSpan>,
397    /// The original query text (for showing context).
398    pub source_query: Option<String>,
399    /// A suggestion for fixing the error.
400    pub hint: Option<String>,
401}
402
403impl QueryError {
404    /// Creates a new query error.
405    pub fn new(kind: QueryErrorKind, message: impl Into<String>) -> Self {
406        Self {
407            kind,
408            message: message.into(),
409            span: None,
410            source_query: None,
411            hint: None,
412        }
413    }
414
415    /// Creates a query timeout error.
416    #[must_use]
417    pub fn timeout() -> Self {
418        Self::new(QueryErrorKind::Execution, "Query exceeded timeout")
419    }
420
421    /// Returns the machine-readable error code for this query error.
422    #[must_use]
423    pub const fn error_code(&self) -> ErrorCode {
424        match self.kind {
425            QueryErrorKind::Lexer | QueryErrorKind::Syntax => ErrorCode::QuerySyntax,
426            QueryErrorKind::Semantic => ErrorCode::QuerySemantic,
427            QueryErrorKind::Optimization => ErrorCode::QueryOptimization,
428            QueryErrorKind::Execution => ErrorCode::QueryExecution,
429        }
430    }
431
432    /// Adds a source span to the error.
433    #[must_use]
434    pub fn with_span(mut self, span: SourceSpan) -> Self {
435        self.span = Some(span);
436        self
437    }
438
439    /// Adds the source query to the error.
440    #[must_use]
441    pub fn with_source(mut self, query: impl Into<String>) -> Self {
442        self.source_query = Some(query.into());
443        self
444    }
445
446    /// Adds a hint to the error.
447    #[must_use]
448    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
449        self.hint = Some(hint.into());
450        self
451    }
452}
453
454impl fmt::Display for QueryError {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        write!(f, "{}: {}", self.kind, self.message)?;
457
458        if let (Some(span), Some(query)) = (&self.span, &self.source_query) {
459            write!(f, "\n  --> query:{}:{}", span.line, span.column)?;
460
461            // Extract and display the relevant line
462            if let Some(line) = query.lines().nth(span.line.saturating_sub(1) as usize) {
463                write!(f, "\n   |")?;
464                write!(f, "\n {} | {}", span.line, line)?;
465                write!(f, "\n   | ")?;
466
467                // Add caret markers
468                for _ in 0..span.column.saturating_sub(1) {
469                    write!(f, " ")?;
470                }
471                for _ in span.start..span.end {
472                    write!(f, "^")?;
473                }
474            }
475        }
476
477        if let Some(hint) = &self.hint {
478            write!(f, "\n   |\n  help: {hint}")?;
479        }
480
481        Ok(())
482    }
483}
484
485impl std::error::Error for QueryError {}
486
487impl From<QueryError> for Error {
488    fn from(e: QueryError) -> Self {
489        Error::Query(e)
490    }
491}
492
493/// The kind of query error.
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum QueryErrorKind {
496    /// Lexical error (invalid token).
497    Lexer,
498    /// Syntax error (parse failure).
499    Syntax,
500    /// Semantic error (type mismatch, unknown identifier, etc.).
501    Semantic,
502    /// Optimization error.
503    Optimization,
504    /// Execution error.
505    Execution,
506}
507
508impl fmt::Display for QueryErrorKind {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        match self {
511            QueryErrorKind::Lexer => write!(f, "lexer error"),
512            QueryErrorKind::Syntax => write!(f, "syntax error"),
513            QueryErrorKind::Semantic => write!(f, "semantic error"),
514            QueryErrorKind::Optimization => write!(f, "optimization error"),
515            QueryErrorKind::Execution => write!(f, "execution error"),
516        }
517    }
518}
519
520/// A span in the source code.
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522pub struct SourceSpan {
523    /// Byte offset of the start.
524    pub start: usize,
525    /// Byte offset of the end.
526    pub end: usize,
527    /// Line number (1-indexed).
528    pub line: u32,
529    /// Column number (1-indexed).
530    pub column: u32,
531}
532
533impl SourceSpan {
534    /// Creates a new source span.
535    pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
536        Self {
537            start,
538            end,
539            line,
540            column,
541        }
542    }
543}
544
545/// A type alias for `Result<T, Error>`.
546pub type Result<T> = std::result::Result<T, Error>;
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    #[test]
553    fn test_error_display() {
554        let err = Error::NodeNotFound(crate::types::NodeId::new(42));
555        assert_eq!(err.to_string(), "GRAFEO-V002: Node not found: 42");
556
557        let err = Error::TypeMismatch {
558            expected: "INT64".to_string(),
559            found: "STRING".to_string(),
560        };
561        assert_eq!(
562            err.to_string(),
563            "GRAFEO-V006: Type mismatch: expected INT64, found STRING"
564        );
565    }
566
567    #[test]
568    fn test_error_codes() {
569        assert_eq!(
570            Error::Internal("x".into()).error_code(),
571            ErrorCode::Internal
572        );
573        assert_eq!(ErrorCode::Internal.as_str(), "GRAFEO-X001");
574        assert!(!ErrorCode::Internal.is_retryable());
575
576        assert_eq!(
577            Error::Transaction(TransactionError::Conflict).error_code(),
578            ErrorCode::TransactionConflict
579        );
580        assert!(ErrorCode::TransactionConflict.is_retryable());
581        assert!(ErrorCode::QueryTimeout.is_retryable());
582        assert!(!ErrorCode::StorageFull.is_retryable());
583    }
584
585    #[test]
586    fn test_query_timeout() {
587        let err = QueryError::timeout();
588        assert_eq!(err.kind, QueryErrorKind::Execution);
589        assert!(err.message.contains("timeout"));
590    }
591
592    #[test]
593    fn test_query_error_formatting() {
594        let query = "MATCH (n:Peron) RETURN n";
595        let err = QueryError::new(QueryErrorKind::Semantic, "Unknown label 'Peron'")
596            .with_span(SourceSpan::new(9, 14, 1, 10))
597            .with_source(query)
598            .with_hint("Did you mean 'Person'?");
599
600        let msg = err.to_string();
601        assert!(msg.contains("Unknown label 'Peron'"));
602        assert!(msg.contains("query:1:10"));
603        assert!(msg.contains("Did you mean 'Person'?"));
604    }
605
606    #[test]
607    fn test_transaction_error() {
608        let err: Error = TransactionError::Conflict.into();
609        assert!(matches!(
610            err,
611            Error::Transaction(TransactionError::Conflict)
612        ));
613    }
614
615    #[test]
616    fn test_io_error_conversion() {
617        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
618        let err: Error = io_err.into();
619        assert!(matches!(err, Error::Io(_)));
620    }
621}