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