use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorCode {
QuerySyntax,
QuerySemantic,
QueryTimeout,
QueryUnsupported,
QueryOptimization,
QueryExecution,
TransactionConflict,
TransactionTimeout,
TransactionReadOnly,
TransactionInvalidState,
TransactionSerialization,
TransactionDeadlock,
StorageFull,
StorageCorrupted,
StorageRecoveryFailed,
InvalidInput,
NodeNotFound,
EdgeNotFound,
PropertyNotFound,
LabelNotFound,
TypeMismatch,
Internal,
SerializationError,
IoError,
}
impl ErrorCode {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::QuerySyntax => "GRAFEO-Q001",
Self::QuerySemantic => "GRAFEO-Q002",
Self::QueryTimeout => "GRAFEO-Q003",
Self::QueryUnsupported => "GRAFEO-Q004",
Self::QueryOptimization => "GRAFEO-Q005",
Self::QueryExecution => "GRAFEO-Q006",
Self::TransactionConflict => "GRAFEO-T001",
Self::TransactionTimeout => "GRAFEO-T002",
Self::TransactionReadOnly => "GRAFEO-T003",
Self::TransactionInvalidState => "GRAFEO-T004",
Self::TransactionSerialization => "GRAFEO-T005",
Self::TransactionDeadlock => "GRAFEO-T006",
Self::StorageFull => "GRAFEO-S001",
Self::StorageCorrupted => "GRAFEO-S002",
Self::StorageRecoveryFailed => "GRAFEO-S003",
Self::InvalidInput => "GRAFEO-V001",
Self::NodeNotFound => "GRAFEO-V002",
Self::EdgeNotFound => "GRAFEO-V003",
Self::PropertyNotFound => "GRAFEO-V004",
Self::LabelNotFound => "GRAFEO-V005",
Self::TypeMismatch => "GRAFEO-V006",
Self::Internal => "GRAFEO-X001",
Self::SerializationError => "GRAFEO-X002",
Self::IoError => "GRAFEO-X003",
}
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(
self,
Self::TransactionConflict
| Self::TransactionTimeout
| Self::TransactionDeadlock
| Self::QueryTimeout
)
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
NodeNotFound(crate::types::NodeId),
EdgeNotFound(crate::types::EdgeId),
PropertyNotFound(String),
LabelNotFound(String),
TypeMismatch {
expected: String,
found: String,
},
InvalidValue(String),
Transaction(TransactionError),
Storage(StorageError),
Query(QueryError),
Serialization(String),
Io(std::io::Error),
Internal(String),
}
impl Error {
#[must_use]
pub fn error_code(&self) -> ErrorCode {
match self {
Error::NodeNotFound(_) => ErrorCode::NodeNotFound,
Error::EdgeNotFound(_) => ErrorCode::EdgeNotFound,
Error::PropertyNotFound(_) => ErrorCode::PropertyNotFound,
Error::LabelNotFound(_) => ErrorCode::LabelNotFound,
Error::TypeMismatch { .. } => ErrorCode::TypeMismatch,
Error::InvalidValue(_) => ErrorCode::InvalidInput,
Error::Transaction(e) => e.error_code(),
Error::Storage(e) => e.error_code(),
Error::Query(e) => e.error_code(),
Error::Serialization(_) => ErrorCode::SerializationError,
Error::Io(_) => ErrorCode::IoError,
Error::Internal(_) => ErrorCode::Internal,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = self.error_code();
match self {
Error::NodeNotFound(id) => write!(f, "{code}: Node not found: {id}"),
Error::EdgeNotFound(id) => write!(f, "{code}: Edge not found: {id}"),
Error::PropertyNotFound(key) => write!(f, "{code}: Property not found: {key}"),
Error::LabelNotFound(label) => write!(f, "{code}: Label not found: {label}"),
Error::TypeMismatch { expected, found } => {
write!(
f,
"{code}: Type mismatch: expected {expected}, found {found}"
)
}
Error::InvalidValue(msg) => write!(f, "{code}: Invalid value: {msg}"),
Error::Transaction(e) => write!(f, "{code}: {e}"),
Error::Storage(e) => write!(f, "{code}: {e}"),
Error::Query(e) => write!(f, "{e}"),
Error::Serialization(msg) => write!(f, "{code}: Serialization error: {msg}"),
Error::Io(e) => write!(f, "{code}: I/O error: {e}"),
Error::Internal(msg) => write!(f, "{code}: Internal error: {msg}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(e) => Some(e),
Error::Transaction(e) => Some(e),
Error::Storage(e) => Some(e),
Error::Query(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum TransactionError {
Aborted,
Conflict,
WriteConflict(String),
SerializationFailure(String),
Deadlock,
Timeout,
ReadOnly,
InvalidState(String),
}
impl TransactionError {
#[must_use]
pub const fn error_code(&self) -> ErrorCode {
match self {
Self::Aborted | Self::Conflict | Self::WriteConflict(_) => {
ErrorCode::TransactionConflict
}
Self::SerializationFailure(_) => ErrorCode::TransactionSerialization,
Self::Deadlock => ErrorCode::TransactionDeadlock,
Self::Timeout => ErrorCode::TransactionTimeout,
Self::ReadOnly => ErrorCode::TransactionReadOnly,
Self::InvalidState(_) => ErrorCode::TransactionInvalidState,
}
}
}
impl fmt::Display for TransactionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransactionError::Aborted => write!(f, "Transaction aborted"),
TransactionError::Conflict => write!(f, "Transaction conflict"),
TransactionError::WriteConflict(msg) => write!(f, "Write conflict: {msg}"),
TransactionError::SerializationFailure(msg) => {
write!(f, "Serialization failure (SSI): {msg}")
}
TransactionError::Deadlock => write!(f, "Deadlock detected"),
TransactionError::Timeout => write!(f, "Transaction timeout"),
TransactionError::ReadOnly => write!(f, "Cannot write in read-only transaction"),
TransactionError::InvalidState(msg) => write!(f, "Invalid transaction state: {msg}"),
}
}
}
impl std::error::Error for TransactionError {}
impl From<TransactionError> for Error {
fn from(e: TransactionError) -> Self {
Error::Transaction(e)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum StorageError {
Corruption(String),
Full,
InvalidWalEntry(String),
RecoveryFailed(String),
CheckpointFailed(String),
}
impl StorageError {
#[must_use]
pub const fn error_code(&self) -> ErrorCode {
match self {
Self::Corruption(_) => ErrorCode::StorageCorrupted,
Self::Full => ErrorCode::StorageFull,
Self::InvalidWalEntry(_) | Self::CheckpointFailed(_) => ErrorCode::StorageCorrupted,
Self::RecoveryFailed(_) => ErrorCode::StorageRecoveryFailed,
}
}
}
impl fmt::Display for StorageError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StorageError::Corruption(msg) => write!(f, "Storage corruption: {msg}"),
StorageError::Full => write!(f, "Storage is full"),
StorageError::InvalidWalEntry(msg) => write!(f, "Invalid WAL entry: {msg}"),
StorageError::RecoveryFailed(msg) => write!(f, "Recovery failed: {msg}"),
StorageError::CheckpointFailed(msg) => write!(f, "Checkpoint failed: {msg}"),
}
}
}
impl std::error::Error for StorageError {}
impl From<StorageError> for Error {
fn from(e: StorageError) -> Self {
Error::Storage(e)
}
}
#[derive(Debug, Clone)]
pub struct QueryError {
pub kind: QueryErrorKind,
pub message: String,
pub span: Option<SourceSpan>,
pub source_query: Option<String>,
pub hint: Option<String>,
}
impl QueryError {
pub fn new(kind: QueryErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
span: None,
source_query: None,
hint: None,
}
}
#[must_use]
pub fn timeout() -> Self {
Self::new(QueryErrorKind::Timeout, "Query exceeded timeout")
}
#[must_use]
pub fn timeout_with_limit(limit: std::time::Duration) -> Self {
let millis = limit.as_millis();
let timeout_display = if millis >= 1000 && millis.is_multiple_of(1000) {
format!("{}s", millis / 1000)
} else {
format!("{millis}ms")
};
Self::new(
QueryErrorKind::Timeout,
format!("Query exceeded the {timeout_display} timeout"),
)
.with_hint(
"Increase with Config::with_query_timeout() or disable with Config::without_query_timeout()"
.to_string(),
)
}
#[must_use]
pub const fn error_code(&self) -> ErrorCode {
match self.kind {
QueryErrorKind::Lexer | QueryErrorKind::Syntax => ErrorCode::QuerySyntax,
QueryErrorKind::Semantic => ErrorCode::QuerySemantic,
QueryErrorKind::Optimization => ErrorCode::QueryOptimization,
QueryErrorKind::Execution => ErrorCode::QueryExecution,
QueryErrorKind::Timeout => ErrorCode::QueryTimeout,
}
}
#[must_use]
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = Some(span);
self
}
#[must_use]
pub fn with_source(mut self, query: impl Into<String>) -> Self {
self.source_query = Some(query.into());
self
}
#[must_use]
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hint = Some(hint.into());
self
}
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.kind, self.message)?;
if let (Some(span), Some(query)) = (&self.span, &self.source_query) {
write!(f, "\n --> query:{}:{}", span.line, span.column)?;
if let Some(line) = query.lines().nth(span.line.saturating_sub(1) as usize) {
write!(f, "\n |")?;
write!(f, "\n {} | {}", span.line, line)?;
write!(f, "\n | ")?;
for _ in 0..span.column.saturating_sub(1) {
write!(f, " ")?;
}
for _ in span.start..span.end {
write!(f, "^")?;
}
}
}
if let Some(hint) = &self.hint {
write!(f, "\n |\n help: {hint}")?;
}
Ok(())
}
}
impl std::error::Error for QueryError {}
impl From<QueryError> for Error {
fn from(e: QueryError) -> Self {
Error::Query(e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum QueryErrorKind {
Lexer,
Syntax,
Semantic,
Optimization,
Execution,
Timeout,
}
impl fmt::Display for QueryErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QueryErrorKind::Lexer => write!(f, "lexer error"),
QueryErrorKind::Syntax => write!(f, "syntax error"),
QueryErrorKind::Semantic => write!(f, "semantic error"),
QueryErrorKind::Optimization => write!(f, "optimization error"),
QueryErrorKind::Execution => write!(f, "execution error"),
QueryErrorKind::Timeout => write!(f, "timeout error"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceSpan {
pub start: usize,
pub end: usize,
pub line: u32,
pub column: u32,
}
impl SourceSpan {
pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
Self {
start,
end,
line,
column,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::NodeNotFound(crate::types::NodeId::new(42));
assert_eq!(err.to_string(), "GRAFEO-V002: Node not found: 42");
let err = Error::TypeMismatch {
expected: "INT64".to_string(),
found: "STRING".to_string(),
};
assert_eq!(
err.to_string(),
"GRAFEO-V006: Type mismatch: expected INT64, found STRING"
);
}
#[test]
fn test_error_codes() {
assert_eq!(
Error::Internal("x".into()).error_code(),
ErrorCode::Internal
);
assert_eq!(ErrorCode::Internal.as_str(), "GRAFEO-X001");
assert!(!ErrorCode::Internal.is_retryable());
assert_eq!(
Error::Transaction(TransactionError::Conflict).error_code(),
ErrorCode::TransactionConflict
);
assert!(ErrorCode::TransactionConflict.is_retryable());
assert!(ErrorCode::QueryTimeout.is_retryable());
assert!(!ErrorCode::StorageFull.is_retryable());
}
#[test]
fn test_query_timeout() {
let err = QueryError::timeout();
assert_eq!(err.kind, QueryErrorKind::Timeout);
assert_eq!(err.error_code(), ErrorCode::QueryTimeout);
assert!(err.error_code().is_retryable());
assert!(err.message.contains("timeout"));
}
#[test]
fn test_query_error_formatting() {
let query = "MATCH (n:Peron) RETURN n";
let err = QueryError::new(QueryErrorKind::Semantic, "Unknown label 'Peron'")
.with_span(SourceSpan::new(9, 14, 1, 10))
.with_source(query)
.with_hint("Did you mean 'Person'?");
let msg = err.to_string();
assert!(msg.contains("Unknown label 'Peron'"));
assert!(msg.contains("query:1:10"));
assert!(msg.contains("Did you mean 'Person'?"));
}
#[test]
fn test_transaction_error() {
let err: Error = TransactionError::Conflict.into();
assert!(matches!(
err,
Error::Transaction(TransactionError::Conflict)
));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err, Error::Io(_)));
}
}