use crate::types::Dialect;
use regex::Regex;
use std::fmt;
use std::sync::OnceLock;
#[cfg(feature = "tracing")]
use tracing::trace;
#[derive(Debug, Clone)]
pub struct ParseError {
pub message: String,
pub position: Option<Position>,
pub dialect: Option<Dialect>,
pub kind: ParseErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParseErrorKind {
#[default]
SyntaxError,
MissingClause,
UnexpectedEof,
UnsupportedFeature,
LexerError,
}
impl ParseError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
position: None,
dialect: None,
kind: ParseErrorKind::SyntaxError,
}
}
pub fn with_position(message: impl Into<String>, line: usize, column: usize) -> Self {
Self {
message: message.into(),
position: Some(Position { line, column }),
dialect: None,
kind: ParseErrorKind::SyntaxError,
}
}
pub fn with_dialect(mut self, dialect: Dialect) -> Self {
self.dialect = Some(dialect);
self
}
pub fn with_kind(mut self, kind: ParseErrorKind) -> Self {
self.kind = kind;
self
}
fn parse_position_from_message(message: &str) -> Option<Position> {
static POSITION_REGEX: OnceLock<Regex> = OnceLock::new();
let re = POSITION_REGEX.get_or_init(|| {
Regex::new(r"Line:\s*(\d+)\s*,\s*Column:\s*(\d+)").expect("Invalid regex pattern")
});
let result = re.captures(message).and_then(|caps| {
let line: usize = caps.get(1)?.as_str().parse().ok()?;
let column: usize = caps.get(2)?.as_str().parse().ok()?;
Some(Position { line, column })
});
#[cfg(feature = "tracing")]
if result.is_none() && (message.contains("Line") || message.contains("Column")) {
trace!(
"Failed to parse position from error message that appears to contain position info: {}",
message
);
}
result
}
fn infer_kind_from_message(message: &str) -> ParseErrorKind {
let lower = message.to_lowercase();
if lower.contains("unexpected end") || lower.contains("eof") {
ParseErrorKind::UnexpectedEof
} else if lower.contains("expected") {
ParseErrorKind::MissingClause
} else if lower.contains("not supported") || lower.contains("unsupported") {
ParseErrorKind::UnsupportedFeature
} else if lower.contains("lexer") || lower.contains("token") {
ParseErrorKind::LexerError
} else {
ParseErrorKind::SyntaxError
}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Parse error")?;
if let Some(dialect) = self.dialect {
write!(f, " ({dialect:?})")?;
}
if let Some(pos) = self.position {
write!(f, " at line {}, column {}", pos.line, pos.column)?;
}
write!(f, ": {}", self.message)
}
}
impl std::error::Error for ParseError {}
impl From<sqlparser::parser::ParserError> for ParseError {
fn from(err: sqlparser::parser::ParserError) -> Self {
let message = err.to_string();
let position = Self::parse_position_from_message(&message);
let kind = Self::infer_kind_from_message(&message);
Self {
message,
position,
dialect: None,
kind,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_position_from_message() {
let msg = "Expected SELECT, found 'INSERT' at Line: 1, Column: 5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, Some(Position { line: 1, column: 5 }));
}
#[test]
fn test_parse_position_no_position() {
let msg = "Unexpected token";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_no_whitespace() {
let msg = "Error at Line:1,Column:5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, Some(Position { line: 1, column: 5 }));
}
#[test]
fn test_parse_position_extra_whitespace() {
let msg = "Error at Line: 42 , Column: 99";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(
pos,
Some(Position {
line: 42,
column: 99
})
);
}
#[test]
fn test_parse_position_large_numbers() {
let msg = "Error at Line: 99999, Column: 88888";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(
pos,
Some(Position {
line: 99999,
column: 88888
})
);
}
#[test]
fn test_parse_position_malformed_non_numeric_line() {
let msg = "Error at Line: abc, Column: 5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_malformed_non_numeric_column() {
let msg = "Error at Line: 1, Column: xyz";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_malformed_empty_values() {
let msg = "Error at Line: , Column: ";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_partial_line_only() {
let msg = "Error at Line: 5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_partial_column_only() {
let msg = "Error at Column: 5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_reversed_order() {
let msg = "Error at Column: 5, Line: 1";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_parse_position_negative_numbers() {
let msg = "Error at Line: -1, Column: -5";
let pos = ParseError::parse_position_from_message(msg);
assert_eq!(pos, None);
}
#[test]
fn test_infer_kind_eof() {
let kind = ParseError::infer_kind_from_message("Unexpected end of input");
assert_eq!(kind, ParseErrorKind::UnexpectedEof);
}
#[test]
fn test_infer_kind_expected() {
let kind = ParseError::infer_kind_from_message("Expected SELECT keyword");
assert_eq!(kind, ParseErrorKind::MissingClause);
}
#[test]
fn test_infer_kind_unsupported() {
let kind = ParseError::infer_kind_from_message("Feature not supported");
assert_eq!(kind, ParseErrorKind::UnsupportedFeature);
let kind = ParseError::infer_kind_from_message("This is unsupported");
assert_eq!(kind, ParseErrorKind::UnsupportedFeature);
}
#[test]
fn test_infer_kind_lexer() {
let kind = ParseError::infer_kind_from_message("Lexer error: invalid character");
assert_eq!(kind, ParseErrorKind::LexerError);
let kind = ParseError::infer_kind_from_message("Invalid token at position 5");
assert_eq!(kind, ParseErrorKind::LexerError);
}
#[test]
fn test_infer_kind_default() {
let kind = ParseError::infer_kind_from_message("Something went wrong");
assert_eq!(kind, ParseErrorKind::SyntaxError);
}
#[test]
fn test_display_with_position() {
let err = ParseError::with_position("Unexpected token", 10, 5);
assert_eq!(
err.to_string(),
"Parse error at line 10, column 5: Unexpected token"
);
}
#[test]
fn test_display_with_dialect() {
let err = ParseError::new("Bad syntax").with_dialect(Dialect::Postgres);
assert_eq!(err.to_string(), "Parse error (Postgres): Bad syntax");
}
#[test]
fn test_display_with_dialect_and_position() {
let err = ParseError::with_position("Bad syntax", 1, 5).with_dialect(Dialect::Snowflake);
assert_eq!(
err.to_string(),
"Parse error (Snowflake) at line 1, column 5: Bad syntax"
);
}
#[test]
fn test_from_parser_error() {
let message = "Expected expression, found EOF at Line: 3, Column: 12";
let pos = ParseError::parse_position_from_message(message);
assert_eq!(
pos,
Some(Position {
line: 3,
column: 12
})
);
}
#[test]
fn test_with_kind_builder() {
let err = ParseError::new("Error")
.with_kind(ParseErrorKind::UnexpectedEof)
.with_dialect(Dialect::Postgres);
assert_eq!(err.kind, ParseErrorKind::UnexpectedEof);
assert_eq!(err.dialect, Some(Dialect::Postgres));
}
#[test]
fn test_error_trait() {
let err = ParseError::new("Test error");
let _: &dyn std::error::Error = &err;
}
}