1use 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#[derive(Debug, Error)]
15pub enum QueryError {
16 #[error("Syntax error: {0}")]
18 Lex(#[from] LexError),
19
20 #[error("Parse error: {0}")]
22 Parse(#[from] ParseError),
23
24 #[error("Validation error: {0}")]
26 Validation(#[from] ValidationError),
27
28 #[error("Execution error: {0}")]
30 Execution(#[from] ExecutionError),
31
32 #[error("query cancelled (cooperative)")]
45 Cancelled,
46}
47
48impl QueryError {
49 #[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 pub fn with_source(self, source: impl Into<String>) -> RichQueryError {
64 RichQueryError {
65 error: self,
66 source: source.into(),
67 }
68 }
69}
70
71#[derive(Debug, Error)]
76#[error("{error}")]
77pub struct RichQueryError {
78 #[source]
79 error: QueryError,
80 source: String,
81}
82
83impl RichQueryError {
84 pub fn new(error: QueryError, source: impl Into<String>) -> Self {
86 Self {
87 error,
88 source: source.into(),
89 }
90 }
91
92 #[must_use]
94 pub fn inner(&self) -> &QueryError {
95 &self.error
96 }
97
98 #[must_use]
100 pub fn exit_code(&self) -> i32 {
101 self.error.exit_code()
102 }
103
104 #[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 let (span, label, suggestion) = self.extract_span_and_label();
129
130 let help = self.build_help_text();
132
133 let mut json = serde_json::json!({
135 "error": {
136 "code": code,
137 "message": message,
138 "query": query,
139 }
140 });
141
142 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 #[allow(clippy::too_many_lines)] 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 #[allow(clippy::too_many_lines)] 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)] 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
699fn span_to_source_span(span: &Span) -> miette::SourceSpan {
701 miette::SourceSpan::new(span.start.into(), span.end - span.start)
702}
703
704#[derive(Debug, Clone, Error)]
706pub enum LexError {
707 #[error("Unterminated string literal at position {}", span.start)]
709 UnterminatedString {
710 span: Span,
712 },
713
714 #[error("Unterminated regex literal at position {}", span.start)]
716 UnterminatedRegex {
717 span: Span,
719 },
720
721 #[error("Invalid escape sequence '\\{char}' at position {}", span.start)]
723 InvalidEscape {
724 char: char,
726 span: Span,
728 },
729
730 #[error("Invalid Unicode escape sequence at position {}: expected hex digit, got '{got}'", span.start)]
732 InvalidUnicodeEscape {
733 got: char,
735 span: Span,
737 },
738
739 #[error("Invalid number '{text}' at position {}", span.start)]
741 InvalidNumber {
742 text: String,
744 span: Span,
746 },
747
748 #[error("Number overflow '{text}' at position {}: {error}", span.start)]
750 NumberOverflow {
751 text: String,
753 error: String,
755 span: Span,
757 },
758
759 #[error("Invalid regex pattern '{pattern}' at position {}: {error}", span.start)]
761 InvalidRegex {
762 pattern: String,
764 error: String,
766 span: Span,
768 },
769
770 #[error("Unexpected character '{char}' at position {}", span.start)]
772 UnexpectedChar {
773 char: char,
775 span: Span,
777 },
778
779 #[error("Unexpected end of input")]
781 UnexpectedEof,
782}
783
784#[derive(Debug, Clone, Error)]
786pub enum ParseError {
787 #[error("Query cannot be empty")]
789 EmptyQuery,
790
791 #[error("Expected field name")]
793 ExpectedIdentifier {
794 token: crate::query::lexer::Token,
796 },
797
798 #[error("Expected operator")]
800 ExpectedOperator {
801 token: crate::query::lexer::Token,
803 },
804
805 #[error("Expected value")]
807 ExpectedValue {
808 token: crate::query::lexer::Token,
810 },
811
812 #[error("Unmatched opening parenthesis at position {}", open_span.start)]
814 UnmatchedParen {
815 open_span: Span,
817 eof: bool,
819 },
820
821 #[error("Expected {expected}")]
823 UnexpectedToken {
824 token: crate::query::lexer::Token,
826 expected: String,
828 },
829
830 #[error("Invalid syntax at position {}: {message}", span.start)]
832 InvalidSyntax {
833 message: String,
835 span: Span,
837 },
838
839 #[error("Unexpected end of input, expected {expected}")]
841 UnexpectedEof {
842 expected: String,
844 },
845}
846
847#[derive(Debug, Clone, Error)]
849pub enum ValidationError {
850 #[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 field: String,
858 suggestion: Option<String>,
860 span: Span,
862 },
863
864 #[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 field: String,
875 operator: Operator,
877 valid_operators: Vec<Operator>,
879 span: Span,
881 },
882
883 #[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 field: String,
894 expected: FieldType,
896 got: Value,
898 span: Span,
900 },
901
902 #[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 field: String,
913 value: String,
915 valid_values: Vec<&'static str>,
917 span: Span,
919 },
920
921 #[error("Invalid regex pattern '{}' at position {}: {}", pattern, span.start, error)]
923 InvalidRegexPattern {
924 pattern: String,
926 error: String,
928 span: Span,
930 },
931
932 #[error("Impossible query at position {}: {}", span.start, message)]
934 ImpossibleQuery {
935 message: String,
937 span: Span,
939 },
940
941 #[error("Field '{}' is not available at position {}: {}", field, span.start, reason)]
943 FieldNotAvailable {
944 field: String,
946 reason: String,
948 span: Span,
950 },
951
952 #[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 input: String,
960 suggestion: String,
962 span: Span,
964 },
965
966 #[error(
968 "Subquery nesting depth {depth} exceeds maximum of {max_depth} at position {}",
969 span.start
970 )]
971 SubqueryDepthExceeded {
972 depth: usize,
974 max_depth: usize,
976 span: Span,
978 },
979}
980
981#[derive(Debug, Error)]
983pub enum ExecutionError {
984 #[error("Index not found at path: {}", path.display())]
986 IndexNotFound {
987 path: PathBuf,
989 },
990
991 #[error("Relation queries require an index. Run `sqry index` for {path}")]
993 RelationQueryRequiresIndex {
994 path: PathBuf,
996 },
997
998 #[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: PathBuf,
1005 index_version: String,
1007 },
1008
1009 #[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: PathBuf,
1016 current_version: u32,
1018 required_version: u32,
1020 },
1021
1022 #[error("Language plugin not found for: {language}")]
1024 PluginNotFound {
1025 language: String,
1027 },
1028
1029 #[error("Failed to evaluate field '{field}': {error}")]
1031 FieldEvaluationFailed {
1032 field: String,
1034 error: String,
1036 },
1037
1038 #[error("Type mismatch: expected {expected}, got {got}")]
1040 TypeMismatch {
1041 expected: &'static str,
1043 got: String,
1045 },
1046
1047 #[error("Invalid regex pattern '{pattern}': {error}")]
1049 InvalidRegex {
1050 pattern: String,
1052 error: String,
1054 },
1055
1056 #[error("Invalid glob pattern '{pattern}': {error}")]
1058 InvalidGlob {
1059 pattern: String,
1061 error: String,
1063 },
1064
1065 #[error("Glob pattern '{pattern}' matched too many files (limit: {limit})")]
1067 TooManyMatches {
1068 pattern: String,
1070 limit: usize,
1072 },
1073
1074 #[error("Failed to read file: {0}")]
1076 FileReadError(#[from] std::io::Error),
1077
1078 #[error("Regex error: {0}")]
1080 RegexError(#[from] regex::Error),
1081
1082 #[error("Query execution timed out after {seconds} seconds")]
1084 Timeout {
1085 seconds: u64,
1087 },
1088
1089 #[error("Query execution was cancelled")]
1091 Cancelled,
1092
1093 #[error("Index version mismatch: expected {expected}, found {found}")]
1095 IndexVersionMismatch {
1096 expected: String,
1098 found: String,
1100 },
1101}
1102
1103fn format_operators(operators: &[Operator]) -> String {
1105 operators
1106 .iter()
1107 .map(|op| format!("'{op}'"))
1108 .collect::<Vec<_>>()
1109 .join(", ")
1110}
1111
1112fn 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 #[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 #[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 #[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 #[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 #[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}