Skip to main content

sqry_core/query/
error.rs

1//! Error types for the query language
2//!
3//! This module defines comprehensive error types for lexing, parsing,
4//! validation, and execution of queries, with helpful error messages
5//! and position information.
6
7use crate::query::types::{FieldType, Operator, Span, Value};
8use miette::{Diagnostic, LabeledSpan, SourceCode};
9use serde_json::Value as JsonValue;
10use std::path::PathBuf;
11use thiserror::Error;
12
13/// Top-level query error encompassing all error types
14#[derive(Debug, Error)]
15pub enum QueryError {
16    /// Lexical analysis error
17    #[error("Syntax error: {0}")]
18    Lex(#[from] LexError),
19
20    /// Parsing error
21    #[error("Parse error: {0}")]
22    Parse(#[from] ParseError),
23
24    /// Validation error
25    #[error("Validation error: {0}")]
26    Validation(#[from] ValidationError),
27
28    /// Execution error
29    #[error("Execution error: {0}")]
30    Execution(#[from] ExecutionError),
31}
32
33impl QueryError {
34    /// Get CLI exit code for this error
35    ///
36    /// Returns:
37    /// - 2 for syntax/validation errors (user input errors)
38    /// - 1 for execution errors (system/runtime errors)
39    #[must_use]
40    pub fn exit_code(&self) -> i32 {
41        match self {
42            QueryError::Lex(_) | QueryError::Parse(_) | QueryError::Validation(_) => 2,
43            QueryError::Execution(_) => 1,
44        }
45    }
46
47    /// Wrap this error with source code context for rich diagnostics
48    pub fn with_source(self, source: impl Into<String>) -> RichQueryError {
49        RichQueryError {
50            error: self,
51            source: source.into(),
52        }
53    }
54}
55
56/// Query error with source code context for rich diagnostics
57///
58/// This wrapper provides beautiful error messages with code snippets,
59/// syntax highlighting, and helpful labels using the miette crate.
60#[derive(Debug, Error)]
61#[error("{error}")]
62pub struct RichQueryError {
63    #[source]
64    error: QueryError,
65    source: String,
66}
67
68impl RichQueryError {
69    /// Create a new rich query error with source context
70    pub fn new(error: QueryError, source: impl Into<String>) -> Self {
71        Self {
72            error,
73            source: source.into(),
74        }
75    }
76
77    /// Get the underlying query error
78    #[must_use]
79    pub fn inner(&self) -> &QueryError {
80        &self.error
81    }
82
83    /// Get CLI exit code for this error
84    #[must_use]
85    pub fn exit_code(&self) -> i32 {
86        self.error.exit_code()
87    }
88
89    /// Convert error to structured JSON for automation/MCP workflows
90    ///
91    /// Returns a JSON object with:
92    /// - `code`: Error code (e.g., "`sqry::validation`")
93    /// - `message`: Human-readable error message
94    /// - `query`: The original query string
95    /// - `span`: Error location (start/end positions) if available
96    /// - `label`: Descriptive label for the error location
97    /// - `help`: Helpful suggestions and guidance
98    /// - `suggestion`: Suggested fix if available
99    #[must_use]
100    pub fn to_json_value(&self) -> JsonValue {
101        let code = match &self.error {
102            QueryError::Lex(_) => "sqry::syntax",
103            QueryError::Parse(_) => "sqry::parse",
104            QueryError::Validation(_) => "sqry::validation",
105            QueryError::Execution(_) => "sqry::execution",
106        };
107
108        let message = self.error.to_string();
109        let query = &self.source;
110
111        // Extract span and label information
112        let (span, label, suggestion) = self.extract_span_and_label();
113
114        // Build help text
115        let help = self.build_help_text();
116
117        // Build JSON object
118        let mut json = serde_json::json!({
119            "error": {
120                "code": code,
121                "message": message,
122                "query": query,
123            }
124        });
125
126        // Add optional fields if present
127        if let Some(span_value) = span {
128            json["error"]["span"] = span_value;
129        }
130        if let Some(label_value) = label {
131            json["error"]["label"] = label_value.into();
132        }
133        json["error"]["help"] = help.into();
134        if let Some(suggestion_value) = suggestion {
135            json["error"]["suggestion"] = suggestion_value.into();
136        }
137
138        json
139    }
140
141    /// Extract span, label, and suggestion from the error
142    #[allow(clippy::too_many_lines)] // Large match table keeps diagnostic mappings centralized.
143    fn extract_span_and_label(&self) -> (Option<JsonValue>, Option<String>, Option<String>) {
144        match &self.error {
145            QueryError::Lex(e) => match e {
146                LexError::UnterminatedString { span } => (
147                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
148                    Some("unterminated string literal starts here".to_string()),
149                    None,
150                ),
151                LexError::UnterminatedRegex { span } => (
152                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
153                    Some("unterminated regex literal starts here".to_string()),
154                    None,
155                ),
156                LexError::InvalidEscape { span, char } => (
157                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
158                    Some(format!("invalid escape sequence '\\{char}'")),
159                    None,
160                ),
161                LexError::InvalidUnicodeEscape { span, .. } => (
162                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
163                    Some("invalid unicode escape sequence".to_string()),
164                    None,
165                ),
166                LexError::InvalidNumber { span, .. } => (
167                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
168                    Some("invalid number format".to_string()),
169                    None,
170                ),
171                LexError::NumberOverflow { span, .. } => (
172                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
173                    Some("number too large".to_string()),
174                    None,
175                ),
176                LexError::InvalidRegex { span, .. } => (
177                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
178                    Some("invalid regex pattern".to_string()),
179                    None,
180                ),
181                LexError::UnexpectedChar { span, char } => (
182                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
183                    Some(format!("unexpected character '{char}'")),
184                    None,
185                ),
186                LexError::UnexpectedEof => (None, None, None),
187            },
188            QueryError::Parse(e) => match e {
189                ParseError::UnmatchedParen { open_span, .. } => (
190                    Some(serde_json::json!({ "start": open_span.start, "end": open_span.end })),
191                    Some("unmatched opening parenthesis".to_string()),
192                    None,
193                ),
194                ParseError::ExpectedIdentifier { token } => (
195                    Some(serde_json::json!({ "start": token.span.start, "end": token.span.end })),
196                    Some("expected field name here".to_string()),
197                    None,
198                ),
199                ParseError::ExpectedOperator { token } => (
200                    Some(serde_json::json!({ "start": token.span.start, "end": token.span.end })),
201                    Some("expected operator here".to_string()),
202                    None,
203                ),
204                ParseError::ExpectedValue { token } => (
205                    Some(serde_json::json!({ "start": token.span.start, "end": token.span.end })),
206                    Some("expected value here".to_string()),
207                    None,
208                ),
209                ParseError::UnexpectedToken { token, .. } => (
210                    Some(serde_json::json!({ "start": token.span.start, "end": token.span.end })),
211                    Some("unexpected token".to_string()),
212                    None,
213                ),
214                ParseError::InvalidSyntax { span, .. } => (
215                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
216                    Some("invalid syntax".to_string()),
217                    None,
218                ),
219                ParseError::EmptyQuery | ParseError::UnexpectedEof { .. } => (None, None, None),
220            },
221            QueryError::Validation(e) => match e {
222                ValidationError::UnknownField {
223                    span, suggestion, ..
224                } => (
225                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
226                    Some("unknown field".to_string()),
227                    suggestion.clone(),
228                ),
229                ValidationError::InvalidOperator { span, .. } => (
230                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
231                    Some("invalid operator for this field type".to_string()),
232                    None,
233                ),
234                ValidationError::TypeMismatch { span, .. } => (
235                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
236                    Some("type mismatch".to_string()),
237                    None,
238                ),
239                ValidationError::InvalidEnumValue { span, .. } => (
240                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
241                    Some("invalid enum value".to_string()),
242                    None,
243                ),
244                ValidationError::InvalidRegexPattern { span, .. } => (
245                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
246                    Some("invalid regex pattern".to_string()),
247                    None,
248                ),
249                ValidationError::ImpossibleQuery { span, .. } => (
250                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
251                    Some("contradictory condition".to_string()),
252                    None,
253                ),
254                ValidationError::FieldNotAvailable { span, .. } => (
255                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
256                    Some("field not available".to_string()),
257                    None,
258                ),
259                ValidationError::UnsafeFuzzyCorrection {
260                    span, suggestion, ..
261                } => (
262                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
263                    Some("field requires exact match".to_string()),
264                    Some(suggestion.clone()),
265                ),
266                ValidationError::SubqueryDepthExceeded { span, .. } => (
267                    Some(serde_json::json!({ "start": span.start, "end": span.end })),
268                    Some("subquery depth exceeded".to_string()),
269                    None,
270                ),
271            },
272            QueryError::Execution(_) => (None, None, None),
273        }
274    }
275
276    /// Build help text for the error
277    #[allow(clippy::too_many_lines)] // Large match table keeps help text centralized.
278    fn build_help_text(&self) -> String {
279        match &self.error {
280            QueryError::Lex(e) => match e {
281                LexError::UnterminatedString { .. } => {
282                    "String literals must be closed with a matching quote".to_string()
283                }
284                LexError::UnterminatedRegex { .. } => {
285                    "Regex literals must be closed with a closing '/'".to_string()
286                }
287                LexError::InvalidEscape { .. } => {
288                    "Valid escape sequences: \\n, \\t, \\r, \\\\, \\\", \\'".to_string()
289                }
290                LexError::InvalidUnicodeEscape { .. } => {
291                    "Unicode escapes should be in the format \\u{XXXX}".to_string()
292                }
293                LexError::InvalidNumber { .. } => {
294                    "Numbers must be valid integers".to_string()
295                }
296                LexError::NumberOverflow { .. } => {
297                    "Number is too large. Maximum value is i64::MAX".to_string()
298                }
299                LexError::InvalidRegex { .. } => {
300                    "Check your regex syntax. Common errors: unbalanced brackets, invalid escapes"
301                        .to_string()
302                }
303                LexError::UnexpectedChar { .. } => {
304                    "Remove or escape the unexpected character".to_string()
305                }
306                LexError::UnexpectedEof => "Query ended unexpectedly".to_string(),
307            },
308            QueryError::Parse(e) => match e {
309                ParseError::UnmatchedParen { .. } => "Add a closing ')'".to_string(),
310                ParseError::ExpectedIdentifier { .. } => {
311                    "Provide a field name (e.g., 'kind', 'name', 'lang')".to_string()
312                }
313                ParseError::ExpectedOperator { .. } => {
314                    "Add an operator like ':', '~=', '>', '<', '>=', or '<='".to_string()
315                }
316                ParseError::ExpectedValue { .. } => {
317                    "Provide a value (string, number, or boolean)".to_string()
318                }
319                ParseError::UnexpectedToken { .. } => {
320                    "Remove the unexpected token or add 'AND', 'OR', 'NOT' between predicates"
321                        .to_string()
322                }
323                ParseError::InvalidSyntax { .. } => {
324                    "Check your query syntax. Use explicit 'AND', 'OR', 'NOT' between predicates"
325                        .to_string()
326                }
327                ParseError::EmptyQuery => "Provide a query expression".to_string(),
328                ParseError::UnexpectedEof { .. } => "Query ended unexpectedly".to_string(),
329            },
330            QueryError::Validation(e) => match e {
331                ValidationError::UnknownField { suggestion, .. } => {
332                    if let Some(sugg) = suggestion {
333                        format!(
334                            "Did you mean '{sugg}'? Use 'sqry query --help' to see available fields"
335                        )
336                    } else {
337                        "Use 'sqry query --help' to see available fields".to_string()
338                    }
339                }
340                ValidationError::InvalidOperator {
341                    field,
342                    valid_operators,
343                    ..
344                } => format!(
345                    "Field '{}' supports these operators: {}",
346                    field,
347                    valid_operators
348                        .iter()
349                        .map(|op| format!("{op:?}"))
350                        .collect::<Vec<_>>()
351                        .join(", ")
352                ),
353                ValidationError::TypeMismatch { expected, got, .. } => {
354                    format!("Expected {expected:?}, got {got:?}")
355                }
356                ValidationError::InvalidEnumValue { valid_values, .. } => {
357                    format!("Valid values: {}", valid_values.join(", "))
358                }
359                ValidationError::InvalidRegexPattern { error, .. } => {
360                    format!("Regex error: {error}")
361                }
362                ValidationError::ImpossibleQuery { message, .. } => message.clone(),
363                ValidationError::FieldNotAvailable { field, reason, .. } => {
364                    format!("Field '{field}' is not available: {reason}")
365                }
366                ValidationError::UnsafeFuzzyCorrection {
367                    input, suggestion, ..
368                } => format!(
369                    "Field '{input}' requires exact spelling. Use '{suggestion}' instead."
370                ),
371                ValidationError::SubqueryDepthExceeded {
372                    depth, max_depth, ..
373                } => format!(
374                    "Subquery nesting depth ({depth}) exceeds maximum ({max_depth}). Simplify the query."
375                ),
376            },
377            QueryError::Execution(e) => match e {
378                ExecutionError::IndexNotFound { .. } => {
379                    "Create an index with 'sqry index'".to_string()
380                }
381                ExecutionError::RelationQueryRequiresIndex { .. } => {
382                    "Run 'sqry index' to create an index with relation data".to_string()
383                }
384                ExecutionError::LegacyIndexMissingRelations { .. } => {
385                    "Rebuild the index with 'sqry index --force' to enable relation queries"
386                        .to_string()
387                }
388                ExecutionError::IndexMissingCDSupport { .. } => {
389                    "Rebuild the index with 'sqry index --force' to enable CD predicates (duplicates:, circular:, unused:)"
390                        .to_string()
391                }
392                ExecutionError::PluginNotFound { .. } => {
393                    "The language may not be supported yet".to_string()
394                }
395                ExecutionError::FieldEvaluationFailed { .. } => {
396                    "Check the field value and type".to_string()
397                }
398                ExecutionError::TypeMismatch { .. } => {
399                    "Ensure the value matches the expected type".to_string()
400                }
401                ExecutionError::InvalidRegex { .. } | ExecutionError::RegexError(_) => {
402                    "Check your regex syntax".to_string()
403                }
404                ExecutionError::InvalidGlob { .. } => {
405                    "Check your glob pattern syntax".to_string()
406                }
407                ExecutionError::TooManyMatches { .. } => {
408                    "Use a more specific pattern or increase the limit".to_string()
409                }
410                ExecutionError::FileReadError(_) => {
411                    "Check file permissions and paths".to_string()
412                }
413                ExecutionError::Timeout { .. } => {
414                    "Consider simplifying the query or increasing the timeout".to_string()
415                }
416                ExecutionError::Cancelled => {
417                    "The query was cancelled by the client".to_string()
418                }
419                ExecutionError::IndexVersionMismatch { .. } => {
420                    "Rebuild the index with 'sqry index --force'".to_string()
421                }
422            },
423        }
424    }
425}
426
427impl Diagnostic for RichQueryError {
428    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
429        match &self.error {
430            QueryError::Lex(_) => Some(Box::new("sqry::syntax")),
431            QueryError::Parse(_) => Some(Box::new("sqry::parse")),
432            QueryError::Validation(_) => Some(Box::new("sqry::validation")),
433            QueryError::Execution(_) => Some(Box::new("sqry::execution")),
434        }
435    }
436
437    fn source_code(&self) -> Option<&dyn SourceCode> {
438        Some(&self.source as &dyn SourceCode)
439    }
440
441    #[allow(clippy::too_many_lines)] // Large match table keeps diagnostic mappings centralized.
442    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
443        let labels: Vec<LabeledSpan> = match &self.error {
444            QueryError::Lex(e) => match e {
445                LexError::UnterminatedString { span } => {
446                    vec![LabeledSpan::new_with_span(
447                        Some("unterminated string literal starts here".to_string()),
448                        span_to_source_span(span),
449                    )]
450                }
451                LexError::UnterminatedRegex { span } => {
452                    vec![LabeledSpan::new_with_span(
453                        Some("unterminated regex literal starts here".to_string()),
454                        span_to_source_span(span),
455                    )]
456                }
457                LexError::InvalidEscape { span, char } => {
458                    vec![LabeledSpan::new_with_span(
459                        Some(format!("invalid escape sequence '\\{char}'")),
460                        span_to_source_span(span),
461                    )]
462                }
463                LexError::InvalidUnicodeEscape { span, .. } => {
464                    vec![LabeledSpan::new_with_span(
465                        Some("invalid unicode escape sequence".to_string()),
466                        span_to_source_span(span),
467                    )]
468                }
469                LexError::InvalidNumber { span, .. } => {
470                    vec![LabeledSpan::new_with_span(
471                        Some("invalid number format".to_string()),
472                        span_to_source_span(span),
473                    )]
474                }
475                LexError::NumberOverflow { span, .. } => {
476                    vec![LabeledSpan::new_with_span(
477                        Some("number too large".to_string()),
478                        span_to_source_span(span),
479                    )]
480                }
481                LexError::InvalidRegex { span, .. } => {
482                    vec![LabeledSpan::new_with_span(
483                        Some("invalid regex pattern".to_string()),
484                        span_to_source_span(span),
485                    )]
486                }
487                LexError::UnexpectedChar { span, char } => {
488                    vec![LabeledSpan::new_with_span(
489                        Some(format!("unexpected character '{char}'")),
490                        span_to_source_span(span),
491                    )]
492                }
493                LexError::UnexpectedEof => vec![],
494            },
495            QueryError::Parse(e) => match e {
496                ParseError::UnmatchedParen { open_span, .. } => {
497                    vec![LabeledSpan::new_with_span(
498                        Some("unmatched opening parenthesis".to_string()),
499                        span_to_source_span(open_span),
500                    )]
501                }
502                ParseError::ExpectedIdentifier { token } => {
503                    vec![LabeledSpan::new_with_span(
504                        Some("expected field name here".to_string()),
505                        span_to_source_span(&token.span),
506                    )]
507                }
508                ParseError::ExpectedOperator { token } => {
509                    vec![LabeledSpan::new_with_span(
510                        Some("expected operator here".to_string()),
511                        span_to_source_span(&token.span),
512                    )]
513                }
514                ParseError::ExpectedValue { token } => {
515                    vec![LabeledSpan::new_with_span(
516                        Some("expected value here".to_string()),
517                        span_to_source_span(&token.span),
518                    )]
519                }
520                ParseError::UnexpectedToken { token, .. } => {
521                    vec![LabeledSpan::new_with_span(
522                        Some("unexpected token".to_string()),
523                        span_to_source_span(&token.span),
524                    )]
525                }
526                ParseError::InvalidSyntax { span, .. } => {
527                    vec![LabeledSpan::new_with_span(
528                        Some("invalid syntax".to_string()),
529                        span_to_source_span(span),
530                    )]
531                }
532                ParseError::EmptyQuery | ParseError::UnexpectedEof { .. } => vec![],
533            },
534            QueryError::Validation(e) => match e {
535                ValidationError::UnknownField {
536                    span, suggestion, ..
537                } => {
538                    let label = if let Some(sugg) = suggestion {
539                        format!("unknown field, did you mean '{sugg}'?")
540                    } else {
541                        "unknown field".to_string()
542                    };
543                    vec![LabeledSpan::new_with_span(
544                        Some(label),
545                        span_to_source_span(span),
546                    )]
547                }
548                ValidationError::InvalidOperator { span, .. } => {
549                    vec![LabeledSpan::new_with_span(
550                        Some("invalid operator for this field type".to_string()),
551                        span_to_source_span(span),
552                    )]
553                }
554                ValidationError::TypeMismatch { span, .. } => {
555                    vec![LabeledSpan::new_with_span(
556                        Some("type mismatch".to_string()),
557                        span_to_source_span(span),
558                    )]
559                }
560                ValidationError::InvalidEnumValue { span, .. } => {
561                    vec![LabeledSpan::new_with_span(
562                        Some("invalid enum value".to_string()),
563                        span_to_source_span(span),
564                    )]
565                }
566                ValidationError::InvalidRegexPattern { span, .. } => {
567                    vec![LabeledSpan::new_with_span(
568                        Some("invalid regex pattern".to_string()),
569                        span_to_source_span(span),
570                    )]
571                }
572                ValidationError::ImpossibleQuery { span, .. } => {
573                    vec![LabeledSpan::new_with_span(
574                        Some("contradictory condition".to_string()),
575                        span_to_source_span(span),
576                    )]
577                }
578                ValidationError::FieldNotAvailable { span, .. } => {
579                    vec![LabeledSpan::new_with_span(
580                        Some("field not available".to_string()),
581                        span_to_source_span(span),
582                    )]
583                }
584                ValidationError::UnsafeFuzzyCorrection {
585                    span, suggestion, ..
586                } => {
587                    vec![LabeledSpan::new_with_span(
588                        Some(format!(
589                            "requires exact match, did you mean '{suggestion}'?"
590                        )),
591                        span_to_source_span(span),
592                    )]
593                }
594                ValidationError::SubqueryDepthExceeded {
595                    depth,
596                    max_depth,
597                    span,
598                } => {
599                    vec![LabeledSpan::new_with_span(
600                        Some(format!(
601                            "nesting depth {depth} exceeds limit of {max_depth}"
602                        )),
603                        span_to_source_span(span),
604                    )]
605                }
606            },
607            QueryError::Execution(_) => vec![],
608        };
609
610        if labels.is_empty() {
611            None
612        } else {
613            Some(Box::new(labels.into_iter()))
614        }
615    }
616
617    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
618        let help_text = match &self.error {
619            QueryError::Lex(e) => match e {
620                LexError::UnterminatedString { .. } => {
621                    Some("String literals must be closed with a matching quote")
622                }
623                LexError::UnterminatedRegex { .. } => {
624                    Some("Regex literals must be closed with a '/' character")
625                }
626                LexError::InvalidEscape { .. } => {
627                    Some("Valid escape sequences are: \\n \\r \\t \\\\ \\\" \\' \\u{XXXX}")
628                }
629                LexError::InvalidRegex { .. } => {
630                    Some("Check the regex syntax. Use /pattern/ or /pattern/flags format")
631                }
632                _ => None,
633            },
634            QueryError::Parse(e) => match e {
635                ParseError::UnmatchedParen { .. } => {
636                    Some("Add a closing ')' or remove the opening '('")
637                }
638                ParseError::ExpectedIdentifier { .. } => {
639                    Some("Valid fields include: kind, name, path, lang, text, async, etc.")
640                }
641                ParseError::ExpectedOperator { .. } => {
642                    Some("Valid operators: : (equals), ~= (regex), >, <, >=, <=")
643                }
644                ParseError::EmptyQuery => Some("Provide a query expression like 'kind:function'"),
645                _ => None,
646            },
647            QueryError::Validation(e) => match e {
648                ValidationError::UnknownField { .. } => {
649                    Some("Use 'sqry query --help' to see available fields")
650                }
651                ValidationError::InvalidOperator {
652                    valid_operators, ..
653                } => {
654                    let ops: Vec<String> =
655                        valid_operators.iter().map(|op| format!("'{op}'")).collect();
656                    return Some(Box::new(format!(
657                        "Valid operators for this field: {}",
658                        ops.join(", ")
659                    )) as Box<dyn std::fmt::Display>);
660                }
661                ValidationError::InvalidEnumValue { valid_values, .. } => {
662                    return Some(
663                        Box::new(format!("Valid values: {}", valid_values.join(", ")))
664                            as Box<dyn std::fmt::Display>,
665                    );
666                }
667                _ => None,
668            },
669            QueryError::Execution(_) => None,
670        };
671
672        help_text.map(|s| Box::new(s.to_string()) as Box<dyn std::fmt::Display>)
673    }
674}
675
676/// Convert Span to miette's `SourceSpan`
677fn span_to_source_span(span: &Span) -> miette::SourceSpan {
678    miette::SourceSpan::new(span.start.into(), span.end - span.start)
679}
680
681/// Lexical analysis errors
682#[derive(Debug, Clone, Error)]
683pub enum LexError {
684    /// Unterminated string literal
685    #[error("Unterminated string literal at position {}", span.start)]
686    UnterminatedString {
687        /// Location of the unterminated string
688        span: Span,
689    },
690
691    /// Unterminated regex literal
692    #[error("Unterminated regex literal at position {}", span.start)]
693    UnterminatedRegex {
694        /// Location of the unterminated regex
695        span: Span,
696    },
697
698    /// Invalid escape sequence
699    #[error("Invalid escape sequence '\\{char}' at position {}", span.start)]
700    InvalidEscape {
701        /// The invalid escape character
702        char: char,
703        /// Location of the invalid escape
704        span: Span,
705    },
706
707    /// Invalid Unicode escape sequence
708    #[error("Invalid Unicode escape sequence at position {}: expected hex digit, got '{got}'", span.start)]
709    InvalidUnicodeEscape {
710        /// The character encountered instead of a hex digit
711        got: char,
712        /// Location of the invalid escape
713        span: Span,
714    },
715
716    /// Invalid number format
717    #[error("Invalid number '{text}' at position {}", span.start)]
718    InvalidNumber {
719        /// The invalid number text
720        text: String,
721        /// Location of the invalid number
722        span: Span,
723    },
724
725    /// Number overflow
726    #[error("Number overflow '{text}' at position {}: {error}", span.start)]
727    NumberOverflow {
728        /// The number text that overflowed
729        text: String,
730        /// The parse error message
731        error: String,
732        /// Location of the number
733        span: Span,
734    },
735
736    /// Invalid regex pattern
737    #[error("Invalid regex pattern '{pattern}' at position {}: {error}", span.start)]
738    InvalidRegex {
739        /// The invalid regex pattern
740        pattern: String,
741        /// The regex compilation error
742        error: String,
743        /// Location of the regex
744        span: Span,
745    },
746
747    /// Unexpected character
748    #[error("Unexpected character '{char}' at position {}", span.start)]
749    UnexpectedChar {
750        /// The unexpected character
751        char: char,
752        /// Location of the character
753        span: Span,
754    },
755
756    /// Unexpected end of input
757    #[error("Unexpected end of input")]
758    UnexpectedEof,
759}
760
761/// Parsing errors
762#[derive(Debug, Clone, Error)]
763pub enum ParseError {
764    /// Empty query string
765    #[error("Query cannot be empty")]
766    EmptyQuery,
767
768    /// Expected an identifier (field name)
769    #[error("Expected field name")]
770    ExpectedIdentifier {
771        /// The unexpected token
772        token: crate::query::lexer::Token,
773    },
774
775    /// Expected an operator
776    #[error("Expected operator")]
777    ExpectedOperator {
778        /// The unexpected token
779        token: crate::query::lexer::Token,
780    },
781
782    /// Expected a value
783    #[error("Expected value")]
784    ExpectedValue {
785        /// The unexpected token
786        token: crate::query::lexer::Token,
787    },
788
789    /// Unmatched opening parenthesis
790    #[error("Unmatched opening parenthesis at position {}", open_span.start)]
791    UnmatchedParen {
792        /// Location of the unmatched parenthesis
793        open_span: Span,
794        /// Whether we reached EOF
795        eof: bool,
796    },
797
798    /// Unexpected token
799    #[error("Expected {expected}")]
800    UnexpectedToken {
801        /// The unexpected token
802        token: crate::query::lexer::Token,
803        /// What was expected
804        expected: String,
805    },
806
807    /// Invalid syntax
808    #[error("Invalid syntax at position {}: {message}", span.start)]
809    InvalidSyntax {
810        /// Description of the syntax error
811        message: String,
812        /// Location of the syntax error
813        span: Span,
814    },
815
816    /// Unexpected end of file
817    #[error("Unexpected end of input, expected {expected}")]
818    UnexpectedEof {
819        /// What was expected
820        expected: String,
821    },
822}
823
824/// Validation errors (semantic checking)
825#[derive(Debug, Clone, Error)]
826pub enum ValidationError {
827    /// Unknown field name
828    #[error("Unknown field '{field}' at position {}{}",
829        span.start,
830        suggestion.as_ref().map(|s| format!(". Did you mean '{s}'?")).unwrap_or_default()
831    )]
832    UnknownField {
833        /// The unknown field name
834        field: String,
835        /// Suggested field name (if any)
836        suggestion: Option<String>,
837        /// Location of the field reference
838        span: Span,
839    },
840
841    /// Invalid operator for field type
842    #[error(
843        "Operator '{}' not supported for field '{}' at position {}. Valid operators: {}",
844        operator,
845        field,
846        span.start,
847        format_operators(valid_operators)
848    )]
849    InvalidOperator {
850        /// The field name
851        field: String,
852        /// The invalid operator
853        operator: Operator,
854        /// Valid operators for this field
855        valid_operators: Vec<Operator>,
856        /// Location of the operator
857        span: Span,
858    },
859
860    /// Type mismatch between field and value
861    #[error(
862        "Type mismatch for field '{}' at position {}: expected {}, got {:?}",
863        field,
864        span.start,
865        format_field_type(expected),
866        got
867    )]
868    TypeMismatch {
869        /// The field name
870        field: String,
871        /// Expected field type
872        expected: FieldType,
873        /// Actual value provided
874        got: Value,
875        /// Location of the value
876        span: Span,
877    },
878
879    /// Invalid enum value
880    #[error(
881        "Invalid value '{}' for field '{}' at position {}. Valid values: {}",
882        value,
883        field,
884        span.start,
885        valid_values.join(", ")
886    )]
887    InvalidEnumValue {
888        /// The field name
889        field: String,
890        /// The invalid value
891        value: String,
892        /// Valid enum values
893        valid_values: Vec<&'static str>,
894        /// Location of the value
895        span: Span,
896    },
897
898    /// Invalid regex pattern
899    #[error("Invalid regex pattern '{}' at position {}: {}", pattern, span.start, error)]
900    InvalidRegexPattern {
901        /// The invalid regex pattern
902        pattern: String,
903        /// The regex compilation error
904        error: String,
905        /// Location of the regex
906        span: Span,
907    },
908
909    /// Impossible query (contradictory conditions)
910    #[error("Impossible query at position {}: {}", span.start, message)]
911    ImpossibleQuery {
912        /// Description of the contradiction
913        message: String,
914        /// Location of the contradiction
915        span: Span,
916    },
917
918    /// Field not available (requires plugin or feature)
919    #[error("Field '{}' is not available at position {}: {}", field, span.start, reason)]
920    FieldNotAvailable {
921        /// The unavailable field name
922        field: String,
923        /// Reason why it's not available
924        reason: String,
925        /// Location of the field reference
926        span: Span,
927    },
928
929    /// Fuzzy correction found but field requires exact match
930    #[error(
931        "Field '{input}' is not recognized at position {}. Did you mean '{suggestion}'? This field requires an exact match.",
932        span.start
933    )]
934    UnsafeFuzzyCorrection {
935        /// The field name the user typed
936        input: String,
937        /// The suggested correct field name
938        suggestion: String,
939        /// Location of the field reference
940        span: Span,
941    },
942
943    /// Subquery nesting depth exceeded
944    #[error(
945        "Subquery nesting depth {depth} exceeds maximum of {max_depth} at position {}",
946        span.start
947    )]
948    SubqueryDepthExceeded {
949        /// Current nesting depth
950        depth: usize,
951        /// Maximum allowed depth
952        max_depth: usize,
953        /// Location of the offending subquery
954        span: Span,
955    },
956}
957
958/// Execution errors (runtime errors)
959#[derive(Debug, Error)]
960pub enum ExecutionError {
961    /// Index not found
962    #[error("Index not found at path: {}", path.display())]
963    IndexNotFound {
964        /// Path where index was expected
965        path: PathBuf,
966    },
967
968    /// Relation queries require an index with relation data
969    #[error("Relation queries require an index. Run `sqry index` for {path}")]
970    RelationQueryRequiresIndex {
971        /// Path that was queried without an index
972        path: PathBuf,
973    },
974
975    /// Relation queries need relation data, but the loaded index lacks it
976    #[error(
977        "Legacy index at {path} (version {index_version}) lacks relation data. Rebuild with `sqry index --force {path}` to enable relation queries."
978    )]
979    LegacyIndexMissingRelations {
980        /// Path to the workspace whose index is missing relations
981        path: PathBuf,
982        /// Version string recorded in the legacy index metadata
983        index_version: String,
984    },
985
986    /// CD predicates require index schema version >= 2 with `body_hash` support
987    #[error(
988        "CD predicates require index schema version {required_version}+, but index at {path} has version {current_version}. Rebuild with `sqry index --force {path}` to enable CD predicates."
989    )]
990    IndexMissingCDSupport {
991        /// Path to the workspace whose index lacks CD support
992        path: PathBuf,
993        /// Current index schema version
994        current_version: u32,
995        /// Required minimum version for CD predicates
996        required_version: u32,
997    },
998
999    /// Plugin not found
1000    #[error("Language plugin not found for: {language}")]
1001    PluginNotFound {
1002        /// Language identifier
1003        language: String,
1004    },
1005
1006    /// Field evaluation failed
1007    #[error("Failed to evaluate field '{field}': {error}")]
1008    FieldEvaluationFailed {
1009        /// The field that failed to evaluate
1010        field: String,
1011        /// The error message
1012        error: String,
1013    },
1014
1015    /// Type mismatch during execution
1016    #[error("Type mismatch: expected {expected}, got {got}")]
1017    TypeMismatch {
1018        /// Expected type
1019        expected: &'static str,
1020        /// Actual type
1021        got: String,
1022    },
1023
1024    /// Invalid regex
1025    #[error("Invalid regex pattern '{pattern}': {error}")]
1026    InvalidRegex {
1027        /// The invalid regex pattern
1028        pattern: String,
1029        /// The regex compilation error
1030        error: String,
1031    },
1032
1033    /// Invalid glob pattern
1034    #[error("Invalid glob pattern '{pattern}': {error}")]
1035    InvalidGlob {
1036        /// The invalid glob pattern
1037        pattern: String,
1038        /// The glob compilation error
1039        error: String,
1040    },
1041
1042    /// Too many glob matches
1043    #[error("Glob pattern '{pattern}' matched too many files (limit: {limit})")]
1044    TooManyMatches {
1045        /// The glob pattern
1046        pattern: String,
1047        /// The match limit
1048        limit: usize,
1049    },
1050
1051    /// File read error
1052    #[error("Failed to read file: {0}")]
1053    FileReadError(#[from] std::io::Error),
1054
1055    /// Regex error
1056    #[error("Regex error: {0}")]
1057    RegexError(#[from] regex::Error),
1058
1059    /// Query timeout
1060    #[error("Query execution timed out after {seconds} seconds")]
1061    Timeout {
1062        /// Timeout duration in seconds
1063        seconds: u64,
1064    },
1065
1066    /// Query was cancelled by the client (LSP $/cancelRequest)
1067    #[error("Query execution was cancelled")]
1068    Cancelled,
1069
1070    /// Index version mismatch
1071    #[error("Index version mismatch: expected {expected}, found {found}")]
1072    IndexVersionMismatch {
1073        /// Expected index version
1074        expected: String,
1075        /// Found index version
1076        found: String,
1077    },
1078}
1079
1080/// Format a list of operators for display
1081fn format_operators(operators: &[Operator]) -> String {
1082    operators
1083        .iter()
1084        .map(|op| format!("'{op}'"))
1085        .collect::<Vec<_>>()
1086        .join(", ")
1087}
1088
1089/// Format a field type for display
1090fn format_field_type(field_type: &FieldType) -> String {
1091    match field_type {
1092        FieldType::String => "string".to_string(),
1093        FieldType::Bool => "boolean".to_string(),
1094        FieldType::Number => "number".to_string(),
1095        FieldType::Enum(values) => format!("enum ({})", values.join(", ")),
1096        FieldType::Path => "path".to_string(),
1097    }
1098}
1099
1100#[cfg(test)]
1101mod tests {
1102    use super::*;
1103
1104    // ===== QueryError tests =====
1105
1106    #[test]
1107    fn test_query_error_exit_code_syntax() {
1108        let lex_err = QueryError::Lex(LexError::UnexpectedEof);
1109        assert_eq!(lex_err.exit_code(), 2);
1110    }
1111
1112    #[test]
1113    fn test_query_error_exit_code_parse() {
1114        let parse_err = QueryError::Parse(ParseError::EmptyQuery);
1115        assert_eq!(parse_err.exit_code(), 2);
1116    }
1117
1118    #[test]
1119    fn test_query_error_exit_code_validation() {
1120        let val_err = QueryError::Validation(ValidationError::UnknownField {
1121            field: "test".to_string(),
1122            suggestion: None,
1123            span: Span::new(0, 4),
1124        });
1125        assert_eq!(val_err.exit_code(), 2);
1126    }
1127
1128    #[test]
1129    fn test_query_error_exit_code_execution() {
1130        let exec_err = QueryError::Execution(ExecutionError::Cancelled);
1131        assert_eq!(exec_err.exit_code(), 1);
1132    }
1133
1134    #[test]
1135    fn test_query_error_with_source() {
1136        let err = QueryError::Lex(LexError::UnexpectedEof);
1137        let rich = err.with_source("kind:function");
1138        assert_eq!(rich.exit_code(), 2);
1139    }
1140
1141    // ===== RichQueryError tests =====
1142
1143    #[test]
1144    fn test_rich_query_error_new() {
1145        let err = QueryError::Parse(ParseError::EmptyQuery);
1146        let rich = RichQueryError::new(err, "");
1147        assert!(matches!(rich.inner(), QueryError::Parse(_)));
1148    }
1149
1150    #[test]
1151    fn test_rich_query_error_to_json_lex() {
1152        let err = QueryError::Lex(LexError::UnterminatedString {
1153            span: Span::new(0, 5),
1154        });
1155        let rich = RichQueryError::new(err, "\"hello");
1156        let json = rich.to_json_value();
1157
1158        assert_eq!(json["error"]["code"], "sqry::syntax");
1159        assert!(
1160            json["error"]["message"]
1161                .as_str()
1162                .unwrap()
1163                .contains("Unterminated")
1164        );
1165        assert_eq!(json["error"]["query"], "\"hello");
1166        assert!(json["error"]["span"].is_object());
1167    }
1168
1169    #[test]
1170    fn test_rich_query_error_to_json_parse() {
1171        let err = QueryError::Parse(ParseError::EmptyQuery);
1172        let rich = RichQueryError::new(err, "");
1173        let json = rich.to_json_value();
1174
1175        assert_eq!(json["error"]["code"], "sqry::parse");
1176    }
1177
1178    #[test]
1179    fn test_rich_query_error_to_json_validation() {
1180        let err = QueryError::Validation(ValidationError::UnknownField {
1181            field: "knd".to_string(),
1182            suggestion: Some("kind".to_string()),
1183            span: Span::new(0, 3),
1184        });
1185        let rich = RichQueryError::new(err, "knd:function");
1186        let json = rich.to_json_value();
1187
1188        assert_eq!(json["error"]["code"], "sqry::validation");
1189        assert!(json["error"]["suggestion"].is_string());
1190    }
1191
1192    #[test]
1193    fn test_rich_query_error_to_json_execution() {
1194        let err = QueryError::Execution(ExecutionError::Cancelled);
1195        let rich = RichQueryError::new(err, "kind:function");
1196        let json = rich.to_json_value();
1197
1198        assert_eq!(json["error"]["code"], "sqry::execution");
1199    }
1200
1201    // ===== LexError tests =====
1202
1203    #[test]
1204    fn test_lex_error_display() {
1205        let err = LexError::UnterminatedString {
1206            span: Span::new(10, 15),
1207        };
1208        let msg = err.to_string();
1209        assert!(msg.contains("Unterminated string"));
1210        assert!(msg.contains("10"));
1211    }
1212
1213    #[test]
1214    fn test_lex_error_invalid_escape() {
1215        let err = LexError::InvalidEscape {
1216            char: 'x',
1217            span: Span::new(5, 6),
1218        };
1219        let msg = err.to_string();
1220        assert!(msg.contains("Invalid escape"));
1221        assert!(msg.contains("\\x"));
1222        assert!(msg.contains('5'));
1223    }
1224
1225    #[test]
1226    fn test_lex_error_unterminated_regex() {
1227        let err = LexError::UnterminatedRegex {
1228            span: Span::new(0, 5),
1229        };
1230        let msg = err.to_string();
1231        assert!(msg.contains("Unterminated regex"));
1232    }
1233
1234    #[test]
1235    fn test_lex_error_invalid_unicode_escape() {
1236        let err = LexError::InvalidUnicodeEscape {
1237            got: 'z',
1238            span: Span::new(0, 6),
1239        };
1240        let msg = err.to_string();
1241        assert!(msg.contains("Invalid Unicode escape"));
1242        assert!(msg.contains("'z'"));
1243    }
1244
1245    #[test]
1246    fn test_lex_error_invalid_number() {
1247        let err = LexError::InvalidNumber {
1248            text: "123abc".to_string(),
1249            span: Span::new(0, 6),
1250        };
1251        let msg = err.to_string();
1252        assert!(msg.contains("Invalid number"));
1253        assert!(msg.contains("123abc"));
1254    }
1255
1256    #[test]
1257    fn test_lex_error_number_overflow() {
1258        let err = LexError::NumberOverflow {
1259            text: "99999999999999999999".to_string(),
1260            error: "too large".to_string(),
1261            span: Span::new(0, 20),
1262        };
1263        let msg = err.to_string();
1264        assert!(msg.contains("Number overflow"));
1265        assert!(msg.contains("too large"));
1266    }
1267
1268    #[test]
1269    fn test_lex_error_invalid_regex() {
1270        let err = LexError::InvalidRegex {
1271            pattern: "[unclosed".to_string(),
1272            error: "bracket not closed".to_string(),
1273            span: Span::new(0, 9),
1274        };
1275        let msg = err.to_string();
1276        assert!(msg.contains("Invalid regex"));
1277        assert!(msg.contains("[unclosed"));
1278    }
1279
1280    #[test]
1281    fn test_lex_error_unexpected_char() {
1282        let err = LexError::UnexpectedChar {
1283            char: '@',
1284            span: Span::new(5, 6),
1285        };
1286        let msg = err.to_string();
1287        assert!(msg.contains("Unexpected character"));
1288        assert!(msg.contains("'@'"));
1289    }
1290
1291    #[test]
1292    fn test_lex_error_unexpected_eof() {
1293        let err = LexError::UnexpectedEof;
1294        let msg = err.to_string();
1295        assert!(msg.contains("Unexpected end of input"));
1296    }
1297
1298    // ===== ParseError tests =====
1299
1300    #[test]
1301    fn test_parse_error_expected_identifier() {
1302        use crate::query::lexer::{Token, TokenType};
1303        let err = ParseError::ExpectedIdentifier {
1304            token: Token::new(TokenType::NumberLiteral(123), Span::new(0, 3)),
1305        };
1306        let msg = err.to_string();
1307        assert!(msg.contains("Expected field name"));
1308    }
1309
1310    #[test]
1311    fn test_validation_error_unknown_field() {
1312        let err = ValidationError::UnknownField {
1313            field: "knd".to_string(),
1314            suggestion: Some("kind".to_string()),
1315            span: Span::new(0, 3),
1316        };
1317        let msg = err.to_string();
1318        assert!(msg.contains("Unknown field 'knd'"));
1319        assert!(msg.contains("Did you mean 'kind'?"));
1320    }
1321
1322    #[test]
1323    fn test_validation_error_unknown_field_no_suggestion() {
1324        let err = ValidationError::UnknownField {
1325            field: "xyz".to_string(),
1326            suggestion: None,
1327            span: Span::new(0, 3),
1328        };
1329        let msg = err.to_string();
1330        assert!(msg.contains("Unknown field 'xyz'"));
1331        assert!(!msg.contains("Did you mean"));
1332    }
1333
1334    #[test]
1335    fn test_validation_error_invalid_operator() {
1336        let err = ValidationError::InvalidOperator {
1337            field: "kind".to_string(),
1338            operator: Operator::Greater,
1339            valid_operators: vec![Operator::Equal, Operator::Regex],
1340            span: Span::new(5, 6),
1341        };
1342        let msg = err.to_string();
1343        assert!(msg.contains("Operator '>'"));
1344        assert!(msg.contains("kind"));
1345        assert!(msg.contains("Valid operators"));
1346        assert!(msg.contains("':'"));
1347        assert!(msg.contains("'~='"));
1348    }
1349
1350    #[test]
1351    fn test_validation_error_type_mismatch() {
1352        let err = ValidationError::TypeMismatch {
1353            field: "async".to_string(),
1354            expected: FieldType::Bool,
1355            got: Value::Number(42),
1356            span: Span::new(10, 12),
1357        };
1358        let msg = err.to_string();
1359        assert!(msg.contains("Type mismatch"));
1360        assert!(msg.contains("async"));
1361        assert!(msg.contains("boolean"));
1362        assert!(msg.contains("Number"));
1363    }
1364
1365    #[test]
1366    fn test_validation_error_invalid_enum_value() {
1367        let err = ValidationError::InvalidEnumValue {
1368            field: "kind".to_string(),
1369            value: "invalid".to_string(),
1370            valid_values: vec!["function", "class", "method"],
1371            span: Span::new(5, 12),
1372        };
1373        let msg = err.to_string();
1374        assert!(msg.contains("Invalid value 'invalid'"));
1375        assert!(msg.contains("kind"));
1376        assert!(msg.contains("function, class, method"));
1377    }
1378
1379    #[test]
1380    fn test_execution_error_type_mismatch() {
1381        let err = ExecutionError::TypeMismatch {
1382            expected: "string",
1383            got: "Number(42)".to_string(),
1384        };
1385        let msg = err.to_string();
1386        assert!(msg.contains("Type mismatch"));
1387        assert!(msg.contains("expected string"));
1388        assert!(msg.contains("got Number(42)"));
1389    }
1390
1391    #[test]
1392    fn test_execution_error_invalid_glob() {
1393        let err = ExecutionError::InvalidGlob {
1394            pattern: "[invalid".to_string(),
1395            error: "unclosed character class".to_string(),
1396        };
1397        let msg = err.to_string();
1398        assert!(msg.contains("Invalid glob pattern"));
1399        assert!(msg.contains("[invalid"));
1400        assert!(msg.contains("unclosed"));
1401    }
1402
1403    #[test]
1404    fn test_execution_error_too_many_matches() {
1405        let err = ExecutionError::TooManyMatches {
1406            pattern: "**/*.rs".to_string(),
1407            limit: 10000,
1408        };
1409        let msg = err.to_string();
1410        assert!(msg.contains("too many files"));
1411        assert!(msg.contains("**/*.rs"));
1412        assert!(msg.contains("10000"));
1413    }
1414
1415    #[test]
1416    fn test_execution_error_index_not_found() {
1417        let err = ExecutionError::IndexNotFound {
1418            path: PathBuf::from("/path/to/project"),
1419        };
1420        let msg = err.to_string();
1421        assert!(msg.contains("Index not found"));
1422        assert!(msg.contains("/path/to/project"));
1423    }
1424
1425    #[test]
1426    fn test_execution_error_relation_query_requires_index() {
1427        let err = ExecutionError::RelationQueryRequiresIndex {
1428            path: PathBuf::from("/project"),
1429        };
1430        let msg = err.to_string();
1431        assert!(msg.contains("Relation queries require an index"));
1432    }
1433
1434    #[test]
1435    fn test_execution_error_legacy_index_missing_relations() {
1436        let err = ExecutionError::LegacyIndexMissingRelations {
1437            path: PathBuf::from("/project"),
1438            index_version: "1.0.0".to_string(),
1439        };
1440        let msg = err.to_string();
1441        assert!(msg.contains("Legacy index"));
1442        assert!(msg.contains("lacks relation data"));
1443    }
1444
1445    #[test]
1446    fn test_execution_error_index_missing_cd_support() {
1447        let err = ExecutionError::IndexMissingCDSupport {
1448            path: PathBuf::from("/project"),
1449            current_version: 1,
1450            required_version: 2,
1451        };
1452        let msg = err.to_string();
1453        assert!(msg.contains("CD predicates require"));
1454    }
1455
1456    #[test]
1457    fn test_execution_error_plugin_not_found() {
1458        let err = ExecutionError::PluginNotFound {
1459            language: "Brainfuck".to_string(),
1460        };
1461        let msg = err.to_string();
1462        assert!(msg.contains("Language plugin not found"));
1463        assert!(msg.contains("Brainfuck"));
1464    }
1465
1466    #[test]
1467    fn test_execution_error_field_evaluation_failed() {
1468        let err = ExecutionError::FieldEvaluationFailed {
1469            field: "custom_field".to_string(),
1470            error: "not implemented".to_string(),
1471        };
1472        let msg = err.to_string();
1473        assert!(msg.contains("Failed to evaluate field"));
1474        assert!(msg.contains("custom_field"));
1475    }
1476
1477    #[test]
1478    fn test_execution_error_invalid_regex() {
1479        let err = ExecutionError::InvalidRegex {
1480            pattern: "(unclosed".to_string(),
1481            error: "unclosed group".to_string(),
1482        };
1483        let msg = err.to_string();
1484        assert!(msg.contains("Invalid regex"));
1485        assert!(msg.contains("(unclosed"));
1486    }
1487
1488    #[test]
1489    fn test_execution_error_timeout() {
1490        let err = ExecutionError::Timeout { seconds: 30 };
1491        let msg = err.to_string();
1492        assert!(msg.contains("timed out"));
1493        assert!(msg.contains("30"));
1494    }
1495
1496    #[test]
1497    fn test_execution_error_cancelled() {
1498        let err = ExecutionError::Cancelled;
1499        let msg = err.to_string();
1500        assert!(msg.contains("cancelled"));
1501    }
1502
1503    #[test]
1504    fn test_execution_error_index_version_mismatch() {
1505        let err = ExecutionError::IndexVersionMismatch {
1506            expected: "2.0".to_string(),
1507            found: "1.0".to_string(),
1508        };
1509        let msg = err.to_string();
1510        assert!(msg.contains("version mismatch"));
1511        assert!(msg.contains("expected 2.0"));
1512        assert!(msg.contains("found 1.0"));
1513    }
1514
1515    // ===== Diagnostic impl tests =====
1516
1517    #[test]
1518    fn test_diagnostic_code() {
1519        use miette::Diagnostic;
1520
1521        let lex_rich = RichQueryError::new(QueryError::Lex(LexError::UnexpectedEof), "test");
1522        assert!(lex_rich.code().unwrap().to_string().contains("syntax"));
1523
1524        let parse_rich = RichQueryError::new(QueryError::Parse(ParseError::EmptyQuery), "");
1525        assert!(parse_rich.code().unwrap().to_string().contains("parse"));
1526    }
1527
1528    #[test]
1529    fn test_query_error_wrapping() {
1530        let lex_err = LexError::UnexpectedEof;
1531        let query_err = QueryError::from(lex_err);
1532        let msg = query_err.to_string();
1533        assert!(msg.contains("Syntax error"));
1534    }
1535
1536    #[test]
1537    fn test_format_operators() {
1538        let ops = vec![Operator::Equal, Operator::Regex, Operator::Greater];
1539        let formatted = format_operators(&ops);
1540        assert_eq!(formatted, "':', '~=', '>'");
1541    }
1542
1543    #[test]
1544    fn test_format_field_type() {
1545        assert_eq!(format_field_type(&FieldType::String), "string");
1546        assert_eq!(format_field_type(&FieldType::Bool), "boolean");
1547        assert_eq!(format_field_type(&FieldType::Number), "number");
1548        assert_eq!(format_field_type(&FieldType::Path), "path");
1549
1550        let enum_type = FieldType::Enum(vec!["function", "class"]);
1551        assert_eq!(format_field_type(&enum_type), "enum (function, class)");
1552    }
1553}