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