1use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum ErrorCode {
33 QuerySyntax,
36 QuerySemantic,
38 QueryTimeout,
40 QueryUnsupported,
42 QueryOptimization,
44 QueryExecution,
46
47 TransactionConflict,
50 TransactionTimeout,
52 TransactionReadOnly,
54 TransactionInvalidState,
56 TransactionSerialization,
58 TransactionDeadlock,
60
61 StorageFull,
64 StorageCorrupted,
66 StorageRecoveryFailed,
68
69 InvalidInput,
72 NodeNotFound,
74 EdgeNotFound,
76 PropertyNotFound,
78 LabelNotFound,
80 TypeMismatch,
82
83 Internal,
86 SerializationError,
88 IoError,
90}
91
92impl ErrorCode {
93 #[must_use]
95 pub const fn as_str(&self) -> &'static str {
96 match self {
97 Self::QuerySyntax => "GRAFEO-Q001",
98 Self::QuerySemantic => "GRAFEO-Q002",
99 Self::QueryTimeout => "GRAFEO-Q003",
100 Self::QueryUnsupported => "GRAFEO-Q004",
101 Self::QueryOptimization => "GRAFEO-Q005",
102 Self::QueryExecution => "GRAFEO-Q006",
103
104 Self::TransactionConflict => "GRAFEO-T001",
105 Self::TransactionTimeout => "GRAFEO-T002",
106 Self::TransactionReadOnly => "GRAFEO-T003",
107 Self::TransactionInvalidState => "GRAFEO-T004",
108 Self::TransactionSerialization => "GRAFEO-T005",
109 Self::TransactionDeadlock => "GRAFEO-T006",
110
111 Self::StorageFull => "GRAFEO-S001",
112 Self::StorageCorrupted => "GRAFEO-S002",
113 Self::StorageRecoveryFailed => "GRAFEO-S003",
114
115 Self::InvalidInput => "GRAFEO-V001",
116 Self::NodeNotFound => "GRAFEO-V002",
117 Self::EdgeNotFound => "GRAFEO-V003",
118 Self::PropertyNotFound => "GRAFEO-V004",
119 Self::LabelNotFound => "GRAFEO-V005",
120 Self::TypeMismatch => "GRAFEO-V006",
121
122 Self::Internal => "GRAFEO-X001",
123 Self::SerializationError => "GRAFEO-X002",
124 Self::IoError => "GRAFEO-X003",
125 }
126 }
127
128 #[must_use]
130 pub const fn is_retryable(&self) -> bool {
131 matches!(
132 self,
133 Self::TransactionConflict
134 | Self::TransactionTimeout
135 | Self::TransactionDeadlock
136 | Self::QueryTimeout
137 )
138 }
139}
140
141impl fmt::Display for ErrorCode {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(self.as_str())
144 }
145}
146
147#[derive(Debug)]
152pub enum Error {
153 NodeNotFound(crate::types::NodeId),
155
156 EdgeNotFound(crate::types::EdgeId),
158
159 PropertyNotFound(String),
161
162 LabelNotFound(String),
164
165 TypeMismatch {
167 expected: String,
169 found: String,
171 },
172
173 InvalidValue(String),
175
176 Transaction(TransactionError),
178
179 Storage(StorageError),
181
182 Query(QueryError),
184
185 Serialization(String),
187
188 Io(std::io::Error),
190
191 Internal(String),
193}
194
195impl Error {
196 #[must_use]
198 pub fn error_code(&self) -> ErrorCode {
199 match self {
200 Error::NodeNotFound(_) => ErrorCode::NodeNotFound,
201 Error::EdgeNotFound(_) => ErrorCode::EdgeNotFound,
202 Error::PropertyNotFound(_) => ErrorCode::PropertyNotFound,
203 Error::LabelNotFound(_) => ErrorCode::LabelNotFound,
204 Error::TypeMismatch { .. } => ErrorCode::TypeMismatch,
205 Error::InvalidValue(_) => ErrorCode::InvalidInput,
206 Error::Transaction(e) => e.error_code(),
207 Error::Storage(e) => e.error_code(),
208 Error::Query(e) => e.error_code(),
209 Error::Serialization(_) => ErrorCode::SerializationError,
210 Error::Io(_) => ErrorCode::IoError,
211 Error::Internal(_) => ErrorCode::Internal,
212 }
213 }
214}
215
216impl fmt::Display for Error {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 let code = self.error_code();
219 match self {
220 Error::NodeNotFound(id) => write!(f, "{code}: Node not found: {id}"),
221 Error::EdgeNotFound(id) => write!(f, "{code}: Edge not found: {id}"),
222 Error::PropertyNotFound(key) => write!(f, "{code}: Property not found: {key}"),
223 Error::LabelNotFound(label) => write!(f, "{code}: Label not found: {label}"),
224 Error::TypeMismatch { expected, found } => {
225 write!(
226 f,
227 "{code}: Type mismatch: expected {expected}, found {found}"
228 )
229 }
230 Error::InvalidValue(msg) => write!(f, "{code}: Invalid value: {msg}"),
231 Error::Transaction(e) => write!(f, "{code}: {e}"),
232 Error::Storage(e) => write!(f, "{code}: {e}"),
233 Error::Query(e) => write!(f, "{e}"),
234 Error::Serialization(msg) => write!(f, "{code}: Serialization error: {msg}"),
235 Error::Io(e) => write!(f, "{code}: I/O error: {e}"),
236 Error::Internal(msg) => write!(f, "{code}: Internal error: {msg}"),
237 }
238 }
239}
240
241impl std::error::Error for Error {
242 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
243 match self {
244 Error::Io(e) => Some(e),
245 Error::Transaction(e) => Some(e),
246 Error::Storage(e) => Some(e),
247 Error::Query(e) => Some(e),
248 _ => None,
249 }
250 }
251}
252
253impl From<std::io::Error> for Error {
254 fn from(e: std::io::Error) -> Self {
255 Error::Io(e)
256 }
257}
258
259#[derive(Debug, Clone)]
261pub enum TransactionError {
262 Aborted,
264
265 Conflict,
267
268 WriteConflict(String),
270
271 SerializationFailure(String),
276
277 Deadlock,
279
280 Timeout,
282
283 ReadOnly,
285
286 InvalidState(String),
288}
289
290impl TransactionError {
291 #[must_use]
293 pub const fn error_code(&self) -> ErrorCode {
294 match self {
295 Self::Aborted | Self::Conflict | Self::WriteConflict(_) => {
296 ErrorCode::TransactionConflict
297 }
298 Self::SerializationFailure(_) => ErrorCode::TransactionSerialization,
299 Self::Deadlock => ErrorCode::TransactionDeadlock,
300 Self::Timeout => ErrorCode::TransactionTimeout,
301 Self::ReadOnly => ErrorCode::TransactionReadOnly,
302 Self::InvalidState(_) => ErrorCode::TransactionInvalidState,
303 }
304 }
305}
306
307impl fmt::Display for TransactionError {
308 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309 match self {
310 TransactionError::Aborted => write!(f, "Transaction aborted"),
311 TransactionError::Conflict => write!(f, "Transaction conflict"),
312 TransactionError::WriteConflict(msg) => write!(f, "Write conflict: {msg}"),
313 TransactionError::SerializationFailure(msg) => {
314 write!(f, "Serialization failure (SSI): {msg}")
315 }
316 TransactionError::Deadlock => write!(f, "Deadlock detected"),
317 TransactionError::Timeout => write!(f, "Transaction timeout"),
318 TransactionError::ReadOnly => write!(f, "Cannot write in read-only transaction"),
319 TransactionError::InvalidState(msg) => write!(f, "Invalid transaction state: {msg}"),
320 }
321 }
322}
323
324impl std::error::Error for TransactionError {}
325
326impl From<TransactionError> for Error {
327 fn from(e: TransactionError) -> Self {
328 Error::Transaction(e)
329 }
330}
331
332#[derive(Debug, Clone)]
334pub enum StorageError {
335 Corruption(String),
337
338 Full,
340
341 InvalidWalEntry(String),
343
344 RecoveryFailed(String),
346
347 CheckpointFailed(String),
349}
350
351impl StorageError {
352 #[must_use]
354 pub const fn error_code(&self) -> ErrorCode {
355 match self {
356 Self::Corruption(_) => ErrorCode::StorageCorrupted,
357 Self::Full => ErrorCode::StorageFull,
358 Self::InvalidWalEntry(_) | Self::CheckpointFailed(_) => ErrorCode::StorageCorrupted,
359 Self::RecoveryFailed(_) => ErrorCode::StorageRecoveryFailed,
360 }
361 }
362}
363
364impl fmt::Display for StorageError {
365 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
366 match self {
367 StorageError::Corruption(msg) => write!(f, "Storage corruption: {msg}"),
368 StorageError::Full => write!(f, "Storage is full"),
369 StorageError::InvalidWalEntry(msg) => write!(f, "Invalid WAL entry: {msg}"),
370 StorageError::RecoveryFailed(msg) => write!(f, "Recovery failed: {msg}"),
371 StorageError::CheckpointFailed(msg) => write!(f, "Checkpoint failed: {msg}"),
372 }
373 }
374}
375
376impl std::error::Error for StorageError {}
377
378impl From<StorageError> for Error {
379 fn from(e: StorageError) -> Self {
380 Error::Storage(e)
381 }
382}
383
384#[derive(Debug, Clone)]
390pub struct QueryError {
391 pub kind: QueryErrorKind,
393 pub message: String,
395 pub span: Option<SourceSpan>,
397 pub source_query: Option<String>,
399 pub hint: Option<String>,
401}
402
403impl QueryError {
404 pub fn new(kind: QueryErrorKind, message: impl Into<String>) -> Self {
406 Self {
407 kind,
408 message: message.into(),
409 span: None,
410 source_query: None,
411 hint: None,
412 }
413 }
414
415 #[must_use]
417 pub fn timeout() -> Self {
418 Self::new(QueryErrorKind::Execution, "Query exceeded timeout")
419 }
420
421 #[must_use]
423 pub const fn error_code(&self) -> ErrorCode {
424 match self.kind {
425 QueryErrorKind::Lexer | QueryErrorKind::Syntax => ErrorCode::QuerySyntax,
426 QueryErrorKind::Semantic => ErrorCode::QuerySemantic,
427 QueryErrorKind::Optimization => ErrorCode::QueryOptimization,
428 QueryErrorKind::Execution => ErrorCode::QueryExecution,
429 }
430 }
431
432 #[must_use]
434 pub fn with_span(mut self, span: SourceSpan) -> Self {
435 self.span = Some(span);
436 self
437 }
438
439 #[must_use]
441 pub fn with_source(mut self, query: impl Into<String>) -> Self {
442 self.source_query = Some(query.into());
443 self
444 }
445
446 #[must_use]
448 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
449 self.hint = Some(hint.into());
450 self
451 }
452}
453
454impl fmt::Display for QueryError {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 write!(f, "{}: {}", self.kind, self.message)?;
457
458 if let (Some(span), Some(query)) = (&self.span, &self.source_query) {
459 write!(f, "\n --> query:{}:{}", span.line, span.column)?;
460
461 if let Some(line) = query.lines().nth(span.line.saturating_sub(1) as usize) {
463 write!(f, "\n |")?;
464 write!(f, "\n {} | {}", span.line, line)?;
465 write!(f, "\n | ")?;
466
467 for _ in 0..span.column.saturating_sub(1) {
469 write!(f, " ")?;
470 }
471 for _ in span.start..span.end {
472 write!(f, "^")?;
473 }
474 }
475 }
476
477 if let Some(hint) = &self.hint {
478 write!(f, "\n |\n help: {hint}")?;
479 }
480
481 Ok(())
482 }
483}
484
485impl std::error::Error for QueryError {}
486
487impl From<QueryError> for Error {
488 fn from(e: QueryError) -> Self {
489 Error::Query(e)
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum QueryErrorKind {
496 Lexer,
498 Syntax,
500 Semantic,
502 Optimization,
504 Execution,
506}
507
508impl fmt::Display for QueryErrorKind {
509 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510 match self {
511 QueryErrorKind::Lexer => write!(f, "lexer error"),
512 QueryErrorKind::Syntax => write!(f, "syntax error"),
513 QueryErrorKind::Semantic => write!(f, "semantic error"),
514 QueryErrorKind::Optimization => write!(f, "optimization error"),
515 QueryErrorKind::Execution => write!(f, "execution error"),
516 }
517 }
518}
519
520#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522pub struct SourceSpan {
523 pub start: usize,
525 pub end: usize,
527 pub line: u32,
529 pub column: u32,
531}
532
533impl SourceSpan {
534 pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
536 Self {
537 start,
538 end,
539 line,
540 column,
541 }
542 }
543}
544
545pub type Result<T> = std::result::Result<T, Error>;
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_error_display() {
554 let err = Error::NodeNotFound(crate::types::NodeId::new(42));
555 assert_eq!(err.to_string(), "GRAFEO-V002: Node not found: 42");
556
557 let err = Error::TypeMismatch {
558 expected: "INT64".to_string(),
559 found: "STRING".to_string(),
560 };
561 assert_eq!(
562 err.to_string(),
563 "GRAFEO-V006: Type mismatch: expected INT64, found STRING"
564 );
565 }
566
567 #[test]
568 fn test_error_codes() {
569 assert_eq!(
570 Error::Internal("x".into()).error_code(),
571 ErrorCode::Internal
572 );
573 assert_eq!(ErrorCode::Internal.as_str(), "GRAFEO-X001");
574 assert!(!ErrorCode::Internal.is_retryable());
575
576 assert_eq!(
577 Error::Transaction(TransactionError::Conflict).error_code(),
578 ErrorCode::TransactionConflict
579 );
580 assert!(ErrorCode::TransactionConflict.is_retryable());
581 assert!(ErrorCode::QueryTimeout.is_retryable());
582 assert!(!ErrorCode::StorageFull.is_retryable());
583 }
584
585 #[test]
586 fn test_query_timeout() {
587 let err = QueryError::timeout();
588 assert_eq!(err.kind, QueryErrorKind::Execution);
589 assert!(err.message.contains("timeout"));
590 }
591
592 #[test]
593 fn test_query_error_formatting() {
594 let query = "MATCH (n:Peron) RETURN n";
595 let err = QueryError::new(QueryErrorKind::Semantic, "Unknown label 'Peron'")
596 .with_span(SourceSpan::new(9, 14, 1, 10))
597 .with_source(query)
598 .with_hint("Did you mean 'Person'?");
599
600 let msg = err.to_string();
601 assert!(msg.contains("Unknown label 'Peron'"));
602 assert!(msg.contains("query:1:10"));
603 assert!(msg.contains("Did you mean 'Person'?"));
604 }
605
606 #[test]
607 fn test_transaction_error() {
608 let err: Error = TransactionError::Conflict.into();
609 assert!(matches!(
610 err,
611 Error::Transaction(TransactionError::Conflict)
612 ));
613 }
614
615 #[test]
616 fn test_io_error_conversion() {
617 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
618 let err: Error = io_err.into();
619 assert!(matches!(err, Error::Io(_)));
620 }
621}