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