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