Skip to main content

graphos_common/utils/
error.rs

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