1use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32#[non_exhaustive]
33pub enum ErrorCode {
34 QuerySyntax,
37 QuerySemantic,
39 QueryTimeout,
41 QueryUnsupported,
43 QueryOptimization,
45 QueryExecution,
47
48 TransactionConflict,
51 TransactionTimeout,
53 TransactionReadOnly,
55 TransactionInvalidState,
57 TransactionSerialization,
59 TransactionDeadlock,
61
62 StorageFull,
65 StorageCorrupted,
67 StorageRecoveryFailed,
69
70 InvalidInput,
73 NodeNotFound,
75 EdgeNotFound,
77 PropertyNotFound,
79 LabelNotFound,
81 TypeMismatch,
83
84 Internal,
87 SerializationError,
89 IoError,
91}
92
93impl ErrorCode {
94 #[must_use]
96 pub const fn as_str(&self) -> &'static str {
97 match self {
98 Self::QuerySyntax => "GRAFEO-Q001",
99 Self::QuerySemantic => "GRAFEO-Q002",
100 Self::QueryTimeout => "GRAFEO-Q003",
101 Self::QueryUnsupported => "GRAFEO-Q004",
102 Self::QueryOptimization => "GRAFEO-Q005",
103 Self::QueryExecution => "GRAFEO-Q006",
104
105 Self::TransactionConflict => "GRAFEO-T001",
106 Self::TransactionTimeout => "GRAFEO-T002",
107 Self::TransactionReadOnly => "GRAFEO-T003",
108 Self::TransactionInvalidState => "GRAFEO-T004",
109 Self::TransactionSerialization => "GRAFEO-T005",
110 Self::TransactionDeadlock => "GRAFEO-T006",
111
112 Self::StorageFull => "GRAFEO-S001",
113 Self::StorageCorrupted => "GRAFEO-S002",
114 Self::StorageRecoveryFailed => "GRAFEO-S003",
115
116 Self::InvalidInput => "GRAFEO-V001",
117 Self::NodeNotFound => "GRAFEO-V002",
118 Self::EdgeNotFound => "GRAFEO-V003",
119 Self::PropertyNotFound => "GRAFEO-V004",
120 Self::LabelNotFound => "GRAFEO-V005",
121 Self::TypeMismatch => "GRAFEO-V006",
122
123 Self::Internal => "GRAFEO-X001",
124 Self::SerializationError => "GRAFEO-X002",
125 Self::IoError => "GRAFEO-X003",
126 }
127 }
128
129 #[must_use]
131 pub const fn is_retryable(&self) -> bool {
132 matches!(
133 self,
134 Self::TransactionConflict
135 | Self::TransactionTimeout
136 | Self::TransactionDeadlock
137 | Self::QueryTimeout
138 )
139 }
140}
141
142impl fmt::Display for ErrorCode {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 f.write_str(self.as_str())
145 }
146}
147
148#[derive(Debug)]
153#[non_exhaustive]
154pub enum Error {
155 NodeNotFound(crate::types::NodeId),
157
158 EdgeNotFound(crate::types::EdgeId),
160
161 PropertyNotFound(String),
163
164 LabelNotFound(String),
166
167 TypeMismatch {
169 expected: String,
171 found: String,
173 },
174
175 InvalidValue(String),
177
178 Transaction(TransactionError),
180
181 Storage(StorageError),
183
184 Query(QueryError),
186
187 Serialization(String),
189
190 Io(std::io::Error),
192
193 Internal(String),
195}
196
197impl Error {
198 #[must_use]
200 pub fn error_code(&self) -> ErrorCode {
201 match self {
202 Error::NodeNotFound(_) => ErrorCode::NodeNotFound,
203 Error::EdgeNotFound(_) => ErrorCode::EdgeNotFound,
204 Error::PropertyNotFound(_) => ErrorCode::PropertyNotFound,
205 Error::LabelNotFound(_) => ErrorCode::LabelNotFound,
206 Error::TypeMismatch { .. } => ErrorCode::TypeMismatch,
207 Error::InvalidValue(_) => ErrorCode::InvalidInput,
208 Error::Transaction(e) => e.error_code(),
209 Error::Storage(e) => e.error_code(),
210 Error::Query(e) => e.error_code(),
211 Error::Serialization(_) => ErrorCode::SerializationError,
212 Error::Io(_) => ErrorCode::IoError,
213 Error::Internal(_) => ErrorCode::Internal,
214 }
215 }
216}
217
218impl fmt::Display for Error {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 let code = self.error_code();
221 match self {
222 Error::NodeNotFound(id) => write!(f, "{code}: Node not found: {id}"),
223 Error::EdgeNotFound(id) => write!(f, "{code}: Edge not found: {id}"),
224 Error::PropertyNotFound(key) => write!(f, "{code}: Property not found: {key}"),
225 Error::LabelNotFound(label) => write!(f, "{code}: Label not found: {label}"),
226 Error::TypeMismatch { expected, found } => {
227 write!(
228 f,
229 "{code}: Type mismatch: expected {expected}, found {found}"
230 )
231 }
232 Error::InvalidValue(msg) => write!(f, "{code}: Invalid value: {msg}"),
233 Error::Transaction(e) => write!(f, "{code}: {e}"),
234 Error::Storage(e) => write!(f, "{code}: {e}"),
235 Error::Query(e) => write!(f, "{e}"),
236 Error::Serialization(msg) => write!(f, "{code}: Serialization error: {msg}"),
237 Error::Io(e) => write!(f, "{code}: I/O error: {e}"),
238 Error::Internal(msg) => write!(f, "{code}: Internal error: {msg}"),
239 }
240 }
241}
242
243impl std::error::Error for Error {
244 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
245 match self {
246 Error::Io(e) => Some(e),
247 Error::Transaction(e) => Some(e),
248 Error::Storage(e) => Some(e),
249 Error::Query(e) => Some(e),
250 _ => None,
251 }
252 }
253}
254
255impl From<std::io::Error> for Error {
256 fn from(e: std::io::Error) -> Self {
257 Error::Io(e)
258 }
259}
260
261#[derive(Debug, Clone)]
263#[non_exhaustive]
264pub enum TransactionError {
265 Aborted,
267
268 Conflict,
270
271 WriteConflict(String),
273
274 SerializationFailure(String),
279
280 Deadlock,
282
283 Timeout,
285
286 ReadOnly,
288
289 InvalidState(String),
291}
292
293impl TransactionError {
294 #[must_use]
296 pub const fn error_code(&self) -> ErrorCode {
297 match self {
298 Self::Aborted | Self::Conflict | Self::WriteConflict(_) => {
299 ErrorCode::TransactionConflict
300 }
301 Self::SerializationFailure(_) => ErrorCode::TransactionSerialization,
302 Self::Deadlock => ErrorCode::TransactionDeadlock,
303 Self::Timeout => ErrorCode::TransactionTimeout,
304 Self::ReadOnly => ErrorCode::TransactionReadOnly,
305 Self::InvalidState(_) => ErrorCode::TransactionInvalidState,
306 }
307 }
308}
309
310impl fmt::Display for TransactionError {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 match self {
313 TransactionError::Aborted => write!(f, "Transaction aborted"),
314 TransactionError::Conflict => write!(f, "Transaction conflict"),
315 TransactionError::WriteConflict(msg) => write!(f, "Write conflict: {msg}"),
316 TransactionError::SerializationFailure(msg) => {
317 write!(f, "Serialization failure (SSI): {msg}")
318 }
319 TransactionError::Deadlock => write!(f, "Deadlock detected"),
320 TransactionError::Timeout => write!(f, "Transaction timeout"),
321 TransactionError::ReadOnly => write!(f, "Cannot write in read-only transaction"),
322 TransactionError::InvalidState(msg) => write!(f, "Invalid transaction state: {msg}"),
323 }
324 }
325}
326
327impl std::error::Error for TransactionError {}
328
329impl From<TransactionError> for Error {
330 fn from(e: TransactionError) -> Self {
331 Error::Transaction(e)
332 }
333}
334
335#[derive(Debug, Clone)]
337#[non_exhaustive]
338pub enum StorageError {
339 Corruption(String),
341
342 Full,
344
345 InvalidWalEntry(String),
347
348 RecoveryFailed(String),
350
351 CheckpointFailed(String),
353}
354
355impl StorageError {
356 #[must_use]
358 pub const fn error_code(&self) -> ErrorCode {
359 match self {
360 Self::Corruption(_) => ErrorCode::StorageCorrupted,
361 Self::Full => ErrorCode::StorageFull,
362 Self::InvalidWalEntry(_) | Self::CheckpointFailed(_) => ErrorCode::StorageCorrupted,
363 Self::RecoveryFailed(_) => ErrorCode::StorageRecoveryFailed,
364 }
365 }
366}
367
368impl fmt::Display for StorageError {
369 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370 match self {
371 StorageError::Corruption(msg) => write!(f, "Storage corruption: {msg}"),
372 StorageError::Full => write!(f, "Storage is full"),
373 StorageError::InvalidWalEntry(msg) => write!(f, "Invalid WAL entry: {msg}"),
374 StorageError::RecoveryFailed(msg) => write!(f, "Recovery failed: {msg}"),
375 StorageError::CheckpointFailed(msg) => write!(f, "Checkpoint failed: {msg}"),
376 }
377 }
378}
379
380impl std::error::Error for StorageError {}
381
382impl From<StorageError> for Error {
383 fn from(e: StorageError) -> Self {
384 Error::Storage(e)
385 }
386}
387
388#[derive(Debug, Clone)]
394pub struct QueryError {
395 pub kind: QueryErrorKind,
397 pub message: String,
399 pub span: Option<SourceSpan>,
401 pub source_query: Option<String>,
403 pub hint: Option<String>,
405}
406
407impl QueryError {
408 pub fn new(kind: QueryErrorKind, message: impl Into<String>) -> Self {
410 Self {
411 kind,
412 message: message.into(),
413 span: None,
414 source_query: None,
415 hint: None,
416 }
417 }
418
419 #[must_use]
421 pub fn timeout() -> Self {
422 Self::new(QueryErrorKind::Timeout, "Query exceeded timeout")
423 }
424
425 #[must_use]
427 pub fn timeout_with_limit(limit: std::time::Duration) -> Self {
428 let millis = limit.as_millis();
429 let timeout_display = if millis >= 1000 && millis.is_multiple_of(1000) {
430 format!("{}s", millis / 1000)
431 } else {
432 format!("{millis}ms")
433 };
434 Self::new(
435 QueryErrorKind::Timeout,
436 format!("Query exceeded the {timeout_display} timeout"),
437 )
438 .with_hint(
439 "Increase with Config::with_query_timeout() or disable with Config::without_query_timeout()"
440 .to_string(),
441 )
442 }
443
444 #[must_use]
446 pub const fn error_code(&self) -> ErrorCode {
447 match self.kind {
448 QueryErrorKind::Lexer | QueryErrorKind::Syntax => ErrorCode::QuerySyntax,
449 QueryErrorKind::Semantic => ErrorCode::QuerySemantic,
450 QueryErrorKind::Optimization => ErrorCode::QueryOptimization,
451 QueryErrorKind::Execution => ErrorCode::QueryExecution,
452 QueryErrorKind::Timeout => ErrorCode::QueryTimeout,
453 }
454 }
455
456 #[must_use]
458 pub fn with_span(mut self, span: SourceSpan) -> Self {
459 self.span = Some(span);
460 self
461 }
462
463 #[must_use]
465 pub fn with_source(mut self, query: impl Into<String>) -> Self {
466 self.source_query = Some(query.into());
467 self
468 }
469
470 #[must_use]
472 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
473 self.hint = Some(hint.into());
474 self
475 }
476}
477
478impl fmt::Display for QueryError {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 write!(f, "{}: {}", self.kind, self.message)?;
481
482 if let (Some(span), Some(query)) = (&self.span, &self.source_query) {
483 write!(f, "\n --> query:{}:{}", span.line, span.column)?;
484
485 if let Some(line) = query.lines().nth(span.line.saturating_sub(1) as usize) {
487 write!(f, "\n |")?;
488 write!(f, "\n {} | {}", span.line, line)?;
489 write!(f, "\n | ")?;
490
491 for _ in 0..span.column.saturating_sub(1) {
493 write!(f, " ")?;
494 }
495 for _ in span.start..span.end {
496 write!(f, "^")?;
497 }
498 }
499 }
500
501 if let Some(hint) = &self.hint {
502 write!(f, "\n |\n help: {hint}")?;
503 }
504
505 Ok(())
506 }
507}
508
509impl std::error::Error for QueryError {}
510
511impl From<QueryError> for Error {
512 fn from(e: QueryError) -> Self {
513 Error::Query(e)
514 }
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq)]
519#[non_exhaustive]
520pub enum QueryErrorKind {
521 Lexer,
523 Syntax,
525 Semantic,
527 Optimization,
529 Execution,
531 Timeout,
533}
534
535impl fmt::Display for QueryErrorKind {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 match self {
538 QueryErrorKind::Lexer => write!(f, "lexer error"),
539 QueryErrorKind::Syntax => write!(f, "syntax error"),
540 QueryErrorKind::Semantic => write!(f, "semantic error"),
541 QueryErrorKind::Optimization => write!(f, "optimization error"),
542 QueryErrorKind::Execution => write!(f, "execution error"),
543 QueryErrorKind::Timeout => write!(f, "timeout error"),
544 }
545 }
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub struct SourceSpan {
551 pub start: usize,
553 pub end: usize,
555 pub line: u32,
557 pub column: u32,
559}
560
561impl SourceSpan {
562 pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
564 Self {
565 start,
566 end,
567 line,
568 column,
569 }
570 }
571}
572
573pub type Result<T> = std::result::Result<T, Error>;
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn test_error_display() {
582 let err = Error::NodeNotFound(crate::types::NodeId::new(42));
583 assert_eq!(err.to_string(), "GRAFEO-V002: Node not found: 42");
584
585 let err = Error::TypeMismatch {
586 expected: "INT64".to_string(),
587 found: "STRING".to_string(),
588 };
589 assert_eq!(
590 err.to_string(),
591 "GRAFEO-V006: Type mismatch: expected INT64, found STRING"
592 );
593 }
594
595 #[test]
596 fn test_error_codes() {
597 assert_eq!(
598 Error::Internal("x".into()).error_code(),
599 ErrorCode::Internal
600 );
601 assert_eq!(ErrorCode::Internal.as_str(), "GRAFEO-X001");
602 assert!(!ErrorCode::Internal.is_retryable());
603
604 assert_eq!(
605 Error::Transaction(TransactionError::Conflict).error_code(),
606 ErrorCode::TransactionConflict
607 );
608 assert!(ErrorCode::TransactionConflict.is_retryable());
609 assert!(ErrorCode::QueryTimeout.is_retryable());
610 assert!(!ErrorCode::StorageFull.is_retryable());
611 }
612
613 #[test]
614 fn test_query_timeout() {
615 let err = QueryError::timeout();
616 assert_eq!(err.kind, QueryErrorKind::Timeout);
617 assert_eq!(err.error_code(), ErrorCode::QueryTimeout);
618 assert!(err.error_code().is_retryable());
619 assert!(err.message.contains("timeout"));
620 }
621
622 #[test]
623 fn test_query_error_formatting() {
624 let query = "MATCH (n:Peron) RETURN n";
625 let err = QueryError::new(QueryErrorKind::Semantic, "Unknown label 'Peron'")
626 .with_span(SourceSpan::new(9, 14, 1, 10))
627 .with_source(query)
628 .with_hint("Did you mean 'Person'?");
629
630 let msg = err.to_string();
631 assert!(msg.contains("Unknown label 'Peron'"));
632 assert!(msg.contains("query:1:10"));
633 assert!(msg.contains("Did you mean 'Person'?"));
634 }
635
636 #[test]
637 fn test_transaction_error() {
638 let err: Error = TransactionError::Conflict.into();
639 assert!(matches!(
640 err,
641 Error::Transaction(TransactionError::Conflict)
642 ));
643 }
644
645 #[test]
646 fn test_io_error_conversion() {
647 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
648 let err: Error = io_err.into();
649 assert!(matches!(err, Error::Io(_)));
650 }
651}