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