Skip to main content

oxigdal_query/
error.rs

1//! Error types for query processing.
2//!
3//! # Error Codes
4//!
5//! Each error variant has an associated error code (e.g., Q001, Q002) for easier
6//! debugging and documentation. Error codes are stable across versions.
7//!
8//! # Helper Methods
9//!
10//! All error types provide:
11//! - `code()` - Returns the error code
12//! - `suggestion()` - Returns helpful hints including alternative query structures
13//! - `context()` - Returns additional context including rule identification and query fragments
14
15/// Result type for query operations.
16pub type Result<T> = std::result::Result<T, QueryError>;
17
18/// Errors that can occur during query processing.
19#[derive(Debug, thiserror::Error)]
20pub enum QueryError {
21    /// Parse error with position information.
22    #[error("Parse error at line {line}, column {column}: {message}")]
23    ParseError {
24        /// Error message.
25        message: String,
26        /// Line number (1-based).
27        line: usize,
28        /// Column number (1-based).
29        column: usize,
30    },
31
32    /// Semantic error in query.
33    #[error("Semantic error: {0}")]
34    SemanticError(String),
35
36    /// Optimization error.
37    #[error("Optimization error: {0}")]
38    OptimizationError(String),
39
40    /// Execution error.
41    #[error("Execution error: {0}")]
42    ExecutionError(String),
43
44    /// Type mismatch error.
45    #[error("Type mismatch: expected {expected}, got {actual}")]
46    TypeMismatch {
47        /// Expected type.
48        expected: String,
49        /// Actual type.
50        actual: String,
51    },
52
53    /// Column not found error.
54    #[error("Column not found: {0}")]
55    ColumnNotFound(String),
56
57    /// Table not found error.
58    #[error("Table not found: {0}")]
59    TableNotFound(String),
60
61    /// Function not found error.
62    #[error("Function not found: {0}")]
63    FunctionNotFound(String),
64
65    /// Invalid argument error.
66    #[error("Invalid argument: {0}")]
67    InvalidArgument(String),
68
69    /// Index not found error.
70    #[error("Index not found: {0}")]
71    IndexNotFound(String),
72
73    /// IO error.
74    #[error("IO error: {0}")]
75    IoError(#[from] std::io::Error),
76
77    /// Internal error.
78    #[error("Internal error: {0}")]
79    InternalError(String),
80
81    /// Unsupported operation.
82    #[error("Unsupported operation: {0}")]
83    Unsupported(String),
84
85    /// SQL parser error.
86    #[error("SQL parser error: {0}")]
87    SqlParserError(String),
88
89    /// Cache error.
90    #[error("Cache error: {0}")]
91    CacheError(String),
92
93    /// Parallel execution error.
94    #[error("Parallel execution error: {0}")]
95    ParallelError(String),
96}
97
98impl From<sqlparser::parser::ParserError> for QueryError {
99    fn from(err: sqlparser::parser::ParserError) -> Self {
100        QueryError::SqlParserError(err.to_string())
101    }
102}
103
104impl QueryError {
105    /// Create a parse error.
106    pub fn parse_error(message: impl Into<String>, line: usize, column: usize) -> Self {
107        QueryError::ParseError {
108            message: message.into(),
109            line,
110            column,
111        }
112    }
113
114    /// Create a semantic error.
115    pub fn semantic(message: impl Into<String>) -> Self {
116        QueryError::SemanticError(message.into())
117    }
118
119    /// Create an optimization error.
120    pub fn optimization(message: impl Into<String>) -> Self {
121        QueryError::OptimizationError(message.into())
122    }
123
124    /// Create an execution error.
125    pub fn execution(message: impl Into<String>) -> Self {
126        QueryError::ExecutionError(message.into())
127    }
128
129    /// Create a type mismatch error.
130    pub fn type_mismatch(expected: impl Into<String>, actual: impl Into<String>) -> Self {
131        QueryError::TypeMismatch {
132            expected: expected.into(),
133            actual: actual.into(),
134        }
135    }
136
137    /// Create an internal error.
138    pub fn internal(message: impl Into<String>) -> Self {
139        QueryError::InternalError(message.into())
140    }
141
142    /// Create an unsupported operation error.
143    pub fn unsupported(message: impl Into<String>) -> Self {
144        QueryError::Unsupported(message.into())
145    }
146
147    /// Get the error code for this query error
148    ///
149    /// Error codes are stable across versions and can be used for documentation
150    /// and error handling.
151    pub fn code(&self) -> &'static str {
152        match self {
153            Self::ParseError { .. } => "Q001",
154            Self::SemanticError(_) => "Q002",
155            Self::OptimizationError(_) => "Q003",
156            Self::ExecutionError(_) => "Q004",
157            Self::TypeMismatch { .. } => "Q005",
158            Self::ColumnNotFound(_) => "Q006",
159            Self::TableNotFound(_) => "Q007",
160            Self::FunctionNotFound(_) => "Q008",
161            Self::InvalidArgument(_) => "Q009",
162            Self::IndexNotFound(_) => "Q010",
163            Self::IoError(_) => "Q011",
164            Self::InternalError(_) => "Q012",
165            Self::Unsupported(_) => "Q013",
166            Self::SqlParserError(_) => "Q014",
167            Self::CacheError(_) => "Q015",
168            Self::ParallelError(_) => "Q016",
169        }
170    }
171
172    /// Get a helpful suggestion for fixing this query error
173    ///
174    /// Returns a human-readable suggestion including alternative query structures.
175    pub fn suggestion(&self) -> Option<&'static str> {
176        match self {
177            Self::ParseError { .. } => Some(
178                "Check SQL syntax. Common issues: missing commas, unmatched parentheses, or incorrect keywords",
179            ),
180            Self::SemanticError(msg) => {
181                if msg.contains("aggregate") {
182                    Some(
183                        "When using aggregate functions, non-aggregated columns must be in GROUP BY",
184                    )
185                } else if msg.contains("subquery") {
186                    Some("Ensure subqueries return the expected number of columns")
187                } else {
188                    Some("Verify table and column references are correct")
189                }
190            }
191            Self::OptimizationError(msg) => {
192                if msg.contains("join") {
193                    Some("Try simplifying the join structure or adding indexes on join columns")
194                } else if msg.contains("predicate") {
195                    Some("Rewrite complex predicates or break into multiple simpler conditions")
196                } else {
197                    Some("Simplify the query or add appropriate indexes")
198                }
199            }
200            Self::ExecutionError(_) => Some(
201                "Check data values and constraints. Ensure operations are valid for the data types",
202            ),
203            Self::TypeMismatch { .. } => {
204                Some("Cast values to the expected type or modify the query to use compatible types")
205            }
206            Self::ColumnNotFound(_) => Some(
207                "Use DESCRIBE or SELECT * to list available columns. Check for typos or case sensitivity",
208            ),
209            Self::TableNotFound(_) => {
210                Some("Verify the table name is correct. Use SHOW TABLES to list available tables")
211            }
212            Self::FunctionNotFound(_) => {
213                Some("Check function name spelling. Use built-in functions or create a UDF")
214            }
215            Self::InvalidArgument(_) => {
216                Some("Check function documentation for correct argument types and count")
217            }
218            Self::IndexNotFound(_) => {
219                Some("Create an index using CREATE INDEX or use a different access pattern")
220            }
221            Self::IoError(_) => {
222                Some("Check file permissions and disk space. Ensure data files are accessible")
223            }
224            Self::InternalError(_) => {
225                Some("This is likely a bug. Please report it with the query that triggered it")
226            }
227            Self::Unsupported(_) => {
228                Some("Use an alternative query structure or feature that is supported")
229            }
230            Self::SqlParserError(_) => {
231                Some("Fix SQL syntax errors. Refer to SQL standard or documentation")
232            }
233            Self::CacheError(_) => Some("Clear cache or increase cache size"),
234            Self::ParallelError(_) => {
235                Some("Reduce parallelism level or check for data race conditions")
236            }
237        }
238    }
239
240    /// Get additional context about this query error
241    ///
242    /// Returns structured context including rule identification and query fragments.
243    pub fn context(&self) -> ErrorContext {
244        match self {
245            Self::ParseError {
246                message,
247                line,
248                column,
249            } => ErrorContext::new("parse_error")
250                .with_detail("message", message.clone())
251                .with_detail("line", line.to_string())
252                .with_detail("column", column.to_string()),
253            Self::SemanticError(msg) => {
254                ErrorContext::new("semantic_error").with_detail("message", msg.clone())
255            }
256            Self::OptimizationError(msg) => ErrorContext::new("optimization_error")
257                .with_detail("message", msg.clone())
258                .with_detail("phase", self.extract_optimization_phase(msg)),
259            Self::ExecutionError(msg) => {
260                ErrorContext::new("execution_error").with_detail("message", msg.clone())
261            }
262            Self::TypeMismatch { expected, actual } => ErrorContext::new("type_mismatch")
263                .with_detail("expected", expected.clone())
264                .with_detail("actual", actual.clone()),
265            Self::ColumnNotFound(name) => {
266                ErrorContext::new("column_not_found").with_detail("column", name.clone())
267            }
268            Self::TableNotFound(name) => {
269                ErrorContext::new("table_not_found").with_detail("table", name.clone())
270            }
271            Self::FunctionNotFound(name) => {
272                ErrorContext::new("function_not_found").with_detail("function", name.clone())
273            }
274            Self::InvalidArgument(msg) => {
275                ErrorContext::new("invalid_argument").with_detail("message", msg.clone())
276            }
277            Self::IndexNotFound(name) => {
278                ErrorContext::new("index_not_found").with_detail("index", name.clone())
279            }
280            Self::IoError(e) => ErrorContext::new("io_error").with_detail("error", e.to_string()),
281            Self::InternalError(msg) => {
282                ErrorContext::new("internal_error").with_detail("message", msg.clone())
283            }
284            Self::Unsupported(msg) => {
285                ErrorContext::new("unsupported").with_detail("message", msg.clone())
286            }
287            Self::SqlParserError(msg) => {
288                ErrorContext::new("sql_parser_error").with_detail("message", msg.clone())
289            }
290            Self::CacheError(msg) => {
291                ErrorContext::new("cache_error").with_detail("message", msg.clone())
292            }
293            Self::ParallelError(msg) => {
294                ErrorContext::new("parallel_error").with_detail("message", msg.clone())
295            }
296        }
297    }
298
299    /// Extract optimization phase from error message
300    fn extract_optimization_phase(&self, msg: &str) -> String {
301        if msg.contains("predicate pushdown") {
302            "predicate_pushdown".to_string()
303        } else if msg.contains("join reorder") {
304            "join_reordering".to_string()
305        } else if msg.contains("projection") {
306            "projection_pushdown".to_string()
307        } else if msg.contains("CSE") || msg.contains("common subexpression") {
308            "common_subexpression_elimination".to_string()
309        } else {
310            "unknown".to_string()
311        }
312    }
313}
314
315/// Additional context information for query errors
316#[derive(Debug, Clone)]
317pub struct ErrorContext {
318    /// Error category for grouping similar errors
319    pub category: &'static str,
320    /// Additional details including rule identification and query fragments
321    pub details: Vec<(String, String)>,
322}
323
324impl ErrorContext {
325    /// Create a new error context
326    pub fn new(category: &'static str) -> Self {
327        Self {
328            category,
329            details: Vec::new(),
330        }
331    }
332
333    /// Add a detail to the context
334    pub fn with_detail(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
335        self.details.push((key.into(), value.into()));
336        self
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_error_codes() {
346        let err = QueryError::ParseError {
347            message: "test".to_string(),
348            line: 1,
349            column: 5,
350        };
351        assert_eq!(err.code(), "Q001");
352
353        let err = QueryError::OptimizationError("join reorder failed".to_string());
354        assert_eq!(err.code(), "Q003");
355
356        let err = QueryError::ColumnNotFound("id".to_string());
357        assert_eq!(err.code(), "Q006");
358    }
359
360    #[test]
361    fn test_error_suggestions() {
362        let err = QueryError::OptimizationError("join reorder failed".to_string());
363        assert!(err.suggestion().is_some());
364        assert!(err.suggestion().is_some_and(|s| s.contains("join")));
365
366        let err = QueryError::ColumnNotFound("id".to_string());
367        assert!(err.suggestion().is_some());
368        assert!(
369            err.suggestion()
370                .is_some_and(|s| s.contains("DESCRIBE") || s.contains("SELECT *"))
371        );
372
373        let err = QueryError::SemanticError("aggregate function".to_string());
374        assert!(err.suggestion().is_some());
375        assert!(err.suggestion().is_some_and(|s| s.contains("GROUP BY")));
376    }
377
378    #[test]
379    fn test_error_context() {
380        let err = QueryError::ParseError {
381            message: "unexpected token".to_string(),
382            line: 10,
383            column: 25,
384        };
385        let ctx = err.context();
386        assert_eq!(ctx.category, "parse_error");
387        assert!(ctx.details.iter().any(|(k, v)| k == "line" && v == "10"));
388        assert!(ctx.details.iter().any(|(k, v)| k == "column" && v == "25"));
389
390        let err = QueryError::TypeMismatch {
391            expected: "INTEGER".to_string(),
392            actual: "TEXT".to_string(),
393        };
394        let ctx = err.context();
395        assert_eq!(ctx.category, "type_mismatch");
396        assert!(
397            ctx.details
398                .iter()
399                .any(|(k, v)| k == "expected" && v == "INTEGER")
400        );
401        assert!(
402            ctx.details
403                .iter()
404                .any(|(k, v)| k == "actual" && v == "TEXT")
405        );
406    }
407
408    #[test]
409    fn test_optimization_phase_extraction() {
410        let err = QueryError::OptimizationError("predicate pushdown failed".to_string());
411        let ctx = err.context();
412        assert!(
413            ctx.details
414                .iter()
415                .any(|(k, v)| k == "phase" && v == "predicate_pushdown")
416        );
417
418        let err = QueryError::OptimizationError("join reorder failed".to_string());
419        let ctx = err.context();
420        assert!(
421            ctx.details
422                .iter()
423                .any(|(k, v)| k == "phase" && v == "join_reordering")
424        );
425
426        let err = QueryError::OptimizationError("CSE failed".to_string());
427        let ctx = err.context();
428        assert!(
429            ctx.details
430                .iter()
431                .any(|(k, v)| k == "phase" && v == "common_subexpression_elimination")
432        );
433    }
434
435    #[test]
436    fn test_helper_constructors() {
437        let err = QueryError::parse_error("test", 1, 5);
438        assert!(matches!(err, QueryError::ParseError { .. }));
439
440        let err = QueryError::type_mismatch("int", "string");
441        assert!(matches!(err, QueryError::TypeMismatch { .. }));
442
443        let err = QueryError::optimization("test");
444        assert!(matches!(err, QueryError::OptimizationError(_)));
445    }
446}