use thiserror::Error;
#[derive(Debug, Error)]
pub enum GqlError {
#[error("Parse error: {0}")]
Parse(#[from] ParseError),
#[error("Compile error: {0}")]
Compile(#[from] CompileError),
#[error("Mutation error: {0}")]
Mutation(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn at(position: usize) -> Self {
Self {
start: position,
end: position,
}
}
}
impl std::fmt::Display for Span {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.start == self.end {
write!(f, "position {}", self.start)
} else {
write!(f, "positions {}..{}", self.start, self.end)
}
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("Syntax error at {span}: {message}")]
SyntaxAt { span: Span, message: String },
#[error("Syntax error: {0}")]
Syntax(String),
#[error("Empty input: query string is empty or contains only whitespace")]
Empty,
#[error("Missing {clause} clause at {span}")]
MissingClause { clause: &'static str, span: Span },
#[error("Missing {0} clause")]
MissingClauseLegacy(&'static str),
#[error("Invalid literal '{value}' at {span}: {reason}")]
InvalidLiteral {
value: String,
span: Span,
reason: &'static str,
},
#[error("Invalid literal: {0}")]
InvalidLiteralLegacy(String),
#[error("Unexpected token '{found}' at {span}, expected {expected}")]
UnexpectedToken {
span: Span,
found: String,
expected: String,
},
#[error("Unexpected end of input at {span}, expected {expected}")]
UnexpectedEof { span: Span, expected: String },
#[error("Invalid range '{range}' at {span}: {reason}")]
InvalidRange {
range: String,
span: Span,
reason: &'static str,
},
}
impl ParseError {
pub fn syntax_at(span: Span, message: impl Into<String>) -> Self {
ParseError::SyntaxAt {
span,
message: message.into(),
}
}
pub fn missing_clause(clause: &'static str, span: Span) -> Self {
ParseError::MissingClause { clause, span }
}
pub fn invalid_literal(value: impl Into<String>, span: Span, reason: &'static str) -> Self {
ParseError::InvalidLiteral {
value: value.into(),
span,
reason,
}
}
pub fn unexpected_token(
span: Span,
found: impl Into<String>,
expected: impl Into<String>,
) -> Self {
ParseError::UnexpectedToken {
span,
found: found.into(),
expected: expected.into(),
}
}
pub fn invalid_range(range: impl Into<String>, span: Span, reason: &'static str) -> Self {
ParseError::InvalidRange {
range: range.into(),
span,
reason,
}
}
pub fn span(&self) -> Option<Span> {
match self {
ParseError::SyntaxAt { span, .. } => Some(*span),
ParseError::MissingClause { span, .. } => Some(*span),
ParseError::InvalidLiteral { span, .. } => Some(*span),
ParseError::UnexpectedToken { span, .. } => Some(*span),
ParseError::UnexpectedEof { span, .. } => Some(*span),
ParseError::InvalidRange { span, .. } => Some(*span),
ParseError::Syntax(_)
| ParseError::Empty
| ParseError::MissingClauseLegacy(_)
| ParseError::InvalidLiteralLegacy(_) => None,
}
}
}
#[derive(Debug, Error)]
pub enum CompileError {
#[error("Undefined variable '{name}'. Did you forget to bind it in MATCH?")]
UndefinedVariable { name: String },
#[error("Variable '{name}' is already defined. Use a different name or reference the existing binding.")]
DuplicateVariable { name: String },
#[error("Empty pattern: MATCH clause requires at least one node pattern like (n)")]
EmptyPattern,
#[error("Pattern must start with a node: found edge pattern without preceding node. Start with (n) before -[e]->")]
PatternMustStartWithNode,
#[error("Unsupported expression '{expr}' in this context")]
UnsupportedExpression { expr: String },
#[error("Unsupported expression in context")]
UnsupportedExpressionLegacy,
#[error("Aggregate function {func}() cannot be used in WHERE clause. Use HAVING or compute in RETURN instead.")]
AggregateInWhere { func: String },
#[error("Aggregates not allowed in WHERE clause")]
AggregateInWhereLegacy,
#[error("Invalid property access on '{variable}': variable is not bound to a node or edge")]
InvalidPropertyAccess { variable: String },
#[error(
"Unsupported aggregation function '{func}'. Supported: COUNT, SUM, AVG, MIN, MAX, COLLECT"
)]
UnsupportedAggregation { func: String },
#[error("Type mismatch: {message}")]
TypeMismatch { message: String },
#[error(
"Expression '{expr}' must appear in GROUP BY clause or be used in an aggregate function"
)]
ExpressionNotInGroupBy { expr: String },
#[error("Unsupported: {0}")]
UnsupportedFeature(String),
#[error("Unbound parameter: ${0}")]
UnboundParameter(String),
#[error(
"FOREACH expression for variable '{variable}' must evaluate to a list, got {actual_type}"
)]
ForeachNotList {
variable: String,
actual_type: String,
},
#[error("Query complexity limit exceeded: {message}")]
ComplexityLimitExceeded { message: String },
#[error("Unknown procedure '{name}'. Available: interstellar.shortestPath, interstellar.dijkstra, interstellar.kShortestPaths, interstellar.bfs, interstellar.dfs, interstellar.astar, interstellar.bidirectionalBfs, interstellar.iddfs, interstellar.searchTextV, interstellar.searchTextAllV, interstellar.searchTextPhraseV, interstellar.searchTextPrefixV, interstellar.searchTextE, interstellar.searchTextAllE, interstellar.searchTextPhraseE, interstellar.searchTextPrefixE")]
UnknownProcedure { name: String },
#[error("Procedure '{procedure}': {message}")]
ProcedureArgumentError { procedure: String, message: String },
}
impl CompileError {
pub fn undefined_variable(name: impl Into<String>) -> Self {
CompileError::UndefinedVariable { name: name.into() }
}
pub fn duplicate_variable(name: impl Into<String>) -> Self {
CompileError::DuplicateVariable { name: name.into() }
}
pub fn unsupported_expression(expr: impl Into<String>) -> Self {
CompileError::UnsupportedExpression { expr: expr.into() }
}
pub fn aggregate_in_where(func: impl Into<String>) -> Self {
CompileError::AggregateInWhere { func: func.into() }
}
pub fn invalid_property_access(variable: impl Into<String>) -> Self {
CompileError::InvalidPropertyAccess {
variable: variable.into(),
}
}
pub fn unsupported_aggregation(func: impl Into<String>) -> Self {
CompileError::UnsupportedAggregation { func: func.into() }
}
pub fn type_mismatch(message: impl Into<String>) -> Self {
CompileError::TypeMismatch {
message: message.into(),
}
}
pub fn expression_not_in_group_by(expr: impl Into<String>) -> Self {
CompileError::ExpressionNotInGroupBy { expr: expr.into() }
}
pub fn unbound_parameter(name: impl Into<String>) -> Self {
CompileError::UnboundParameter(name.into())
}
pub fn foreach_not_list(variable: impl Into<String>, actual_type: impl Into<String>) -> Self {
CompileError::ForeachNotList {
variable: variable.into(),
actual_type: actual_type.into(),
}
}
pub fn complexity_limit_exceeded(message: impl Into<String>) -> Self {
CompileError::ComplexityLimitExceeded {
message: message.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_display() {
let span = Span::new(10, 20);
assert_eq!(format!("{}", span), "positions 10..20");
let span = Span::at(5);
assert_eq!(format!("{}", span), "position 5");
}
#[test]
fn test_parse_error_messages() {
let err = ParseError::syntax_at(Span::new(0, 5), "unexpected keyword");
assert!(format!("{}", err).contains("position"));
assert!(format!("{}", err).contains("unexpected keyword"));
let err = ParseError::missing_clause("RETURN", Span::at(10));
assert!(format!("{}", err).contains("RETURN"));
let err = ParseError::invalid_literal("abc", Span::new(5, 8), "expected integer");
assert!(format!("{}", err).contains("abc"));
assert!(format!("{}", err).contains("expected integer"));
let err = ParseError::unexpected_token(Span::at(3), "}", "identifier");
assert!(format!("{}", err).contains("}"));
assert!(format!("{}", err).contains("identifier"));
}
#[test]
fn test_compile_error_messages() {
let err = CompileError::undefined_variable("x");
let msg = format!("{}", err);
assert!(msg.contains("x"));
assert!(msg.contains("Did you forget"));
let err = CompileError::duplicate_variable("n");
let msg = format!("{}", err);
assert!(msg.contains("n"));
assert!(msg.contains("already defined"));
let err = CompileError::aggregate_in_where("COUNT");
let msg = format!("{}", err);
assert!(msg.contains("COUNT"));
assert!(msg.contains("WHERE"));
}
#[test]
fn test_parse_error_span_extraction() {
let err = ParseError::syntax_at(Span::new(5, 10), "test");
assert_eq!(err.span(), Some(Span::new(5, 10)));
let err = ParseError::Syntax("test".to_string());
assert_eq!(err.span(), None);
let err = ParseError::Empty;
assert_eq!(err.span(), None);
}
}