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
6use std::fmt;
7
8/// The main error type - covers everything that can go wrong in Grafeo.
9///
10/// Most methods return `Result<T, Error>`. Use pattern matching to handle
11/// specific cases, or just propagate with `?`.
12#[derive(Debug)]
13pub enum Error {
14    /// A node was not found.
15    NodeNotFound(crate::types::NodeId),
16
17    /// An edge was not found.
18    EdgeNotFound(crate::types::EdgeId),
19
20    /// A property key was not found.
21    PropertyNotFound(String),
22
23    /// A label was not found.
24    LabelNotFound(String),
25
26    /// Type mismatch error.
27    TypeMismatch {
28        /// The expected type.
29        expected: String,
30        /// The actual type found.
31        found: String,
32    },
33
34    /// Invalid value error.
35    InvalidValue(String),
36
37    /// Transaction error.
38    Transaction(TransactionError),
39
40    /// Storage error.
41    Storage(StorageError),
42
43    /// Query error.
44    Query(QueryError),
45
46    /// Serialization error.
47    Serialization(String),
48
49    /// I/O error.
50    Io(std::io::Error),
51
52    /// Internal error (should not happen in normal operation).
53    Internal(String),
54}
55
56impl fmt::Display for Error {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Error::NodeNotFound(id) => write!(f, "Node not found: {id}"),
60            Error::EdgeNotFound(id) => write!(f, "Edge not found: {id}"),
61            Error::PropertyNotFound(key) => write!(f, "Property not found: {key}"),
62            Error::LabelNotFound(label) => write!(f, "Label not found: {label}"),
63            Error::TypeMismatch { expected, found } => {
64                write!(f, "Type mismatch: expected {expected}, found {found}")
65            }
66            Error::InvalidValue(msg) => write!(f, "Invalid value: {msg}"),
67            Error::Transaction(e) => write!(f, "Transaction error: {e}"),
68            Error::Storage(e) => write!(f, "Storage error: {e}"),
69            Error::Query(e) => write!(f, "Query error: {e}"),
70            Error::Serialization(msg) => write!(f, "Serialization error: {msg}"),
71            Error::Io(e) => write!(f, "I/O error: {e}"),
72            Error::Internal(msg) => write!(f, "Internal error: {msg}"),
73        }
74    }
75}
76
77impl std::error::Error for Error {
78    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
79        match self {
80            Error::Io(e) => Some(e),
81            Error::Transaction(e) => Some(e),
82            Error::Storage(e) => Some(e),
83            Error::Query(e) => Some(e),
84            _ => None,
85        }
86    }
87}
88
89impl From<std::io::Error> for Error {
90    fn from(e: std::io::Error) -> Self {
91        Error::Io(e)
92    }
93}
94
95/// Transaction-specific errors.
96#[derive(Debug, Clone)]
97pub enum TransactionError {
98    /// Transaction was aborted.
99    Aborted,
100
101    /// Transaction commit failed due to conflict.
102    Conflict,
103
104    /// Write-write conflict with another transaction.
105    WriteConflict(String),
106
107    /// Serialization failure (SSI violation).
108    ///
109    /// Occurs when running at Serializable isolation level and a read-write
110    /// conflict is detected (we read data that another committed transaction wrote).
111    SerializationFailure(String),
112
113    /// Deadlock detected.
114    Deadlock,
115
116    /// Transaction timed out.
117    Timeout,
118
119    /// Transaction is read-only but attempted a write.
120    ReadOnly,
121
122    /// Invalid transaction state.
123    InvalidState(String),
124}
125
126impl fmt::Display for TransactionError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        match self {
129            TransactionError::Aborted => write!(f, "Transaction aborted"),
130            TransactionError::Conflict => write!(f, "Transaction conflict"),
131            TransactionError::WriteConflict(msg) => write!(f, "Write conflict: {msg}"),
132            TransactionError::SerializationFailure(msg) => {
133                write!(f, "Serialization failure (SSI): {msg}")
134            }
135            TransactionError::Deadlock => write!(f, "Deadlock detected"),
136            TransactionError::Timeout => write!(f, "Transaction timeout"),
137            TransactionError::ReadOnly => write!(f, "Cannot write in read-only transaction"),
138            TransactionError::InvalidState(msg) => write!(f, "Invalid transaction state: {msg}"),
139        }
140    }
141}
142
143impl std::error::Error for TransactionError {}
144
145impl From<TransactionError> for Error {
146    fn from(e: TransactionError) -> Self {
147        Error::Transaction(e)
148    }
149}
150
151/// Storage-specific errors.
152#[derive(Debug, Clone)]
153pub enum StorageError {
154    /// Corruption detected in storage.
155    Corruption(String),
156
157    /// Storage is full.
158    Full,
159
160    /// Invalid WAL entry.
161    InvalidWalEntry(String),
162
163    /// Recovery failed.
164    RecoveryFailed(String),
165
166    /// Checkpoint failed.
167    CheckpointFailed(String),
168}
169
170impl fmt::Display for StorageError {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            StorageError::Corruption(msg) => write!(f, "Storage corruption: {msg}"),
174            StorageError::Full => write!(f, "Storage is full"),
175            StorageError::InvalidWalEntry(msg) => write!(f, "Invalid WAL entry: {msg}"),
176            StorageError::RecoveryFailed(msg) => write!(f, "Recovery failed: {msg}"),
177            StorageError::CheckpointFailed(msg) => write!(f, "Checkpoint failed: {msg}"),
178        }
179    }
180}
181
182impl std::error::Error for StorageError {}
183
184impl From<StorageError> for Error {
185    fn from(e: StorageError) -> Self {
186        Error::Storage(e)
187    }
188}
189
190/// A query error with source location and helpful hints.
191///
192/// When something goes wrong in a query (syntax error, unknown label, type
193/// mismatch), you get one of these. The error message includes the location
194/// in your query and often a suggestion for fixing it.
195#[derive(Debug, Clone)]
196pub struct QueryError {
197    /// What category of error (lexer, syntax, semantic, etc.)
198    pub kind: QueryErrorKind,
199    /// Human-readable explanation of what went wrong.
200    pub message: String,
201    /// Where in the query the error occurred.
202    pub span: Option<SourceSpan>,
203    /// The original query text (for showing context).
204    pub source_query: Option<String>,
205    /// A suggestion for fixing the error.
206    pub hint: Option<String>,
207}
208
209impl QueryError {
210    /// Creates a new query error.
211    pub fn new(kind: QueryErrorKind, message: impl Into<String>) -> Self {
212        Self {
213            kind,
214            message: message.into(),
215            span: None,
216            source_query: None,
217            hint: None,
218        }
219    }
220
221    /// Adds a source span to the error.
222    #[must_use]
223    pub fn with_span(mut self, span: SourceSpan) -> Self {
224        self.span = Some(span);
225        self
226    }
227
228    /// Adds the source query to the error.
229    #[must_use]
230    pub fn with_source(mut self, query: impl Into<String>) -> Self {
231        self.source_query = Some(query.into());
232        self
233    }
234
235    /// Adds a hint to the error.
236    #[must_use]
237    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
238        self.hint = Some(hint.into());
239        self
240    }
241}
242
243impl fmt::Display for QueryError {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "{}: {}", self.kind, self.message)?;
246
247        if let (Some(span), Some(query)) = (&self.span, &self.source_query) {
248            write!(f, "\n  --> query:{}:{}", span.line, span.column)?;
249
250            // Extract and display the relevant line
251            if let Some(line) = query.lines().nth(span.line.saturating_sub(1) as usize) {
252                write!(f, "\n   |")?;
253                write!(f, "\n {} | {}", span.line, line)?;
254                write!(f, "\n   | ")?;
255
256                // Add caret markers
257                for _ in 0..span.column.saturating_sub(1) {
258                    write!(f, " ")?;
259                }
260                for _ in span.start..span.end {
261                    write!(f, "^")?;
262                }
263            }
264        }
265
266        if let Some(hint) = &self.hint {
267            write!(f, "\n   |\n  help: {hint}")?;
268        }
269
270        Ok(())
271    }
272}
273
274impl std::error::Error for QueryError {}
275
276impl From<QueryError> for Error {
277    fn from(e: QueryError) -> Self {
278        Error::Query(e)
279    }
280}
281
282/// The kind of query error.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum QueryErrorKind {
285    /// Lexical error (invalid token).
286    Lexer,
287    /// Syntax error (parse failure).
288    Syntax,
289    /// Semantic error (type mismatch, unknown identifier, etc.).
290    Semantic,
291    /// Optimization error.
292    Optimization,
293    /// Execution error.
294    Execution,
295}
296
297impl fmt::Display for QueryErrorKind {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            QueryErrorKind::Lexer => write!(f, "lexer error"),
301            QueryErrorKind::Syntax => write!(f, "syntax error"),
302            QueryErrorKind::Semantic => write!(f, "semantic error"),
303            QueryErrorKind::Optimization => write!(f, "optimization error"),
304            QueryErrorKind::Execution => write!(f, "execution error"),
305        }
306    }
307}
308
309/// A span in the source code.
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub struct SourceSpan {
312    /// Byte offset of the start.
313    pub start: usize,
314    /// Byte offset of the end.
315    pub end: usize,
316    /// Line number (1-indexed).
317    pub line: u32,
318    /// Column number (1-indexed).
319    pub column: u32,
320}
321
322impl SourceSpan {
323    /// Creates a new source span.
324    pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
325        Self {
326            start,
327            end,
328            line,
329            column,
330        }
331    }
332}
333
334/// A type alias for `Result<T, Error>`.
335pub type Result<T> = std::result::Result<T, Error>;
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_error_display() {
343        let err = Error::NodeNotFound(crate::types::NodeId::new(42));
344        assert_eq!(err.to_string(), "Node not found: 42");
345
346        let err = Error::TypeMismatch {
347            expected: "INT64".to_string(),
348            found: "STRING".to_string(),
349        };
350        assert_eq!(
351            err.to_string(),
352            "Type mismatch: expected INT64, found STRING"
353        );
354    }
355
356    #[test]
357    fn test_query_error_formatting() {
358        let query = "MATCH (n:Peron) RETURN n";
359        let err = QueryError::new(QueryErrorKind::Semantic, "Unknown label 'Peron'")
360            .with_span(SourceSpan::new(9, 14, 1, 10))
361            .with_source(query)
362            .with_hint("Did you mean 'Person'?");
363
364        let msg = err.to_string();
365        assert!(msg.contains("Unknown label 'Peron'"));
366        assert!(msg.contains("query:1:10"));
367        assert!(msg.contains("Did you mean 'Person'?"));
368    }
369
370    #[test]
371    fn test_transaction_error() {
372        let err: Error = TransactionError::Conflict.into();
373        assert!(matches!(
374            err,
375            Error::Transaction(TransactionError::Conflict)
376        ));
377    }
378
379    #[test]
380    fn test_io_error_conversion() {
381        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
382        let err: Error = io_err.into();
383        assert!(matches!(err, Error::Io(_)));
384    }
385}