Skip to main content

polyglot_sql/
error.rs

1//! Error types for polyglot-sql
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use thiserror::Error;
6
7/// The result type for polyglot operations
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Errors that can occur during SQL parsing and generation
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum Error {
14    /// Error during tokenization
15    #[error("Tokenization error at line {line}, column {column}: {message}")]
16    Tokenize {
17        message: String,
18        line: usize,
19        column: usize,
20        start: usize,
21        end: usize,
22    },
23
24    /// Error during parsing
25    #[error("Parse error at line {line}, column {column}: {message}")]
26    Parse {
27        message: String,
28        line: usize,
29        column: usize,
30        start: usize,
31        end: usize,
32    },
33
34    /// Error during SQL generation
35    #[error("Generation error: {0}")]
36    Generate(String),
37
38    /// Unsupported feature for the target dialect
39    #[error("Unsupported: {feature} is not supported in {dialect}")]
40    Unsupported { feature: String, dialect: String },
41
42    /// Invalid SQL syntax
43    #[error("Syntax error at line {line}, column {column}: {message}")]
44    Syntax {
45        message: String,
46        line: usize,
47        column: usize,
48        start: usize,
49        end: usize,
50    },
51
52    /// Invalid input for an operation after parsing has completed.
53    #[error("Invalid input: {0}")]
54    InvalidInput(String),
55
56    /// A requested output column could not be resolved.
57    #[error("Cannot resolve {target}: {reason}")]
58    ColumnResolution {
59        target: ColumnResolutionTarget,
60        reason: ColumnResolutionReason,
61    },
62
63    /// Internal error (should not happen in normal usage)
64    #[error("Internal error: {0}")]
65    Internal(String),
66}
67
68impl Error {
69    /// Create a tokenization error
70    pub fn tokenize(
71        message: impl Into<String>,
72        line: usize,
73        column: usize,
74        start: usize,
75        end: usize,
76    ) -> Self {
77        Error::Tokenize {
78            message: message.into(),
79            line,
80            column,
81            start,
82            end,
83        }
84    }
85
86    /// Create a parse error with position information
87    pub fn parse(
88        message: impl Into<String>,
89        line: usize,
90        column: usize,
91        start: usize,
92        end: usize,
93    ) -> Self {
94        Error::Parse {
95            message: message.into(),
96            line,
97            column,
98            start,
99            end,
100        }
101    }
102
103    /// Get the line number if available
104    pub fn line(&self) -> Option<usize> {
105        match self {
106            Error::Tokenize { line, .. }
107            | Error::Parse { line, .. }
108            | Error::Syntax { line, .. } => Some(*line),
109            _ => None,
110        }
111    }
112
113    /// Get the column number if available
114    pub fn column(&self) -> Option<usize> {
115        match self {
116            Error::Tokenize { column, .. }
117            | Error::Parse { column, .. }
118            | Error::Syntax { column, .. } => Some(*column),
119            _ => None,
120        }
121    }
122
123    /// Get the start byte offset if available
124    pub fn start(&self) -> Option<usize> {
125        match self {
126            Error::Tokenize { start, .. }
127            | Error::Parse { start, .. }
128            | Error::Syntax { start, .. } => Some(*start),
129            _ => None,
130        }
131    }
132
133    /// Get the end byte offset if available
134    pub fn end(&self) -> Option<usize> {
135        match self {
136            Error::Tokenize { end, .. } | Error::Parse { end, .. } | Error::Syntax { end, .. } => {
137                Some(*end)
138            }
139            _ => None,
140        }
141    }
142
143    /// Create a generation error
144    pub fn generate(message: impl Into<String>) -> Self {
145        Error::Generate(message.into())
146    }
147
148    /// Create an unsupported feature error
149    pub fn unsupported(feature: impl Into<String>, dialect: impl Into<String>) -> Self {
150        Error::Unsupported {
151            feature: feature.into(),
152            dialect: dialect.into(),
153        }
154    }
155
156    /// Create a syntax error
157    pub fn syntax(
158        message: impl Into<String>,
159        line: usize,
160        column: usize,
161        start: usize,
162        end: usize,
163    ) -> Self {
164        Error::Syntax {
165            message: message.into(),
166            line,
167            column,
168            start,
169            end,
170        }
171    }
172
173    /// Create an invalid-input error.
174    pub fn invalid_input(message: impl Into<String>) -> Self {
175        Error::InvalidInput(message.into())
176    }
177
178    /// Create a structured column-resolution error.
179    pub fn column_resolution(
180        target: ColumnResolutionTarget,
181        reason: ColumnResolutionReason,
182    ) -> Self {
183        Error::ColumnResolution { target, reason }
184    }
185
186    /// Create an internal error
187    pub fn internal(message: impl Into<String>) -> Self {
188        Error::Internal(message.into())
189    }
190}
191
192/// The output-column selector that failed to resolve.
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(tag = "kind", rename_all = "snake_case")]
195pub enum ColumnResolutionTarget {
196    /// Resolve an output column by name.
197    Name { name: String },
198    /// Resolve an output column by zero-based ordinal.
199    Ordinal { ordinal: usize },
200}
201
202impl fmt::Display for ColumnResolutionTarget {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        match self {
205            Self::Name { name } => write!(f, "column '{name}'"),
206            Self::Ordinal { ordinal } => write!(f, "output ordinal {ordinal}"),
207        }
208    }
209}
210
211/// Why an output-column selector could not be resolved.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
213#[serde(rename_all = "snake_case")]
214pub enum ColumnResolutionReason {
215    /// The output shape is known and does not contain the requested column.
216    NotFound,
217    /// An unresolved wildcard prevents the output position from being known.
218    Indeterminate,
219    /// More than one output position matches the requested name.
220    Ambiguous,
221}
222
223impl fmt::Display for ColumnResolutionReason {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            Self::NotFound => f.write_str("not found"),
227            Self::Indeterminate => {
228                f.write_str("indeterminate because an output wildcard could not be expanded")
229            }
230            Self::Ambiguous => f.write_str("ambiguous"),
231        }
232    }
233}
234
235/// Severity level for validation errors
236#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
237#[serde(rename_all = "lowercase")]
238pub enum ValidationSeverity {
239    /// An error that prevents the query from being valid
240    Error,
241    /// A warning about potential issues
242    Warning,
243}
244
245/// A single validation error or warning
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ValidationError {
248    /// The error/warning message
249    pub message: String,
250    /// Line number where the error occurred (1-based)
251    pub line: Option<usize>,
252    /// Column number where the error occurred (1-based)
253    pub column: Option<usize>,
254    /// Severity of the validation issue
255    pub severity: ValidationSeverity,
256    /// Error code (e.g., "E001", "W001")
257    pub code: String,
258    /// Start byte offset of the error range
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub start: Option<usize>,
261    /// End byte offset of the error range (exclusive)
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub end: Option<usize>,
264}
265
266impl ValidationError {
267    /// Create a new validation error
268    pub fn error(message: impl Into<String>, code: impl Into<String>) -> Self {
269        Self {
270            message: message.into(),
271            line: None,
272            column: None,
273            severity: ValidationSeverity::Error,
274            code: code.into(),
275            start: None,
276            end: None,
277        }
278    }
279
280    /// Create a new validation warning
281    pub fn warning(message: impl Into<String>, code: impl Into<String>) -> Self {
282        Self {
283            message: message.into(),
284            line: None,
285            column: None,
286            severity: ValidationSeverity::Warning,
287            code: code.into(),
288            start: None,
289            end: None,
290        }
291    }
292
293    /// Set the line number
294    pub fn with_line(mut self, line: usize) -> Self {
295        self.line = Some(line);
296        self
297    }
298
299    /// Set the column number
300    pub fn with_column(mut self, column: usize) -> Self {
301        self.column = Some(column);
302        self
303    }
304
305    /// Set both line and column
306    pub fn with_location(mut self, line: usize, column: usize) -> Self {
307        self.line = Some(line);
308        self.column = Some(column);
309        self
310    }
311
312    /// Set the start/end byte offsets
313    pub fn with_span(mut self, start: Option<usize>, end: Option<usize>) -> Self {
314        self.start = start;
315        self.end = end;
316        self
317    }
318}
319
320/// Result of validating SQL
321#[derive(Debug, Serialize, Deserialize)]
322pub struct ValidationResult {
323    /// Whether the SQL is valid (no errors, warnings are allowed)
324    pub valid: bool,
325    /// List of validation errors and warnings
326    pub errors: Vec<ValidationError>,
327}
328
329impl ValidationResult {
330    /// Create a successful validation result
331    pub fn success() -> Self {
332        Self {
333            valid: true,
334            errors: Vec::new(),
335        }
336    }
337
338    /// Create a validation result with errors
339    pub fn with_errors(errors: Vec<ValidationError>) -> Self {
340        let has_errors = errors
341            .iter()
342            .any(|e| e.severity == ValidationSeverity::Error);
343        Self {
344            valid: !has_errors,
345            errors,
346        }
347    }
348
349    /// Add an error to the result
350    pub fn add_error(&mut self, error: ValidationError) {
351        if error.severity == ValidationSeverity::Error {
352            self.valid = false;
353        }
354        self.errors.push(error);
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_parse_error_has_position() {
364        let err = Error::parse("test message", 5, 10, 20, 25);
365        assert_eq!(err.line(), Some(5));
366        assert_eq!(err.column(), Some(10));
367        assert_eq!(err.start(), Some(20));
368        assert_eq!(err.end(), Some(25));
369        assert!(err.to_string().contains("line 5"));
370        assert!(err.to_string().contains("column 10"));
371        assert!(err.to_string().contains("test message"));
372    }
373
374    #[test]
375    fn test_tokenize_error_has_position() {
376        let err = Error::tokenize("bad token", 3, 7, 15, 20);
377        assert_eq!(err.line(), Some(3));
378        assert_eq!(err.column(), Some(7));
379        assert_eq!(err.start(), Some(15));
380        assert_eq!(err.end(), Some(20));
381    }
382
383    #[test]
384    fn test_generate_error_has_no_position() {
385        let err = Error::generate("gen error");
386        assert_eq!(err.line(), None);
387        assert_eq!(err.column(), None);
388        assert_eq!(err.start(), None);
389        assert_eq!(err.end(), None);
390    }
391
392    #[test]
393    fn test_parse_error_position_from_parser() {
394        // Parse invalid SQL and verify the error carries position info
395        use crate::dialects::{Dialect, DialectType};
396        let d = Dialect::get(DialectType::Generic);
397        let result = d.parse("SELECT 1 + 2)");
398        assert!(result.is_err());
399        let err = result.unwrap_err();
400        assert!(
401            err.line().is_some(),
402            "Parse error should have line: {:?}",
403            err
404        );
405        assert!(
406            err.column().is_some(),
407            "Parse error should have column: {:?}",
408            err
409        );
410        assert_eq!(err.line(), Some(1));
411    }
412
413    #[test]
414    fn test_parse_error_has_span_offsets() {
415        use crate::dialects::{Dialect, DialectType};
416        let d = Dialect::get(DialectType::Generic);
417        let result = d.parse("SELECT 1 + 2)");
418        assert!(result.is_err());
419        let err = result.unwrap_err();
420        assert!(
421            err.start().is_some(),
422            "Parse error should have start offset: {:?}",
423            err
424        );
425        assert!(
426            err.end().is_some(),
427            "Parse error should have end offset: {:?}",
428            err
429        );
430        // The ')' is at byte offset 12
431        assert_eq!(err.start(), Some(12));
432        assert_eq!(err.end(), Some(13));
433    }
434
435    #[test]
436    fn test_validation_error_with_span() {
437        let err = ValidationError::error("test", "E001")
438            .with_location(1, 5)
439            .with_span(Some(4), Some(10));
440        assert_eq!(err.start, Some(4));
441        assert_eq!(err.end, Some(10));
442        assert_eq!(err.line, Some(1));
443        assert_eq!(err.column, Some(5));
444    }
445}