Skip to main content

polyglot_sql/
error.rs

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