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