Skip to main content

gaman_core/parsers/
error.rs

1use crate::dialects::Dialect;
2use crate::parsers::segments::SqlSegment;
3use crate::states::SchemaValidationError;
4use sqlparser::parser::ParserError;
5
6#[derive(Debug, thiserror::Error)]
7pub enum ParseError {
8    /// The lowered schema failed dialect preparation.
9    #[error("{0}")]
10    SchemaValidation(#[from] SchemaValidationError),
11    #[error("{dialect} SQL parse error{location}: {message}")]
12    Parse {
13        dialect: &'static str,
14        message: String,
15        line: Option<usize>,
16        column: Option<usize>,
17        segment_ordinal: Option<usize>,
18        segment_start_line: Option<usize>,
19        segment_start_column: Option<usize>,
20        location: String,
21    },
22    #[error("unsupported {dialect} SQL statement '{statement}': {reason}")]
23    UnsupportedStatement {
24        dialect: &'static str,
25        statement: String,
26        reason: String,
27    },
28    #[error("{dialect} SQL segmentation error at line {line}, column {column}: {reason}")]
29    Segment {
30        dialect: &'static str,
31        line: usize,
32        column: usize,
33        reason: String,
34    },
35    #[error("unsupported SQL dialect '{0}'")]
36    UnsupportedDialect(String),
37    #[error("CREATE INDEX references unknown table '{table}'")]
38    UnknownTable { table: String },
39    #[error("CREATE TRIGGER references unknown table '{table}'")]
40    UnknownTriggerTable { table: String },
41    #[error("duplicate table '{0}'")]
42    DuplicateTable(String),
43    #[error("cannot read '{path}': {source}")]
44    Io {
45        path: String,
46        #[source]
47        source: std::io::Error,
48    },
49}
50
51impl ParseError {
52    pub(crate) fn parse_in_segment(
53        dialect: Dialect,
54        segment: &SqlSegment,
55        error: ParserError,
56    ) -> Self {
57        let message = error.to_string();
58        let (line, column) = extract_sqlparser_location(&message)
59            .map(|(line, column)| remap_segment_location(segment, line, column))
60            .unwrap_or((None, None));
61        let location = parse_location_suffix(line, column, segment);
62
63        Self::Parse {
64            dialect: dialect.as_str(),
65            message,
66            line,
67            column,
68            segment_ordinal: Some(segment.ordinal),
69            segment_start_line: Some(segment.start_line),
70            segment_start_column: Some(segment.start_column),
71            location,
72        }
73    }
74
75    pub(crate) fn unsupported(
76        dialect: Dialect,
77        statement: impl Into<String>,
78        reason: impl Into<String>,
79    ) -> Self {
80        Self::UnsupportedStatement {
81            dialect: dialect.as_str(),
82            statement: statement.into(),
83            reason: reason.into(),
84        }
85    }
86
87    pub(crate) fn segment(
88        dialect: Dialect,
89        line: usize,
90        column: usize,
91        reason: impl Into<String>,
92    ) -> Self {
93        Self::Segment {
94            dialect: dialect.as_str(),
95            line,
96            column,
97            reason: reason.into(),
98        }
99    }
100}
101
102fn extract_sqlparser_location(message: &str) -> Option<(usize, usize)> {
103    let (_, location) = message.rsplit_once(" at Line: ")?;
104    let (line, column) = location.split_once(", Column: ")?;
105    Some((line.parse().ok()?, column.parse().ok()?))
106}
107
108fn remap_segment_location(
109    segment: &SqlSegment,
110    line: usize,
111    column: usize,
112) -> (Option<usize>, Option<usize>) {
113    let source_line = segment.start_line + line.saturating_sub(1);
114    let source_column = if line == 1 {
115        segment.start_column + column.saturating_sub(1)
116    } else {
117        column
118    };
119    (Some(source_line), Some(source_column))
120}
121
122fn parse_location_suffix(
123    line: Option<usize>,
124    column: Option<usize>,
125    segment: &SqlSegment,
126) -> String {
127    match (line, column) {
128        (Some(line), Some(column)) => format!(" at line {line}, column {column}"),
129        _ => format!(
130            " near segment {} starting at line {}, column {}",
131            segment.ordinal, segment.start_line, segment.start_column
132        ),
133    }
134}