use std::fmt;
use std::path::PathBuf;
use crate::graph::blueprint::expr::ExprError;
use crate::graph::languages::cypher::planner::schema_check::SchemaError;
use crate::graph::schema::ValidationError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KgErrorCode {
CypherSyntax,
CypherTimeout,
CypherExecution,
CypherTypeMismatch,
Schema,
Validation,
Expr,
NodeNotFound,
ConnectionNotFound,
PropertyNotFound,
FileNotFound,
FileFormat,
FileIo,
InvalidArgument,
MissingArgument,
Internal,
}
impl KgErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
KgErrorCode::CypherSyntax => "CypherSyntax",
KgErrorCode::CypherTimeout => "CypherTimeout",
KgErrorCode::CypherExecution => "CypherExecution",
KgErrorCode::CypherTypeMismatch => "CypherTypeMismatch",
KgErrorCode::Schema => "Schema",
KgErrorCode::Validation => "Validation",
KgErrorCode::Expr => "Expr",
KgErrorCode::NodeNotFound => "NodeNotFound",
KgErrorCode::ConnectionNotFound => "ConnectionNotFound",
KgErrorCode::PropertyNotFound => "PropertyNotFound",
KgErrorCode::FileNotFound => "FileNotFound",
KgErrorCode::FileFormat => "FileFormat",
KgErrorCode::FileIo => "FileIo",
KgErrorCode::InvalidArgument => "InvalidArgument",
KgErrorCode::MissingArgument => "MissingArgument",
KgErrorCode::Internal => "Internal",
}
}
pub fn http_status_code(&self) -> u16 {
match self {
KgErrorCode::CypherSyntax
| KgErrorCode::CypherTypeMismatch
| KgErrorCode::InvalidArgument
| KgErrorCode::MissingArgument => 400,
KgErrorCode::NodeNotFound
| KgErrorCode::ConnectionNotFound
| KgErrorCode::PropertyNotFound
| KgErrorCode::FileNotFound => 404,
KgErrorCode::CypherTimeout => 408,
KgErrorCode::Schema | KgErrorCode::Validation | KgErrorCode::Expr => 422,
KgErrorCode::CypherExecution
| KgErrorCode::FileFormat
| KgErrorCode::FileIo
| KgErrorCode::Internal => 500,
}
}
pub fn neo4j_status_code(&self) -> &'static str {
match self {
KgErrorCode::CypherSyntax => "Neo.ClientError.Statement.SyntaxError",
KgErrorCode::CypherTimeout => "Neo.ClientError.Transaction.TransactionTimedOut",
KgErrorCode::CypherTypeMismatch => "Neo.ClientError.Statement.TypeError",
KgErrorCode::CypherExecution => "Neo.DatabaseError.Statement.ExecutionFailed",
KgErrorCode::Schema => "Neo.ClientError.Schema.ConstraintValidationFailed",
KgErrorCode::Validation | KgErrorCode::Expr => {
"Neo.ClientError.Statement.ArgumentError"
}
KgErrorCode::NodeNotFound
| KgErrorCode::ConnectionNotFound
| KgErrorCode::PropertyNotFound => "Neo.ClientError.Statement.EntityNotFound",
KgErrorCode::InvalidArgument => "Neo.ClientError.Statement.ArgumentError",
KgErrorCode::MissingArgument => "Neo.ClientError.Statement.ParameterMissing",
KgErrorCode::FileNotFound
| KgErrorCode::FileFormat
| KgErrorCode::FileIo
| KgErrorCode::Internal => "Neo.DatabaseError.General.UnknownError",
}
}
}
impl fmt::Display for KgErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub enum KgError {
CypherSyntax {
message: String,
line: Option<usize>,
col: Option<usize>,
},
CypherTimeout { elapsed_ms: u64, limit_ms: u64 },
CypherExecution {
message: String,
position: Option<(usize, usize)>,
},
CypherTypeMismatch {
expected: String,
found: String,
context: String,
},
Schema {
kind: SchemaErrorKindRepr,
message: String,
},
Validation(ValidationError),
Expr(ExprError),
NodeNotFound { node_type: String, id: String },
ConnectionNotFound { connection_type: String },
PropertyNotFound { node_type: String, property: String },
FileNotFound(PathBuf),
FileFormat { path: PathBuf, message: String },
FileIo(std::io::Error),
InvalidArgument {
argument: String,
expected: String,
found: String,
},
Argument(String),
MissingArgument(String),
Internal {
message: String,
location: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaErrorKindRepr {
UnknownProperty,
}
impl From<crate::graph::languages::cypher::planner::schema_check::SchemaErrorKind>
for SchemaErrorKindRepr
{
fn from(
value: crate::graph::languages::cypher::planner::schema_check::SchemaErrorKind,
) -> Self {
use crate::graph::languages::cypher::planner::schema_check::SchemaErrorKind;
match value {
SchemaErrorKind::UnknownProperty => SchemaErrorKindRepr::UnknownProperty,
}
}
}
impl KgError {
pub fn code(&self) -> KgErrorCode {
match self {
KgError::CypherSyntax { .. } => KgErrorCode::CypherSyntax,
KgError::CypherTimeout { .. } => KgErrorCode::CypherTimeout,
KgError::CypherExecution { .. } => KgErrorCode::CypherExecution,
KgError::CypherTypeMismatch { .. } => KgErrorCode::CypherTypeMismatch,
KgError::Schema { .. } => KgErrorCode::Schema,
KgError::Validation(_) => KgErrorCode::Validation,
KgError::Expr(_) => KgErrorCode::Expr,
KgError::NodeNotFound { .. } => KgErrorCode::NodeNotFound,
KgError::ConnectionNotFound { .. } => KgErrorCode::ConnectionNotFound,
KgError::PropertyNotFound { .. } => KgErrorCode::PropertyNotFound,
KgError::FileNotFound(_) => KgErrorCode::FileNotFound,
KgError::FileFormat { .. } => KgErrorCode::FileFormat,
KgError::FileIo(_) => KgErrorCode::FileIo,
KgError::InvalidArgument { .. } | KgError::Argument(_) => KgErrorCode::InvalidArgument,
KgError::MissingArgument(_) => KgErrorCode::MissingArgument,
KgError::Internal { .. } => KgErrorCode::Internal,
}
}
pub fn position(&self) -> Option<(usize, usize)> {
match self {
KgError::CypherSyntax {
line: Some(l),
col: Some(c),
..
} => Some((*l, *c)),
KgError::CypherExecution {
position: Some(p), ..
} => Some(*p),
_ => None,
}
}
}
impl fmt::Display for KgError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KgError::CypherSyntax { message, line, col } => match (line, col) {
(Some(l), Some(c)) => write!(
f,
"Cypher syntax error at line {}, col {}: {}",
l, c, message
),
_ => write!(f, "Cypher syntax error: {}", message),
},
KgError::CypherTimeout {
elapsed_ms,
limit_ms,
} => write!(
f,
"Cypher query exceeded timeout: elapsed {}ms, limit {}ms",
elapsed_ms, limit_ms
),
KgError::CypherExecution { message, position } => match position {
Some((l, c)) => write!(
f,
"Cypher execution error at line {}, col {}: {}",
l, c, message
),
None => write!(f, "Cypher execution error: {}", message),
},
KgError::CypherTypeMismatch {
expected,
found,
context,
} => write!(
f,
"Cypher type mismatch in {}: expected {}, found {}",
context, expected, found
),
KgError::Schema { message, .. } => write!(f, "Schema error: {}", message),
KgError::Validation(v) => write!(f, "Validation error: {}", v),
KgError::Expr(e) => write!(f, "Expression error: {}", e),
KgError::NodeNotFound { node_type, id } => {
write!(f, "Node not found: {} with id {:?}", node_type, id)
}
KgError::ConnectionNotFound { connection_type } => {
write!(f, "Connection type not found: {}", connection_type)
}
KgError::PropertyNotFound {
node_type,
property,
} => write!(f, "Property '{}' not found on {}", property, node_type),
KgError::FileNotFound(path) => write!(f, "File not found: {}", path.display()),
KgError::FileFormat { path, message } => {
write!(f, "File format error ({}): {}", path.display(), message)
}
KgError::FileIo(e) => write!(f, "File I/O error: {}", e),
KgError::InvalidArgument {
argument,
expected,
found,
} => write!(
f,
"Invalid argument '{}': expected {}, found {}",
argument, expected, found
),
KgError::Argument(message) => write!(f, "Invalid argument: {}", message),
KgError::MissingArgument(name) => write!(f, "Missing required argument: {}", name),
KgError::Internal { message, location } => {
write!(f, "Internal error at {}: {}", location, message)
}
}
}
}
impl std::error::Error for KgError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
KgError::FileIo(e) => Some(e),
_ => None,
}
}
}
impl From<SchemaError> for KgError {
fn from(e: SchemaError) -> Self {
KgError::Schema {
kind: e.kind.into(),
message: e.message,
}
}
}
impl From<ValidationError> for KgError {
fn from(e: ValidationError) -> Self {
KgError::Validation(e)
}
}
impl From<ExprError> for KgError {
fn from(e: ExprError) -> Self {
KgError::Expr(e)
}
}
impl From<std::io::Error> for KgError {
fn from(e: std::io::Error) -> Self {
match e.kind() {
std::io::ErrorKind::NotFound => {
KgError::FileIo(e)
}
_ => KgError::FileIo(e),
}
}
}
#[allow(dead_code)]
pub type KgResult<T> = std::result::Result<T, KgError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn code_round_trip() {
let e = KgError::CypherSyntax {
message: "expected RETURN".to_string(),
line: Some(3),
col: Some(12),
};
assert_eq!(e.code(), KgErrorCode::CypherSyntax);
assert_eq!(e.position(), Some((3, 12)));
}
#[test]
fn display_includes_position() {
let e = KgError::CypherSyntax {
message: "expected RETURN".to_string(),
line: Some(3),
col: Some(12),
};
let s = format!("{}", e);
assert!(s.contains("line 3"));
assert!(s.contains("col 12"));
assert!(s.contains("expected RETURN"));
}
#[test]
fn display_without_position() {
let e = KgError::CypherExecution {
message: "div by zero".to_string(),
position: None,
};
let s = format!("{}", e);
assert!(s.contains("div by zero"));
assert!(!s.contains("line"));
}
#[test]
fn kgerror_code_as_str_stable() {
assert_eq!(KgErrorCode::CypherSyntax.as_str(), "CypherSyntax");
assert_eq!(KgErrorCode::NodeNotFound.as_str(), "NodeNotFound");
assert_eq!(KgErrorCode::FileFormat.as_str(), "FileFormat");
}
#[test]
fn from_schema_error_preserves_kind_and_message() {
use crate::graph::languages::cypher::planner::schema_check::SchemaErrorKind;
let se = SchemaError {
kind: SchemaErrorKind::UnknownProperty,
message: "no such property 'foo'".to_string(),
};
let kg: KgError = se.into();
assert_eq!(kg.code(), KgErrorCode::Schema);
match kg {
KgError::Schema { kind, message } => {
assert_eq!(kind, SchemaErrorKindRepr::UnknownProperty);
assert_eq!(message, "no such property 'foo'");
}
_ => panic!("expected Schema variant"),
}
}
#[test]
fn from_io_error_classifies_as_file_io() {
let io = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
let kg: KgError = io.into();
assert_eq!(kg.code(), KgErrorCode::FileIo);
}
#[test]
fn http_status_code_categorises_correctly() {
assert_eq!(KgErrorCode::CypherSyntax.http_status_code(), 400);
assert_eq!(KgErrorCode::CypherTypeMismatch.http_status_code(), 400);
assert_eq!(KgErrorCode::InvalidArgument.http_status_code(), 400);
assert_eq!(KgErrorCode::MissingArgument.http_status_code(), 400);
assert_eq!(KgErrorCode::NodeNotFound.http_status_code(), 404);
assert_eq!(KgErrorCode::ConnectionNotFound.http_status_code(), 404);
assert_eq!(KgErrorCode::PropertyNotFound.http_status_code(), 404);
assert_eq!(KgErrorCode::FileNotFound.http_status_code(), 404);
assert_eq!(KgErrorCode::CypherTimeout.http_status_code(), 408);
assert_eq!(KgErrorCode::Schema.http_status_code(), 422);
assert_eq!(KgErrorCode::Validation.http_status_code(), 422);
assert_eq!(KgErrorCode::Expr.http_status_code(), 422);
assert_eq!(KgErrorCode::CypherExecution.http_status_code(), 500);
assert_eq!(KgErrorCode::FileFormat.http_status_code(), 500);
assert_eq!(KgErrorCode::FileIo.http_status_code(), 500);
assert_eq!(KgErrorCode::Internal.http_status_code(), 500);
}
#[test]
fn every_error_code_has_an_http_status() {
for code in [
KgErrorCode::CypherSyntax,
KgErrorCode::CypherTimeout,
KgErrorCode::CypherExecution,
KgErrorCode::CypherTypeMismatch,
KgErrorCode::Schema,
KgErrorCode::Validation,
KgErrorCode::Expr,
KgErrorCode::NodeNotFound,
KgErrorCode::ConnectionNotFound,
KgErrorCode::PropertyNotFound,
KgErrorCode::FileNotFound,
KgErrorCode::FileFormat,
KgErrorCode::FileIo,
KgErrorCode::InvalidArgument,
KgErrorCode::MissingArgument,
KgErrorCode::Internal,
] {
let code_val = code.http_status_code();
assert!(
(400..=599).contains(&code_val),
"code {code:?} mapped to non-4xx-5xx http status: {code_val}"
);
}
}
}