1use serde::Serialize;
16
17use crate::ast::{
18 AlertAction, AlertRule, AnalysisTarget, AnalysisVerb, BinOp, CollectionTarget, ComposeTarget,
19 ConfigTarget, Duration, DurationUnit, EventsQuery, Expression, InspectQuery, LogsQuery,
20 Operator, PipelineNode, Query, SetValue, SingularTarget, SingularTargetKind, SortDirection,
21 TimeSelector, Value,
22};
23use crate::{ANSI_BOLD, ANSI_FG_CYAN, ANSI_FG_GREEN, ANSI_FG_RED, ANSI_FG_YELLOW, ANSI_RESET};
24
25#[derive(Debug, Clone, PartialEq, Serialize)]
26pub struct ParsedQuery {
27 pub source: String,
28 pub query: Query,
29}
30
31#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct ParseError {
33 pub column: usize,
34 pub message: String,
35 pub source: Option<String>,
37 pub suggestion: Option<String>,
39}
40
41impl std::error::Error for ParseError {}
42
43impl std::fmt::Display for ParseError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "parse error at column {}: {}", self.column, self.message)?;
48
49 self.fmt_source_pointer(f)?;
50 self.fmt_suggestion_plain(f)?;
51
52 Ok(())
53 }
54}
55
56impl ParseError {
57 pub fn new(column: usize, message: impl Into<String>) -> Self {
58 Self {
59 column,
60 message: message.into(),
61 source: None,
62 suggestion: None,
63 }
64 }
65
66 #[must_use]
67 pub fn with_source(mut self, source: String) -> Self {
68 self.source = Some(source);
69 self
70 }
71
72 #[must_use]
74 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
75 self.suggestion = Some(suggestion.into());
76 self
77 }
78
79 #[must_use]
81 pub fn format_color(&self) -> String {
82 let mut out = String::new();
83
84 use std::fmt::Write;
86 write!(
87 out,
88 "{ansi_bold}{ansi_red}error{reset} at column {col}: {msg}",
89 ansi_bold = ANSI_BOLD,
90 ansi_red = ANSI_FG_RED,
91 reset = ANSI_RESET,
92 col = self.column,
93 msg = self.message,
94 )
95 .expect("write! to String is infallible");
96
97 if let Some(ref source) = self.source {
99 let mut remaining = self.column;
100 let display_line = source
101 .lines()
102 .find(|line| {
103 let line_len = line.chars().count();
104 if remaining <= line_len || line_len == 0 && remaining == 1 {
105 true
106 } else {
107 remaining = remaining.saturating_sub(line_len + 1);
108 false
109 }
110 })
111 .unwrap_or(source);
112
113 let col = remaining.min(display_line.chars().count());
114 let pointer = format!("{}^", " ".repeat(col.saturating_sub(1)));
115
116 write!(
117 out,
118 "\n {bold}{cyan}-->{reset} {line}",
119 bold = ANSI_BOLD,
120 cyan = ANSI_FG_CYAN,
121 reset = ANSI_RESET,
122 line = display_line,
123 )
124 .expect("write! to String is infallible");
125 write!(
126 out,
127 "\n {green}{ptr}{reset}",
128 green = ANSI_FG_GREEN,
129 ptr = pointer,
130 reset = ANSI_RESET,
131 )
132 .expect("write! to String is infallible");
133 }
134
135 if let Some(ref suggestion) = self.suggestion {
137 write!(
138 out,
139 "\n {yellow}help: {suggestion}{reset}",
140 yellow = ANSI_FG_YELLOW,
141 reset = ANSI_RESET,
142 )
143 .expect("write! to String is infallible");
144 }
145
146 out
147 }
148
149 fn fmt_source_pointer(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 if let Some(ref source) = self.source {
152 let mut remaining = self.column;
153 let display_line = source
154 .lines()
155 .find(|line| {
156 let line_len = line.chars().count();
157 if remaining <= line_len || line_len == 0 && remaining == 1 {
158 true
159 } else {
160 remaining = remaining.saturating_sub(line_len + 1);
161 false
162 }
163 })
164 .unwrap_or(source);
165
166 let col = remaining.min(display_line.chars().count());
167 write!(f, "\n --> {display_line}")?;
168 write!(f, "\n {}^", " ".repeat(col.saturating_sub(1)))?;
169 }
170 Ok(())
171 }
172
173 fn fmt_suggestion_plain(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 if let Some(ref suggestion) = self.suggestion {
176 write!(f, "\n help: {suggestion}")?;
177 }
178 Ok(())
179 }
180}
181
182pub fn parse(source: &str) -> Result<ParsedQuery, ParseError> {
183 let trimmed = source.trim();
184
185 let result = (|| {
186 if trimmed.is_empty() {
187 return Err(
188 ParseError::new(1, "empty DOL query").with_suggestion("try `observe containers`")
189 );
190 }
191
192 let tokens = tokenize(trimmed)?;
193 let mut parser = Parser::new(tokens, trimmed.chars().count() + 1);
194 let query = parser.parse_query()?;
195 parser.expect_eof()?;
196
197 Ok(ParsedQuery {
198 source: trimmed.to_owned(),
199 query,
200 })
201 })();
202
203 result.map_err(|e| e.with_source(trimmed.to_owned()))
205}
206
207#[derive(Debug, Clone, PartialEq)]
208struct Token {
209 kind: TokenKind,
210 column: usize,
211}
212
213#[derive(Debug, Clone, PartialEq)]
214enum TokenKind {
215 Ident(String),
216 String(String),
217 Integer(i64),
218 Float(f64),
219 Percentage(f64),
220 Duration(Duration),
221 Eq,
222 NotEq,
223 Gt,
224 Lt,
225 Gte,
226 Lte,
227 Pipe,
228 Comma,
229 LParen,
230 RParen,
231 Plus,
232 Minus,
233 Star,
234 Slash,
235 Percent,
236}
237
238struct Parser {
239 tokens: Vec<Token>,
240 cursor: usize,
241 eof_column: usize,
242}
243
244impl Parser {
245 const fn new(tokens: Vec<Token>, eof_column: usize) -> Self {
246 Self {
247 tokens,
248 cursor: 0,
249 eof_column,
250 }
251 }
252
253 fn parse_query(&mut self) -> Result<Query, ParseError> {
256 match self.peek_ident() {
257 Some("observe") => self.parse_observe(),
258 Some("events") => self.parse_events(),
259 Some("inspect") => self.parse_inspect(),
260 Some("analyze") => self.parse_analyze(),
261 Some("alert") => self.parse_alert_rule().map(Query::Alert),
262 Some("logs") => self.parse_logs(),
263 Some("compose") => self.parse_compose(),
264 Some("ping") => {
265 self.advance();
266 Ok(Query::Ping)
267 }
268 Some("fields") => self.parse_fields(),
269 Some(other) => Err(self
270 .error_here(format!(
271 "expected query family, found `{other}`"
272 ))
273 .with_suggestion("try one of: `observe`, `events`, `inspect`, `analyze`, `alert`, `compose`, `logs`, `ping`".to_string())),
274 None => Err(self.error_here("expected query family")),
275 }
276 }
277
278 fn parse_observe(&mut self) -> Result<Query, ParseError> {
279 self.expect_ident("observe")?;
280 if self.consume_ident("compose") {
282 let project = self.expect_identifier_like("compose project name")?;
283 let target = self.parse_compose_target();
284 let pipeline = self.parse_pipeline()?;
285 return Ok(Query::Compose(crate::ast::ComposeQuery {
286 project,
287 target,
288 pipeline,
289 service: None,
290 port_number: None,
291 tail: None,
292 config_target: None,
293 }));
294 }
295 let target = self.parse_collection_target()?;
296 let time = self.parse_optional_time_selector()?;
297 let filter = self.parse_optional_inline_where()?;
298 let join = if self.consume_ident("join") {
299 let right = self.parse_collection_target()?;
300 self.expect_ident("on")?;
301 let left_key = self.parse_arith_expression()?;
302 self.expect(&TokenKind::Eq, "`=`")?;
303 let right_key = self.parse_arith_expression()?;
304 Some(crate::ast::JoinClause {
305 right,
306 left_key,
307 right_key,
308 })
309 } else {
310 None
311 };
312 let pipeline = self.parse_pipeline()?;
313 Ok(Query::Observe(crate::ast::ObserveQuery {
314 target,
315 time,
316 filter,
317 join,
318 pipeline,
319 }))
320 }
321
322 fn parse_events(&mut self) -> Result<Query, ParseError> {
323 self.expect_ident("events")?;
324 let target = self.parse_collection_target()?;
325 let time = self.parse_optional_time_selector()?;
326 let filter = self.parse_optional_inline_where()?;
327 let pipeline = self.parse_pipeline()?;
328 Ok(Query::Events(EventsQuery {
329 target,
330 time,
331 filter,
332 pipeline,
333 }))
334 }
335
336 fn parse_inspect(&mut self) -> Result<Query, ParseError> {
337 self.expect_ident("inspect")?;
338 let target = self.parse_singular_target()?;
339 let at = if self.consume_ident("at") {
340 Some(self.expect_timestamp_string()?)
341 } else {
342 None
343 };
344 Ok(Query::Inspect(InspectQuery { target, at }))
345 }
346
347 fn parse_analyze(&mut self) -> Result<Query, ParseError> {
348 self.expect_ident("analyze")?;
349 let target = self.parse_analysis_target()?;
350 let verb = self.parse_analysis_verb()?;
351 let subject = if self.is_at_time_or_pipe_or_eof() {
352 None
353 } else {
354 Some(self.expect_identifier_like("analysis subject")?)
355 };
356 let time = self.parse_optional_time_selector()?;
357 let pipeline = self.parse_pipeline()?;
358 Ok(Query::Analyze(crate::ast::AnalyzeQuery {
359 target,
360 verb,
361 subject,
362 time,
363 pipeline,
364 }))
365 }
366
367 fn parse_alert_rule(&mut self) -> Result<AlertRule, ParseError> {
368 self.expect_ident("alert")?;
369 self.expect_ident("when")?;
370 let condition = self.parse_expression()?;
371 let duration = if self.consume_ident("for") {
372 Some(self.expect_duration()?)
373 } else {
374 None
375 };
376 self.expect_ident("then")?;
377 let action = self.parse_alert_action()?;
378 Ok(AlertRule {
379 condition,
380 duration,
381 action,
382 })
383 }
384
385 fn parse_compose(&mut self) -> Result<Query, ParseError> {
386 self.expect_ident("compose")?;
387
388 if self.consume_ident("ls") {
389 let pipeline = self.parse_pipeline()?;
390 return Ok(Query::Compose(crate::ast::ComposeQuery {
391 project: String::new(),
392 target: ComposeTarget::Projects,
393 pipeline,
394 service: None,
395 port_number: None,
396 tail: None,
397 config_target: None,
398 }));
399 }
400
401 let project = self.expect_identifier_like("compose project name")?;
402 let target = self.parse_compose_target();
403
404 let (service, port_number, tail, config_target) = match target {
405 ComposeTarget::Logs => {
406 let svc = self.expect_identifier_like("service name")?;
407 let t = if self.consume_ident("tail") {
408 Some(self.expect_u64("tail")?)
409 } else {
410 None
411 };
412 (Some(svc), None, t, None)
413 }
414 ComposeTarget::Port => {
415 let svc = self.expect_identifier_like("service name")?;
416 let port = Some(self.expect_u64("port number")?);
417 (Some(svc), port, None, None)
418 }
419 ComposeTarget::Config => {
420 let ct = if self.consume_ident("services") {
421 Some(ConfigTarget::Services)
422 } else if self.consume_ident("networks") {
423 Some(ConfigTarget::Networks)
424 } else if self.consume_ident("volumes") {
425 Some(ConfigTarget::Volumes)
426 } else {
427 Some(ConfigTarget::All)
428 };
429 (None, None, None, ct)
430 }
431 _ => (None, None, None, None),
432 };
433
434 let pipeline = self.parse_pipeline()?;
435 Ok(Query::Compose(crate::ast::ComposeQuery {
436 project,
437 target,
438 pipeline,
439 service,
440 port_number,
441 tail,
442 config_target,
443 }))
444 }
445
446 fn parse_compose_target(&mut self) -> ComposeTarget {
447 if self.consume_ident("services") {
448 ComposeTarget::Services
449 } else if self.consume_ident("networks") {
450 ComposeTarget::Networks
451 } else if self.consume_ident("volumes") {
452 ComposeTarget::Volumes
453 } else if self.consume_ident("health") {
454 ComposeTarget::Health
455 } else if self.consume_ident("images") {
456 ComposeTarget::Images
457 } else if self.consume_ident("stats") {
458 ComposeTarget::Stats
459 } else if self.consume_ident("ps") {
460 ComposeTarget::Ps
461 } else if self.consume_ident("events") {
462 ComposeTarget::Events
463 } else if self.consume_ident("port") {
464 ComposeTarget::Port
465 } else if self.consume_ident("config") {
466 ComposeTarget::Config
467 } else if self.consume_ident("logs") {
468 ComposeTarget::Logs
469 } else {
470 self.consume_ident("containers");
471 ComposeTarget::Containers
472 }
473 }
474
475 fn parse_logs(&mut self) -> Result<Query, ParseError> {
476 self.expect_ident("logs")?;
477 self.expect_ident("container")?;
478 let container = self.expect_identifier_like("container name or ID")?;
479 let tail = if self.consume_ident("tail") {
480 Some(self.expect_u64("tail")?)
481 } else {
482 None
483 };
484 let filter = self.parse_optional_inline_where()?;
485 let pipeline = self.parse_pipeline()?;
486 Ok(Query::Logs(LogsQuery {
487 container,
488 tail,
489 filter,
490 pipeline,
491 }))
492 }
493
494 fn parse_fields(&mut self) -> Result<Query, ParseError> {
495 self.expect_ident("fields")?;
496 let target = self.parse_collection_target()?;
497 Ok(Query::Fields(target))
498 }
499
500 fn parse_collection_target(&mut self) -> Result<CollectionTarget, ParseError> {
503 match self.peek_ident() {
504 Some("containers") => {
505 self.advance();
506 Ok(CollectionTarget::Containers)
507 }
508 Some("images") => {
509 self.advance();
510 Ok(CollectionTarget::Images)
511 }
512 Some("networks") => {
513 self.advance();
514 Ok(CollectionTarget::Networks)
515 }
516 Some("volumes") => {
517 self.advance();
518 Ok(CollectionTarget::Volumes)
519 }
520 Some(other) => {
521 let suggestion = suggest_similar_target(other);
522 Err(self
523 .error_here(format!("expected collection target, found `{other}`"))
524 .with_suggestion(suggestion))
525 }
526 None => Err(self.error_here(
527 "expected collection target: containers, images, networks, or volumes",
528 )),
529 }
530 }
531
532 fn parse_singular_target(&mut self) -> Result<SingularTarget, ParseError> {
533 let kind = match self.peek_ident() {
534 Some("container") => {
535 self.advance();
536 SingularTargetKind::Container
537 }
538 Some("image") => {
539 self.advance();
540 SingularTargetKind::Image
541 }
542 Some("network") => {
543 self.advance();
544 SingularTargetKind::Network
545 }
546 Some("volume") => {
547 self.advance();
548 SingularTargetKind::Volume
549 }
550 Some(other) => {
551 let suggestion = suggest_similar_target(other);
552 return Err(self
553 .error_here(format!("expected singular target, found `{other}`"))
554 .with_suggestion(suggestion));
555 }
556 None => {
557 return Err(self
558 .error_here("expected singular target: container, image, network, or volume"));
559 }
560 };
561 let value = self.expect_identifier_like("target name or ID")?;
562 Ok(SingularTarget { kind, value })
563 }
564
565 fn parse_analysis_target(&mut self) -> Result<AnalysisTarget, ParseError> {
566 if let Ok(target) = self.parse_collection_target() {
567 return Ok(AnalysisTarget::Collection(target));
568 }
569 self.parse_singular_target().map(AnalysisTarget::Singular)
570 }
571
572 fn parse_analysis_verb(&mut self) -> Result<AnalysisVerb, ParseError> {
573 match self.peek_ident() {
574 Some("find") => {
575 self.advance();
576 Ok(AnalysisVerb::Find)
577 }
578 Some("correlate") => {
579 self.advance();
580 Ok(AnalysisVerb::Correlate)
581 }
582 Some("explain") => {
583 self.advance();
584 Ok(AnalysisVerb::Explain)
585 }
586 Some(other) => {
587 let extra = suggest_keyword(other, ANALYSIS_VERBS);
588 let base = "try one of: `find`, `correlate`, `explain`";
589 let suggestion = match extra {
590 Some(s) => format!("{s} {base}"),
591 None => base.to_owned(),
592 };
593 Err(self
594 .error_here(format!("expected analysis verb, found `{other}`"))
595 .with_suggestion(suggestion))
596 }
597 None => Err(self.error_here("expected analysis verb: find, correlate, or explain")),
598 }
599 }
600
601 fn parse_alert_action(&mut self) -> Result<AlertAction, ParseError> {
602 match self.peek_ident() {
603 Some("print") => {
604 self.advance();
605 let msg = self.expect_string("alert message")?;
606 Ok(AlertAction::Print(msg))
607 }
608 Some("webhook") => {
609 self.advance();
610 let url = self.expect_string("webhook URL")?;
611 Ok(AlertAction::Webhook(url))
612 }
613 Some("restart") => {
614 self.advance();
615 let target = self.parse_singular_target()?;
616 Ok(AlertAction::Restart(target))
617 }
618 Some("slack") => {
619 self.advance();
620 let url = self.expect_string("Slack webhook URL")?;
621 Ok(AlertAction::Slack(url))
622 }
623 Some("discord") => {
624 self.advance();
625 let url = self.expect_string("Discord webhook URL")?;
626 Ok(AlertAction::Discord(url))
627 }
628 Some("email") => {
629 self.advance();
630 let to = self.expect_string("email recipient")?;
631 let subject = if self.consume_ident("subject") {
632 self.expect_string("email subject")?
633 } else {
634 "DockQL Alert Notification".to_owned()
635 };
636 Ok(AlertAction::Email { to, subject })
637 }
638 Some(other) => {
639 let extra = suggest_keyword(other, ALERT_ACTIONS);
640 let base = "try one of: `print`, `webhook`, `restart`, `slack`, `discord`, `email`";
641 let suggestion = match extra {
642 Some(s) => format!("{s} {base}"),
643 None => base.to_owned(),
644 };
645 Err(self
646 .error_here(format!("expected alert action, found `{other}`"))
647 .with_suggestion(suggestion))
648 }
649 None => Err(self.error_here(
650 "expected alert action: print, webhook, restart, slack, discord, or email",
651 )),
652 }
653 }
654
655 fn parse_optional_time_selector(&mut self) -> Result<Option<TimeSelector>, ParseError> {
658 if self.consume_ident("last") {
659 let duration = self.expect_duration()?;
660 Ok(Some(TimeSelector::Last(duration)))
661 } else if self.consume_ident("from") {
662 let from = self.expect_timestamp_string()?;
663 self.expect_ident("to")?;
664 let to = self.expect_timestamp_string()?;
665 Ok(Some(TimeSelector::Range { from, to }))
666 } else {
667 Ok(None)
668 }
669 }
670
671 fn parse_optional_inline_where(&mut self) -> Result<Option<Expression>, ParseError> {
672 if self.consume_ident("where") {
673 Ok(Some(self.parse_expression()?))
674 } else {
675 Ok(None)
676 }
677 }
678
679 fn parse_pipeline(&mut self) -> Result<Vec<PipelineNode>, ParseError> {
682 let mut nodes = Vec::new();
683 while self.consume(&TokenKind::Pipe) {
684 nodes.push(self.parse_pipeline_node()?);
685 }
686 Ok(nodes)
687 }
688
689 fn parse_pipeline_node(&mut self) -> Result<PipelineNode, ParseError> {
690 if self.consume_ident("where") {
691 return Ok(PipelineNode::Where(self.parse_expression()?));
692 }
693 if self.consume_ident("select") {
694 return Ok(PipelineNode::Select(self.parse_field_list()?));
695 }
696 if self.consume_ident("group") {
697 self.expect_ident("by")?;
698 let fields = self.parse_field_list()?;
699 let aggregates = if self.consume_ident("with") {
700 self.parse_aggregates()?
701 } else {
702 Vec::new()
703 };
704 return Ok(PipelineNode::GroupBy { fields, aggregates });
705 }
706 if self.consume_ident("having") {
707 return Ok(PipelineNode::Having(self.parse_expression()?));
708 }
709 if self.consume_ident("sort") {
710 self.consume_ident("by");
711 let mut fields = Vec::new();
712 loop {
713 let field = self.expect_identifier_like("sort field")?;
714 let direction = if self.consume_ident("desc") {
715 SortDirection::Desc
716 } else {
717 self.consume_ident("asc");
718 SortDirection::Asc
719 };
720 fields.push((field, direction));
721 if !self.consume(&TokenKind::Comma) {
722 break;
723 }
724 }
725 return Ok(PipelineNode::SortBy { fields });
726 }
727 if self.consume_ident("limit") {
728 return Ok(PipelineNode::Limit(self.expect_u64("limit")?));
729 }
730 if self.consume_ident("offset") {
731 return Ok(PipelineNode::Offset(self.expect_u64("offset")?));
732 }
733 if self.consume_ident("distinct") {
734 return Ok(PipelineNode::Distinct);
735 }
736 if self.consume_ident("alert") {
737 return Ok(PipelineNode::Alert(self.expect_string("alert message")?));
738 }
739 if self.consume_ident("if") {
740 return self.parse_if_pipeline();
741 }
742 if self.consume_ident("set") {
743 return self.parse_set_pipeline();
744 }
745 if self.consume_ident("debug") {
746 return Ok(PipelineNode::Debug);
747 }
748 if self.consume_ident("assert") {
749 return Ok(PipelineNode::Assert(self.parse_expression()?));
750 }
751 if self.consume_ident("fill") {
752 return self.parse_fill_pipeline();
753 }
754 if self.consume_ident("row_number") {
755 return self.parse_row_number_pipeline();
756 }
757 if self.consume_ident("rank") {
758 return self.parse_window_func_with_field("rank");
759 }
760 if self.consume_ident("lag") {
761 return self.parse_window_func_with_field("lag");
762 }
763 if self.consume_ident("lead") {
764 return self.parse_window_func_with_field("lead");
765 }
766 if self.consume_ident("let") {
767 return self.parse_let_pipeline();
768 }
769 Err({
770 let extra = self.peek().and_then(|t| match &t.kind {
771 TokenKind::Ident(name) => suggest_keyword(name, PIPELINE_KEYWORDS),
772 _ => None,
773 });
774 let base = "use `| where`, `| select`, `| sort`, `| group by`, `| limit`, `| set`, `| if`, `| fill`, `| let`, `| row_number`, `| rank`, `| lag`, `| lead`, `| debug`, or `| assert`";
775 let suggestion = match extra {
776 Some(s) => format!("{s} {base}"),
777 None => base.to_owned(),
778 };
779 self.error_here("expected pipeline node")
780 .with_suggestion(suggestion)
781 })
782 }
783
784 fn parse_aggregate_arg(&mut self) -> Result<String, ParseError> {
786 let token = self
787 .advance()
788 .ok_or_else(|| self.error_here("expected aggregate argument"))?;
789 match token.kind {
790 TokenKind::Ident(s) | TokenKind::String(s) => Ok(s),
791 TokenKind::Integer(n) => Ok(n.to_string()),
792 TokenKind::Float(f) => Ok(f.to_string()),
793 _ => Err(ParseError::new(
794 token.column,
795 "expected aggregate argument (identifier, number, or string)",
796 )),
797 }
798 }
799
800 fn parse_aggregates(&mut self) -> Result<Vec<crate::ast::AggregateExpr>, ParseError> {
801 let mut aggs = Vec::new();
802 loop {
803 let func = self.expect_identifier_like("aggregate function")?;
804 self.expect(&TokenKind::LParen, "`(`")?;
805 let field = self.expect_identifier_like("aggregate field")?;
806 let mut args = Vec::new();
808 if self.consume(&TokenKind::Comma) {
809 let arg = self.parse_aggregate_arg()?;
810 args.push(arg);
811 while self.consume(&TokenKind::Comma) {
812 let arg = self.parse_aggregate_arg()?;
813 args.push(arg);
814 }
815 }
816 self.expect(&TokenKind::RParen, "`)`")?;
817 let alias = if self.consume_ident("as") {
818 self.expect_identifier_like("alias")?
819 } else {
820 field.clone()
821 };
822 aggs.push(crate::ast::AggregateExpr {
823 function: func,
824 field,
825 alias,
826 args,
827 });
828 if !self.consume(&TokenKind::Comma) {
829 break;
830 }
831 }
832 Ok(aggs)
833 }
834
835 fn parse_if_pipeline(&mut self) -> Result<PipelineNode, ParseError> {
836 let condition = self.parse_expression()?;
837 self.expect_ident("then")?;
838 let then_branch = self.parse_inline_pipeline_nodes()?;
839 let else_branch = if self.consume_ident("else") {
840 if self.consume_ident("if") {
841 Some(vec![self.parse_if_pipeline()?])
842 } else {
843 Some(self.parse_inline_pipeline_nodes()?)
844 }
845 } else {
846 None
847 };
848 Ok(PipelineNode::If {
849 condition,
850 then_branch,
851 else_branch,
852 })
853 }
854
855 fn parse_inline_pipeline_nodes(&mut self) -> Result<Vec<PipelineNode>, ParseError> {
856 let mut nodes = vec![self.parse_pipeline_node()?];
857 while self.consume(&TokenKind::Pipe) {
858 nodes.push(self.parse_pipeline_node()?);
859 }
860 Ok(nodes)
861 }
862
863 fn parse_set_pipeline(&mut self) -> Result<PipelineNode, ParseError> {
864 let field = self.expect_identifier_like("field name")?;
865 self.expect(&TokenKind::Eq, "`=`")?;
866 let value = if self.consume_ident("case") {
867 self.parse_set_case()?
868 } else if self.consume_ident("if") {
869 self.parse_set_if_else()?
870 } else {
871 SetValue::Expr(self.parse_expression()?)
872 };
873 Ok(PipelineNode::Set { field, value })
874 }
875
876 fn parse_set_case(&mut self) -> Result<SetValue, ParseError> {
877 let mut when_clauses = Vec::new();
878 while self.consume_ident("when") {
879 let condition = self.parse_expression()?;
880 self.expect_ident("then")?;
881 let result = self.parse_expression()?;
882 when_clauses.push((condition, result));
883 }
884 let else_value = if self.consume_ident("else") {
885 Some(self.parse_expression()?)
886 } else {
887 None
888 };
889 self.expect_ident("end")?;
890 Ok(SetValue::Case {
891 when_clauses,
892 else_value,
893 })
894 }
895
896 fn parse_set_if_else(&mut self) -> Result<SetValue, ParseError> {
897 let condition = self.parse_expression()?;
898 self.expect_ident("then")?;
899 let then_value = self.parse_expression()?;
900 let else_value = if self.consume_ident("else") {
901 Some(self.parse_expression()?)
902 } else {
903 None
904 };
905 Ok(SetValue::IfElse {
906 condition,
907 then_value,
908 else_value,
909 })
910 }
911
912 fn parse_field_list(&mut self) -> Result<Vec<String>, ParseError> {
913 let mut fields = vec![self.expect_identifier_like("field")?];
914 while self.consume(&TokenKind::Comma) {
915 fields.push(self.expect_identifier_like("field")?);
916 }
917 Ok(fields)
918 }
919
920 fn parse_expression(&mut self) -> Result<Expression, ParseError> {
923 self.parse_or_expression()
924 }
925
926 fn parse_or_expression(&mut self) -> Result<Expression, ParseError> {
927 let mut expr = self.parse_and_expression()?;
928 while self.consume_ident("or") {
929 let rhs = self.parse_and_expression()?;
930 expr = Expression::Or(Box::new(expr), Box::new(rhs));
931 }
932 Ok(expr)
933 }
934
935 fn parse_and_expression(&mut self) -> Result<Expression, ParseError> {
936 let mut expr = self.parse_not_expression()?;
937 while self.consume_ident("and") {
938 let rhs = self.parse_not_expression()?;
939 expr = Expression::And(Box::new(expr), Box::new(rhs));
940 }
941 Ok(expr)
942 }
943
944 fn parse_not_expression(&mut self) -> Result<Expression, ParseError> {
945 if self.consume_ident("not") {
946 return Ok(Expression::Not(Box::new(self.parse_not_expression()?)));
947 }
948 self.parse_comparison_expression()
949 }
950
951 fn parse_comparison_expression(&mut self) -> Result<Expression, ParseError> {
952 let left = self.parse_arith_expression()?;
953
954 if self.consume_ident("in") {
956 self.expect(&TokenKind::LParen, "`(`")?;
957 let values = self.parse_value_list()?;
958 self.expect(&TokenKind::RParen, "`)`")?;
959 return Ok(Expression::In {
960 expr: Box::new(left),
961 values,
962 });
963 }
964
965 if self.consume_ident("is") {
967 if self.consume_ident("not") {
968 self.expect_ident("null")?;
969 return Ok(Expression::IsNotNull(Box::new(left)));
970 }
971 self.expect_ident("null")?;
972 return Ok(Expression::IsNull(Box::new(left)));
973 }
974
975 if self.consume_ident("between") {
977 let low = self.parse_arith_expression()?;
978 self.expect_ident("and")?;
979 let high = self.parse_arith_expression()?;
980 return Ok(Expression::Between {
981 expr: Box::new(left),
982 low: Box::new(low),
983 high: Box::new(high),
984 });
985 }
986
987 if let Some(op) = self.parse_optional_operator() {
989 let right = self.parse_rhs_expression()?;
990 return Ok(Expression::Comparison {
991 left: Box::new(left),
992 operator: op,
993 right: Box::new(right),
994 });
995 }
996
997 Ok(left)
998 }
999
1000 fn parse_rhs_expression(&mut self) -> Result<Expression, ParseError> {
1003 if let Ok(value) = self.try_parse_rhs_value() {
1005 return Ok(Expression::Literal(value));
1006 }
1007 self.parse_arith_expression()
1009 }
1010
1011 fn try_parse_rhs_value(&mut self) -> Result<Value, ParseError> {
1014 let token = self
1015 .peek()
1016 .ok_or_else(|| self.error_here("expected value"))?
1017 .clone();
1018 match &token.kind {
1019 TokenKind::String(_)
1020 | TokenKind::Ident(_)
1021 | TokenKind::Integer(_)
1022 | TokenKind::Float(_)
1023 | TokenKind::Percentage(_) => {}
1024 _ => return Err(ParseError::new(token.column, "not a value")),
1025 }
1026 self.advance();
1027 match token.kind {
1028 TokenKind::String(value) => Ok(Value::String(value)),
1029 TokenKind::Ident(value) if value == "true" => Ok(Value::Boolean(true)),
1030 TokenKind::Ident(value) if value == "false" => Ok(Value::Boolean(false)),
1031 TokenKind::Ident(value) if value.starts_with('$') => {
1032 Ok(Value::Identifier(value[1..].to_owned()))
1033 }
1034 TokenKind::Ident(value) => Ok(Value::Identifier(value)),
1035 TokenKind::Integer(value) => Ok(Value::Integer(value)),
1036 TokenKind::Float(value) => Ok(Value::Float(value)),
1037 TokenKind::Percentage(value) => Ok(Value::Percentage(value)),
1038 _ => unreachable!(),
1039 }
1040 }
1041
1042 fn parse_arith_expression(&mut self) -> Result<Expression, ParseError> {
1043 self.parse_term(0)
1044 }
1045
1046 fn parse_term(&mut self, min_prec: u8) -> Result<Expression, ParseError> {
1048 let mut left = self.parse_unary_arith()?;
1049
1050 loop {
1051 let op = match self.peek().map(|t| &t.kind) {
1052 Some(TokenKind::Plus) if 1 >= min_prec => {
1053 self.advance();
1054 BinOp::Add
1055 }
1056 Some(TokenKind::Minus) if 1 >= min_prec => {
1057 self.advance();
1058 BinOp::Sub
1059 }
1060 Some(TokenKind::Star) if 2 >= min_prec => {
1061 self.advance();
1062 BinOp::Mul
1063 }
1064 Some(TokenKind::Slash) if 2 >= min_prec => {
1065 self.advance();
1066 BinOp::Div
1067 }
1068 Some(TokenKind::Percent) if 2 >= min_prec => {
1069 self.advance();
1070 BinOp::Mod
1071 }
1072 _ => break,
1073 };
1074 let next_prec = match op {
1075 BinOp::Add | BinOp::Sub => 1,
1076 BinOp::Mul | BinOp::Div | BinOp::Mod => 2,
1077 };
1078 let right = self.parse_term(next_prec)?;
1079 left = Expression::Arithmetic {
1080 left: Box::new(left),
1081 op,
1082 right: Box::new(right),
1083 };
1084 }
1085
1086 Ok(left)
1087 }
1088
1089 fn parse_unary_arith(&mut self) -> Result<Expression, ParseError> {
1090 if self.consume(&TokenKind::Minus) {
1091 let expr = self.parse_unary_arith()?;
1092 return Ok(Expression::Arithmetic {
1093 left: Box::new(Expression::Literal(Value::Integer(0))),
1094 op: BinOp::Sub,
1095 right: Box::new(expr),
1096 });
1097 }
1098 self.parse_primary_expression()
1099 }
1100
1101 fn parse_primary_expression(&mut self) -> Result<Expression, ParseError> {
1102 if self.consume(&TokenKind::LParen) {
1104 let expr = self.parse_expression()?;
1105 self.expect(&TokenKind::RParen, "`)`")?;
1106 return Ok(expr);
1107 }
1108
1109 if let Some(Token {
1111 kind: TokenKind::Ident(name),
1112 ..
1113 }) = self.peek()
1114 && let Some(peek2) = self.tokens.get(self.cursor + 1)
1115 && matches!(peek2.kind, TokenKind::LParen)
1116 {
1117 let name = name.clone();
1118 self.advance(); self.advance(); let mut args = Vec::new();
1121 if !self.check(&TokenKind::RParen) {
1122 args.push(self.parse_expression()?);
1123 while self.consume(&TokenKind::Comma) {
1124 args.push(self.parse_expression()?);
1125 }
1126 }
1127 self.expect(&TokenKind::RParen, "`)`")?;
1128 return Ok(Expression::FnCall { name, args });
1129 }
1130
1131 if let Ok(value) = self.try_parse_value() {
1133 return Ok(Expression::Literal(value));
1134 }
1135
1136 let field = self.expect_identifier_like("field")?;
1138 #[allow(clippy::option_if_let_else)]
1139 if let Some(name) = field.strip_prefix('$') {
1140 Ok(Expression::Field(name.to_owned()))
1141 } else {
1142 Ok(Expression::Field(field))
1143 }
1144 }
1145
1146 fn parse_value_list(&mut self) -> Result<Vec<Value>, ParseError> {
1147 let mut values = vec![self.parse_value()?];
1148 while self.consume(&TokenKind::Comma) {
1149 values.push(self.parse_value()?);
1150 }
1151 Ok(values)
1152 }
1153
1154 fn parse_fill_pipeline(&mut self) -> Result<PipelineNode, ParseError> {
1155 let field = self.expect_identifier_like("field name")?;
1156 self.expect_ident("with")?;
1157 let default = if self.consume_ident("case") {
1158 self.parse_set_case()?
1159 } else if self.consume_ident("if") {
1160 self.parse_set_if_else()?
1161 } else {
1162 crate::ast::SetValue::Expr(self.parse_expression()?)
1163 };
1164 let condition = if self.consume_ident("where") {
1165 Some(self.parse_expression()?)
1166 } else {
1167 None
1168 };
1169 Ok(PipelineNode::Fill {
1170 field,
1171 default,
1172 condition,
1173 })
1174 }
1175
1176 fn parse_row_number_pipeline(&mut self) -> Result<PipelineNode, ParseError> {
1177 self.consume_ident("as");
1178 let alias = self.expect_identifier_like("alias for row number")?;
1179 Ok(PipelineNode::RowNumber { alias })
1180 }
1181
1182 fn parse_window_func_with_field(
1183 &mut self,
1184 func_name: &str,
1185 ) -> Result<PipelineNode, ParseError> {
1186 self.expect(&TokenKind::LParen, "`(`")?;
1187 let field = self.expect_identifier_like("field name")?;
1188 let mut offset = 1u64;
1189 if self.consume(&TokenKind::Comma) {
1190 offset = self.expect_u64("offset")?;
1191 }
1192 self.expect(&TokenKind::RParen, "`)`")?;
1193 self.consume_ident("as");
1194 let alias = self.expect_identifier_like("alias")?;
1195 match func_name {
1196 "rank" => Ok(PipelineNode::Rank { field, alias }),
1197 "lag" => Ok(PipelineNode::Lag {
1198 field,
1199 alias,
1200 offset,
1201 }),
1202 "lead" => Ok(PipelineNode::Lead {
1203 field,
1204 alias,
1205 offset,
1206 }),
1207 _ => unreachable!(),
1208 }
1209 }
1210
1211 fn parse_let_pipeline(&mut self) -> Result<PipelineNode, ParseError> {
1212 let name = self.expect_identifier_like("variable name")?;
1213 #[allow(clippy::option_if_let_else)]
1214 let name = if let Some(stripped) = name.strip_prefix('$') {
1215 stripped.to_owned()
1216 } else {
1217 name
1218 };
1219 self.expect(&TokenKind::Eq, "`=`")?;
1220 let value = self.parse_expression()?;
1221 Ok(PipelineNode::Let { name, value })
1222 }
1223
1224 fn parse_optional_operator(&mut self) -> Option<Operator> {
1225 let op = match self.peek().map(|t| &t.kind) {
1226 Some(TokenKind::Eq) => Operator::Eq,
1227 Some(TokenKind::NotEq) => Operator::NotEq,
1228 Some(TokenKind::Gt) => Operator::Gt,
1229 Some(TokenKind::Lt) => Operator::Lt,
1230 Some(TokenKind::Gte) => Operator::Gte,
1231 Some(TokenKind::Lte) => Operator::Lte,
1232 Some(TokenKind::Ident(v)) if v == "contains" => Operator::Contains,
1233 Some(TokenKind::Ident(v)) if v == "matches" => Operator::Matches,
1234 Some(TokenKind::Ident(v)) if v == "starts_with" => Operator::StartsWith,
1235 Some(TokenKind::Ident(v)) if v == "ends_with" => Operator::EndsWith,
1236 _ => return None,
1237 };
1238 self.advance();
1239 Some(op)
1240 }
1241
1242 fn try_parse_value(&mut self) -> Result<Value, ParseError> {
1243 let token = self
1244 .peek()
1245 .ok_or_else(|| self.error_here("expected value"))?
1246 .clone();
1247 match &token.kind {
1248 TokenKind::Ident(v) if v == "true" || v == "false" => {}
1249 TokenKind::String(_)
1250 | TokenKind::Integer(_)
1251 | TokenKind::Float(_)
1252 | TokenKind::Percentage(_) => {}
1253 _ => return Err(ParseError::new(token.column, "not a value")),
1254 }
1255 self.advance();
1256 match token.kind {
1257 TokenKind::String(value) => Ok(Value::String(value)),
1258 TokenKind::Ident(value) if value == "true" => Ok(Value::Boolean(true)),
1259 TokenKind::Ident(value) if value == "false" => Ok(Value::Boolean(false)),
1260 TokenKind::Ident(value) if value.starts_with('$') => {
1261 Ok(Value::Identifier(value[1..].to_owned()))
1262 }
1263 TokenKind::Ident(value) => Ok(Value::Identifier(value)),
1264 TokenKind::Integer(value) => Ok(Value::Integer(value)),
1265 TokenKind::Float(value) => Ok(Value::Float(value)),
1266 TokenKind::Percentage(value) => Ok(Value::Percentage(value)),
1267 _ => unreachable!(),
1268 }
1269 }
1270
1271 fn parse_value(&mut self) -> Result<Value, ParseError> {
1272 let token = self
1273 .advance()
1274 .ok_or_else(|| self.error_here("expected value"))?;
1275 match token.kind {
1276 TokenKind::String(value) => Ok(Value::String(value)),
1277 TokenKind::Ident(value) if value == "true" => Ok(Value::Boolean(true)),
1278 TokenKind::Ident(value) if value == "false" => Ok(Value::Boolean(false)),
1279 TokenKind::Ident(value) => Ok(Value::Identifier(value)),
1280 TokenKind::Integer(value) => Ok(Value::Integer(value)),
1281 TokenKind::Float(value) => Ok(Value::Float(value)),
1282 TokenKind::Percentage(value) => Ok(Value::Percentage(value)),
1283 _ => Err(ParseError::new(token.column, "expected value")),
1284 }
1285 }
1286
1287 fn expect_duration(&mut self) -> Result<Duration, ParseError> {
1288 let token = self
1289 .advance()
1290 .ok_or_else(|| self.error_here("expected duration such as `5m`"))?;
1291 match token.kind {
1292 TokenKind::Duration(d) => Ok(d),
1293 _ => Err(ParseError::new(
1294 token.column,
1295 "expected duration such as `5m`",
1296 )),
1297 }
1298 }
1299
1300 fn expect_u64(&mut self, context: &str) -> Result<u64, ParseError> {
1301 let token = self
1302 .advance()
1303 .ok_or_else(|| self.error_here(format!("expected integer for {context}")))?;
1304 match token.kind {
1305 TokenKind::Integer(n) if n >= 0 => Ok(n as u64),
1306 TokenKind::Integer(_) => Err(ParseError::new(
1307 token.column,
1308 format!("{context} must be non-negative"),
1309 )),
1310 _ => Err(ParseError::new(
1311 token.column,
1312 format!("expected integer for {context}"),
1313 )),
1314 }
1315 }
1316
1317 fn expect_string(&mut self, context: &str) -> Result<String, ParseError> {
1318 let token = self
1319 .advance()
1320 .ok_or_else(|| self.error_here(format!("expected string for {context}")))?;
1321 match token.kind {
1322 TokenKind::String(s) => Ok(s),
1323 _ => Err(ParseError::new(
1324 token.column,
1325 format!("expected a quoted string for {context}"),
1326 )),
1327 }
1328 }
1329
1330 fn expect_timestamp_string(&mut self) -> Result<String, ParseError> {
1331 let token = self
1332 .advance()
1333 .ok_or_else(|| self.error_here("expected timestamp string"))?;
1334 match token.kind {
1335 TokenKind::String(s) => Ok(s),
1336 _ => Err(ParseError::new(
1337 token.column,
1338 "expected a quoted timestamp string",
1339 ))?,
1340 }
1341 }
1342
1343 fn expect_identifier_like(&mut self, context: &str) -> Result<String, ParseError> {
1344 let token = self
1345 .advance()
1346 .ok_or_else(|| self.error_here(format!("expected {context}")))?;
1347 match token.kind {
1348 TokenKind::Ident(s) | TokenKind::String(s) => Ok(s),
1349 _ => Err(ParseError::new(token.column, format!("expected {context}"))),
1350 }
1351 }
1352
1353 fn expect_ident(&mut self, expected: &str) -> Result<(), ParseError> {
1354 let token = self
1355 .advance()
1356 .ok_or_else(|| self.error_here(format!("expected `{expected}`")))?;
1357 match &token.kind {
1358 TokenKind::Ident(value) if value == expected => Ok(()),
1359 _ => {
1360 let found = token_display(&token);
1361 Err(ParseError::new(
1362 token.column,
1363 format!("expected `{expected}`, found `{found}`"),
1364 ))
1365 }
1366 }
1367 }
1368
1369 fn expect(&mut self, kind: &TokenKind, description: &str) -> Result<(), ParseError> {
1370 let token = self
1371 .advance()
1372 .ok_or_else(|| self.error_here(format!("expected {description}")))?;
1373 if token.kind == *kind {
1374 Ok(())
1375 } else {
1376 let found = token_display(&token);
1377 Err(ParseError::new(
1378 token.column,
1379 format!("expected {description}, found `{found}`"),
1380 ))
1381 }
1382 }
1383
1384 fn expect_eof(&self) -> Result<(), ParseError> {
1385 if let Some(token) = self.peek() {
1386 return Err(
1387 ParseError::new(token.column, "unexpected token after query".to_string())
1388 .with_suggestion(format!(
1389 "remove or check the `{}` at column {}",
1390 token_display(token),
1391 token.column
1392 )),
1393 );
1394 }
1395 Ok(())
1396 }
1397
1398 fn consume_ident(&mut self, expected: &str) -> bool {
1399 match self.peek().map(|t| &t.kind) {
1400 Some(TokenKind::Ident(v)) if v == expected => {
1401 self.advance();
1402 true
1403 }
1404 _ => false,
1405 }
1406 }
1407
1408 fn consume(&mut self, kind: &TokenKind) -> bool {
1409 match self.peek().map(|t| &t.kind) {
1410 Some(k) if *k == *kind => {
1411 self.advance();
1412 true
1413 }
1414 _ => false,
1415 }
1416 }
1417
1418 fn check(&self, kind: &TokenKind) -> bool {
1419 self.peek().map(|t| &t.kind) == Some(kind)
1420 }
1421
1422 fn is_at_time_or_pipe_or_eof(&self) -> bool {
1423 match self.peek().map(|t| &t.kind) {
1424 None | Some(TokenKind::Pipe) => true,
1425 Some(TokenKind::Ident(v)) if v == "last" || v == "from" => true,
1426 _ => false,
1427 }
1428 }
1429
1430 fn peek(&self) -> Option<&Token> {
1431 self.tokens.get(self.cursor)
1432 }
1433
1434 fn peek_ident(&self) -> Option<&str> {
1435 match self.peek()?.kind {
1436 TokenKind::Ident(ref s) => Some(s.as_str()),
1437 _ => None,
1438 }
1439 }
1440
1441 fn advance(&mut self) -> Option<Token> {
1442 let token = self.tokens.get(self.cursor).cloned()?;
1443 self.cursor += 1;
1444 Some(token)
1445 }
1446
1447 fn error_here(&self, message: impl Into<String>) -> ParseError {
1448 let col = self.peek().map_or(self.eof_column, |t| t.column);
1449 ParseError::new(col, message)
1450 }
1451}
1452
1453fn token_display(token: &Token) -> String {
1455 match &token.kind {
1456 TokenKind::Ident(s) => s.clone(),
1457 TokenKind::String(s) => format!("\"{s}\""),
1458 TokenKind::Integer(n) => n.to_string(),
1459 TokenKind::Float(f) => f.to_string(),
1460 TokenKind::Percentage(p) => format!("{p}%"),
1461 TokenKind::Duration(d) => {
1462 let unit = match d.unit {
1463 DurationUnit::Seconds => "s",
1464 DurationUnit::Minutes => "m",
1465 DurationUnit::Hours => "h",
1466 DurationUnit::Days => "d",
1467 };
1468 format!("{}{}", d.value, unit)
1469 }
1470 TokenKind::Pipe => "|".to_owned(),
1471 TokenKind::Comma => ",".to_owned(),
1472 TokenKind::LParen => "(".to_owned(),
1473 TokenKind::RParen => ")".to_owned(),
1474 TokenKind::Plus => "+".to_owned(),
1475 TokenKind::Minus => "-".to_owned(),
1476 TokenKind::Star => "*".to_owned(),
1477 TokenKind::Slash => "/".to_owned(),
1478 TokenKind::Percent => "%".to_owned(),
1479 TokenKind::Eq => "=".to_owned(),
1480 TokenKind::NotEq => "!=".to_owned(),
1481 TokenKind::Gt => ">".to_owned(),
1482 TokenKind::Lt => "<".to_owned(),
1483 TokenKind::Gte => ">=".to_owned(),
1484 TokenKind::Lte => "<=".to_owned(),
1485 }
1486}
1487
1488const PIPELINE_KEYWORDS: &[&str] = &[
1490 "where",
1491 "select",
1492 "group",
1493 "having",
1494 "sort",
1495 "limit",
1496 "offset",
1497 "distinct",
1498 "alert",
1499 "if",
1500 "set",
1501 "fill",
1502 "let",
1503 "row_number",
1504 "rank",
1505 "lag",
1506 "lead",
1507];
1508
1509const ANALYSIS_VERBS: &[&str] = &["find", "correlate", "explain"];
1511
1512const ALERT_ACTIONS: &[&str] = &["print", "webhook", "restart", "slack", "discord", "email"];
1514
1515fn suggest_keyword(input: &str, keywords: &[&str]) -> Option<String> {
1517 if input.is_empty() {
1518 return None;
1519 }
1520 let first_char = &input[..1];
1521 keywords
1522 .iter()
1523 .filter(|k| k.starts_with(first_char))
1524 .min_by_key(|k| str_similarity(input, k))
1525 .map(|suggestion| format!("did you mean `{suggestion}`?"))
1526}
1527
1528fn suggest_similar_target(input: &str) -> String {
1530 let targets = [
1531 "containers",
1532 "images",
1533 "networks",
1534 "volumes",
1535 "container",
1536 "image",
1537 "network",
1538 "volume",
1539 ];
1540 let suggestion = suggest_keyword(input, &targets);
1541 let base = "try one of: containers, images, networks, volumes";
1542 match suggestion {
1543 Some(s) => format!("{s} {base}"),
1544 None => base.to_owned(),
1545 }
1546}
1547
1548fn str_similarity(a: &str, b: &str) -> usize {
1551 strsim::levenshtein(a, b)
1552}
1553
1554fn tokenize(source: &str) -> Result<Vec<Token>, ParseError> {
1557 let mut tokens = Vec::new();
1558 let chars: Vec<char> = source.chars().collect();
1559 let mut i = 0;
1560
1561 while i < chars.len() {
1562 let col = i + 1;
1563
1564 if chars[i].is_whitespace() {
1565 i += 1;
1566 continue;
1567 }
1568
1569 if chars[i] == '#' {
1571 while i < chars.len() && chars[i] != '\n' {
1572 i += 1;
1573 }
1574 continue;
1575 }
1576
1577 match chars[i] {
1579 '|' => {
1580 tokens.push(Token {
1581 kind: TokenKind::Pipe,
1582 column: col,
1583 });
1584 i += 1;
1585 continue;
1586 }
1587 ',' => {
1588 tokens.push(Token {
1589 kind: TokenKind::Comma,
1590 column: col,
1591 });
1592 i += 1;
1593 continue;
1594 }
1595 '(' => {
1596 tokens.push(Token {
1597 kind: TokenKind::LParen,
1598 column: col,
1599 });
1600 i += 1;
1601 continue;
1602 }
1603 ')' => {
1604 tokens.push(Token {
1605 kind: TokenKind::RParen,
1606 column: col,
1607 });
1608 i += 1;
1609 continue;
1610 }
1611 '+' => {
1612 tokens.push(Token {
1613 kind: TokenKind::Plus,
1614 column: col,
1615 });
1616 i += 1;
1617 continue;
1618 }
1619 '*' => {
1620 tokens.push(Token {
1621 kind: TokenKind::Star,
1622 column: col,
1623 });
1624 i += 1;
1625 continue;
1626 }
1627 '/' => {
1628 tokens.push(Token {
1629 kind: TokenKind::Slash,
1630 column: col,
1631 });
1632 i += 1;
1633 continue;
1634 }
1635 '%' => {
1636 tokens.push(Token {
1637 kind: TokenKind::Percent,
1638 column: col,
1639 });
1640 i += 1;
1641 continue;
1642 }
1643 _ => {}
1644 }
1645
1646 if chars[i] == '"' {
1648 let start = i;
1649 i += 1;
1650 let mut s = String::new();
1651 while i < chars.len() && chars[i] != '"' {
1652 if chars[i] == '\\' && i + 1 < chars.len() {
1653 i += 1;
1654 match chars[i] {
1655 '"' => s.push('"'),
1656 '\\' => s.push('\\'),
1657 'n' => s.push('\n'),
1658 't' => s.push('\t'),
1659 c => {
1660 s.push('\\');
1661 s.push(c);
1662 }
1663 }
1664 } else {
1665 s.push(chars[i]);
1666 }
1667 i += 1;
1668 }
1669 if i >= chars.len() {
1670 return Err(ParseError::new(start + 1, "unterminated string")
1671 .with_suggestion("add a closing `\"` at the end of the string"));
1672 }
1673 i += 1; tokens.push(Token {
1675 kind: TokenKind::String(s),
1676 column: col,
1677 });
1678 continue;
1679 }
1680
1681 if chars[i].is_ascii_digit()
1683 || chars[i] == '-' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit()
1684 {
1685 let start = i;
1686 if chars[i] == '-' {
1687 i += 1;
1688 }
1689 while i < chars.len() && chars[i].is_ascii_digit() {
1690 i += 1;
1691 }
1692 let mut is_float = false;
1693 if i < chars.len() && chars[i] == '.' {
1694 is_float = true;
1695 i += 1;
1696 while i < chars.len() && chars[i].is_ascii_digit() {
1697 i += 1;
1698 }
1699 }
1700 let num_str: &str = &source[start..i];
1701
1702 if i < chars.len() && chars[i] == '%' {
1703 i += 1;
1704 let val: f64 = num_str
1705 .parse()
1706 .map_err(|_| ParseError::new(col, "invalid percentage"))?;
1707 tokens.push(Token {
1708 kind: TokenKind::Percentage(val),
1709 column: col,
1710 });
1711 } else if i < chars.len() && matches!(chars[i], 's' | 'm' | 'h' | 'd') {
1712 let unit_char = chars[i];
1713 i += 1;
1714 let val: u64 = num_str
1715 .parse()
1716 .map_err(|_| ParseError::new(col, "invalid duration"))?;
1717 let unit = match unit_char {
1718 's' => DurationUnit::Seconds,
1719 'm' => DurationUnit::Minutes,
1720 'h' => DurationUnit::Hours,
1721 'd' => DurationUnit::Days,
1722 _ => unreachable!(),
1723 };
1724 tokens.push(Token {
1725 kind: TokenKind::Duration(Duration { value: val, unit }),
1726 column: col,
1727 });
1728 } else if is_float {
1729 let val: f64 = num_str
1730 .parse()
1731 .map_err(|_| ParseError::new(col, "invalid float"))?;
1732 tokens.push(Token {
1733 kind: TokenKind::Float(val),
1734 column: col,
1735 });
1736 } else {
1737 let val: i64 = num_str
1738 .parse()
1739 .map_err(|_| ParseError::new(col, "invalid integer"))?;
1740 tokens.push(Token {
1741 kind: TokenKind::Integer(val),
1742 column: col,
1743 });
1744 }
1745 continue;
1746 }
1747
1748 if chars[i] == '>' && i + 1 < chars.len() && chars[i + 1] == '=' {
1750 tokens.push(Token {
1751 kind: TokenKind::Gte,
1752 column: col,
1753 });
1754 i += 2;
1755 continue;
1756 }
1757 if chars[i] == '<' && i + 1 < chars.len() && chars[i + 1] == '=' {
1758 tokens.push(Token {
1759 kind: TokenKind::Lte,
1760 column: col,
1761 });
1762 i += 2;
1763 continue;
1764 }
1765 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '=' {
1766 tokens.push(Token {
1767 kind: TokenKind::NotEq,
1768 column: col,
1769 });
1770 i += 2;
1771 continue;
1772 }
1773 if chars[i] == '=' {
1774 tokens.push(Token {
1775 kind: TokenKind::Eq,
1776 column: col,
1777 });
1778 i += 1;
1779 continue;
1780 }
1781 if chars[i] == '>' {
1782 tokens.push(Token {
1783 kind: TokenKind::Gt,
1784 column: col,
1785 });
1786 i += 1;
1787 continue;
1788 }
1789 if chars[i] == '<' {
1790 tokens.push(Token {
1791 kind: TokenKind::Lt,
1792 column: col,
1793 });
1794 i += 1;
1795 continue;
1796 }
1797 if chars[i] == '-' && i + 1 < chars.len() && chars[i + 1] == '>' {
1798 tokens.push(Token {
1799 kind: TokenKind::Pipe,
1800 column: col,
1801 });
1802 i += 2;
1803 continue;
1804 }
1805
1806 if chars[i] == '-' {
1808 tokens.push(Token {
1809 kind: TokenKind::Minus,
1810 column: col,
1811 });
1812 i += 1;
1813 continue;
1814 }
1815
1816 if chars[i].is_ascii_alphanumeric()
1818 || chars[i] == '_'
1819 || chars[i] == '.'
1820 || chars[i] == ':'
1821 || chars[i] == '@'
1822 || chars[i] == '/'
1823 || chars[i] == '$'
1824 {
1825 let start = i;
1826 while i < chars.len()
1827 && (chars[i].is_ascii_alphanumeric()
1828 || chars[i] == '_'
1829 || chars[i] == '.'
1830 || chars[i] == ':'
1831 || chars[i] == '@'
1832 || chars[i] == '/'
1833 || chars[i] == '-'
1834 || chars[i] == '$')
1835 {
1836 i += 1;
1837 }
1838 tokens.push(Token {
1839 kind: TokenKind::Ident(source[start..i].to_owned()),
1840 column: col,
1841 });
1842 continue;
1843 }
1844
1845 return Err(
1846 ParseError::new(col, format!("unexpected character `{}`", chars[i]))
1847 .with_suggestion("expected letters, numbers, `_`, `.`, `:`, `@`, `/`, `-`, or `$`"),
1848 );
1849 }
1850
1851 Ok(tokens)
1852}
1853
1854#[cfg(test)]
1861mod tests {
1862 use super::*;
1863
1864 fn parse_one(query: &str) -> ParsedQuery {
1865 parse(query).unwrap()
1866 }
1867
1868 #[test]
1869 fn parses_observe_containers() {
1870 let q = parse_one("observe containers");
1871 assert!(matches!(q.query, Query::Observe(_)));
1872 }
1873
1874 #[test]
1875 fn parses_inline_where() {
1876 let q = parse_one("observe containers where status = running");
1877 match q.query {
1878 Query::Observe(ref o) => assert!(o.filter.is_some()),
1879 _ => panic!("expected Observe"),
1880 }
1881 }
1882
1883 #[test]
1884 fn parses_pipe_query() {
1885 let q = parse_one(
1886 "observe containers | where cpu > 80% | select name, image, cpu | sort cpu desc | limit 10",
1887 );
1888 assert!(matches!(q.query, Query::Observe(_)));
1889 }
1890
1891 #[test]
1892 fn parses_fields_containers() {
1893 let q = parse_one("fields containers");
1894 assert!(matches!(
1895 q.query,
1896 Query::Fields(CollectionTarget::Containers)
1897 ));
1898 }
1899
1900 #[test]
1901 fn parses_fields_images() {
1902 let q = parse_one("fields images");
1903 assert!(matches!(q.query, Query::Fields(CollectionTarget::Images)));
1904 }
1905
1906 #[test]
1907 fn parses_events_query() {
1908 let q = parse_one("events containers");
1909 assert!(matches!(q.query, Query::Events(_)));
1910 }
1911
1912 #[test]
1913 fn parses_inspect_at_query() {
1914 let q = parse_one(r#"inspect container api-service at "2026-01-01 12:00:00""#);
1915 assert!(matches!(q.query, Query::Inspect(_)));
1916 }
1917
1918 #[test]
1919 fn parses_alert_query() {
1920 let q = parse_one(r#"alert when cpu > 85% for 2m then print "High CPU""#);
1921 assert!(matches!(q.query, Query::Alert(_)));
1922 }
1923
1924 #[test]
1925 fn parses_alert_slack_action() {
1926 let q =
1927 parse_one(r#"alert when cpu > 80% then slack "https://hooks.slack.com/services/xxx""#);
1928 let Query::Alert(ref rule) = q.query else {
1929 panic!("expected Alert")
1930 };
1931 assert!(matches!(rule.action, AlertAction::Slack(_)));
1932 if let AlertAction::Slack(ref url) = rule.action {
1933 assert_eq!(url, "https://hooks.slack.com/services/xxx");
1934 }
1935 }
1936
1937 #[test]
1938 fn parses_alert_discord_action() {
1939 let q = parse_one(
1940 r#"alert when cpu > 80% then discord "https://discord.com/api/webhooks/xxx""#,
1941 );
1942 let Query::Alert(ref rule) = q.query else {
1943 panic!("expected Alert")
1944 };
1945 assert!(matches!(rule.action, AlertAction::Discord(_)));
1946 if let AlertAction::Discord(ref url) = rule.action {
1947 assert_eq!(url, "https://discord.com/api/webhooks/xxx");
1948 }
1949 }
1950
1951 #[test]
1952 fn parses_alert_email_action() {
1953 let q = parse_one(r#"alert when cpu > 80% then email "admin@example.com""#);
1954 let Query::Alert(ref rule) = q.query else {
1955 panic!("expected Alert")
1956 };
1957 assert!(matches!(rule.action, AlertAction::Email { .. }));
1958 if let AlertAction::Email {
1959 ref to,
1960 ref subject,
1961 } = rule.action
1962 {
1963 assert_eq!(to, "admin@example.com");
1964 assert_eq!(subject, "DockQL Alert Notification");
1965 }
1966 }
1967
1968 #[test]
1969 fn parses_alert_email_with_custom_subject() {
1970 let q = parse_one(
1971 r#"alert when cpu > 80% then email "admin@example.com" subject "High CPU Alert""#,
1972 );
1973 let Query::Alert(ref rule) = q.query else {
1974 panic!("expected Alert")
1975 };
1976 if let AlertAction::Email {
1977 ref to,
1978 ref subject,
1979 } = rule.action
1980 {
1981 assert_eq!(to, "admin@example.com");
1982 assert_eq!(subject, "High CPU Alert");
1983 }
1984 }
1985
1986 #[test]
1987 fn parses_analyze_query() {
1988 let q = parse_one("analyze containers find anomalies");
1989 assert!(matches!(q.query, Query::Analyze(_)));
1990 }
1991
1992 #[test]
1993 fn parses_sort_and_limit() {
1994 let q = parse_one("observe containers | sort by state desc | limit 5");
1995 if let Query::Observe(ref o) = q.query {
1996 assert!(matches!(o.pipeline[0], PipelineNode::SortBy { .. }));
1997 assert!(matches!(o.pipeline[1], PipelineNode::Limit(5)));
1998 } else {
1999 panic!("expected Observe");
2000 }
2001 }
2002
2003 #[test]
2004 fn parses_boolean_precedence() {
2005 let q =
2006 parse_one("observe containers where status = running and (cpu > 80% or memory > 90%)");
2007 let Query::Observe(ref o) = q.query else {
2008 panic!("expected Observe")
2009 };
2010 let filter = o.filter.as_ref().expect("expected filter");
2011 assert!(matches!(filter, Expression::And(_, _)));
2012 }
2013
2014 #[test]
2015 fn parses_inline_where_expression() {
2016 let q = parse_one("observe containers where status = running and cpu > 80%");
2017 let Query::Observe(ref o) = q.query else {
2018 panic!("expected Observe")
2019 };
2020 assert!(matches!(o.filter, Some(Expression::And(_, _))));
2021 }
2022
2023 #[test]
2024 fn parses_if_then_pipeline() {
2025 let q = parse_one(r#"observe containers | if cpu > 90% then alert "Critical CPU""#);
2026 let Query::Observe(ref o) = q.query else {
2027 panic!("expected Observe")
2028 };
2029 assert!(matches!(o.pipeline[0], PipelineNode::If { .. }));
2030 }
2031
2032 #[test]
2033 fn parses_if_then_else_if() {
2034 let q = parse_one(
2035 r#"observe containers | if cpu > 90% then alert "Critical" else if cpu > 70% then alert "Warning""#,
2036 );
2037 let Query::Observe(ref o) = q.query else {
2038 panic!("expected Observe")
2039 };
2040 assert!(matches!(o.pipeline[0], PipelineNode::If { .. }));
2041 }
2042
2043 #[test]
2044 fn parses_set_literal() {
2045 let q = parse_one(r#"observe containers | set tier = "prod""#);
2046 let Query::Observe(ref o) = q.query else {
2047 panic!("expected Observe")
2048 };
2049 assert!(matches!(o.pipeline[0], PipelineNode::Set { .. }));
2050 }
2051
2052 #[test]
2053 fn parses_set_case_when() {
2054 let q = parse_one(
2055 r#"observe containers | set severity = case when cpu > 80% then "critical" else "ok" end"#,
2056 );
2057 let Query::Observe(ref o) = q.query else {
2058 panic!("expected Observe")
2059 };
2060 match &o.pipeline[0] {
2061 PipelineNode::Set { value, .. } => assert!(matches!(value, SetValue::Case { .. })),
2062 _ => panic!("expected Set with Case"),
2063 }
2064 }
2065
2066 #[test]
2067 fn parses_set_if_else() {
2068 let q = parse_one(
2069 r#"observe containers | set health = if state = running then "up" else "down""#,
2070 );
2071 let Query::Observe(ref o) = q.query else {
2072 panic!("expected Observe")
2073 };
2074 match &o.pipeline[0] {
2075 PipelineNode::Set { value, .. } => assert!(matches!(value, SetValue::IfElse { .. })),
2076 _ => panic!("expected Set with IfElse"),
2077 }
2078 }
2079
2080 #[test]
2081 fn parses_in_operator() {
2082 let q = parse_one(r#"observe containers | where image in ("nginx", "redis")"#);
2083 let Query::Observe(ref o) = q.query else {
2084 panic!("expected Observe")
2085 };
2086 assert!(matches!(
2087 o.pipeline[0],
2088 PipelineNode::Where(Expression::In { .. })
2089 ));
2090 }
2091
2092 #[test]
2093 fn rejects_empty_query() {
2094 assert!(parse("").is_err());
2095 assert!(parse(" ").is_err());
2096 }
2097
2098 #[test]
2099 fn reports_position_for_bad_where() {
2100 let err = parse("observe containers where").unwrap_err();
2101 assert!(err.column >= 22);
2102 }
2103
2104 #[test]
2105 fn shows_context_with_pointer() {
2106 let err = parse("observe containers where").unwrap_err();
2108 let msg = err.to_string();
2109 assert!(msg.contains("-->"));
2110 assert!(
2111 msg.contains("^"),
2112 "error display should have a pointer: {msg}"
2113 );
2114 assert!(
2115 msg.contains("observe containers where"),
2116 "error display should show the source query: {msg}"
2117 );
2118 }
2119
2120 #[test]
2121 fn shows_context_with_pointer_at_pipe() {
2122 let err = parse("observe containers | where | sort by cpu").unwrap_err();
2124 let msg = err.to_string();
2125 assert!(msg.contains("-->"));
2126 assert!(
2127 msg.contains("^"),
2128 "error display should have a pointer: {msg}"
2129 );
2130 assert!(
2131 msg.contains("observe containers | where | sort by cpu"),
2132 "error display should show the source query: {msg}"
2133 );
2134 let pointer_pos = msg.find('^').unwrap();
2136 let source_pos = msg.find("where |").unwrap_or(msg.len());
2137 assert!(
2138 pointer_pos > source_pos,
2139 "pointer should be after `where |`: {msg}"
2140 );
2141 }
2142
2143 #[test]
2144 fn all_example_files_parse() {
2145 use std::path::Path;
2146 let examples_dir = Path::new("examples");
2147 if !examples_dir.exists() {
2148 return;
2149 }
2150 let mut count = 0;
2151 for entry in std::fs::read_dir(examples_dir).unwrap() {
2152 let entry = entry.unwrap();
2153 if entry
2154 .path()
2155 .extension()
2156 .map(|e| e == "dol")
2157 .unwrap_or(false)
2158 {
2159 let content = std::fs::read_to_string(entry.path()).unwrap();
2160 let result = parse(&content);
2161 if let Err(ref e) = result {
2162 panic!("Failed to parse {}: {}", entry.path().display(), e);
2163 }
2164 count += 1;
2165 }
2166 }
2167 assert!(count > 0, "no example files found");
2168 }
2169
2170 #[test]
2173 fn parses_arithmetic_expression() {
2174 let q = parse_one(r#"observe containers | set mem_gb = memory / 1073741824"#);
2175 let Query::Observe(ref o) = q.query else {
2176 panic!("expected Observe")
2177 };
2178 match &o.pipeline[0] {
2179 PipelineNode::Set { value, .. } => {
2180 assert!(matches!(
2181 value,
2182 SetValue::Expr(Expression::Arithmetic { op: BinOp::Div, .. })
2183 ));
2184 }
2185 _ => panic!("expected Set with Expr"),
2186 }
2187 }
2188
2189 #[test]
2190 fn parses_between() {
2191 let q = parse_one("observe containers where cpu between 50 and 80");
2192 let Query::Observe(ref o) = q.query else {
2193 panic!("expected Observe")
2194 };
2195 let filter = o.filter.as_ref().expect("expected filter");
2196 assert!(matches!(filter, Expression::Between { .. }));
2197 }
2198
2199 #[test]
2200 fn parses_is_null() {
2201 let q = parse_one("observe containers where finished_at is null");
2202 let Query::Observe(ref o) = q.query else {
2203 panic!("expected Observe")
2204 };
2205 let filter = o.filter.as_ref().expect("expected filter");
2206 assert!(matches!(filter, Expression::IsNull(_)));
2207 }
2208
2209 #[test]
2210 fn parses_is_not_null() {
2211 let q = parse_one("observe containers where finished_at is not null");
2212 let Query::Observe(ref o) = q.query else {
2213 panic!("expected Observe")
2214 };
2215 let filter = o.filter.as_ref().expect("expected filter");
2216 assert!(matches!(filter, Expression::IsNotNull(_)));
2217 }
2218
2219 #[test]
2220 fn parses_function_call() {
2221 let q = parse_one("observe containers | where upper(name) contains \"API\"");
2222 let Query::Observe(ref o) = q.query else {
2223 panic!("expected Observe")
2224 };
2225 match &o.pipeline[0] {
2226 PipelineNode::Where(Expression::Comparison { left, .. }) => {
2227 assert!(
2228 matches!(left.as_ref(), Expression::FnCall { name, .. } if name == "upper")
2229 );
2230 }
2231 _ => panic!("expected FnCall comparison"),
2232 }
2233 }
2234
2235 #[test]
2236 fn parses_multi_field_sort() {
2237 let q = parse_one("observe containers | sort by state desc, cpu desc");
2238 let Query::Observe(ref o) = q.query else {
2239 panic!("expected Observe")
2240 };
2241 match &o.pipeline[0] {
2242 PipelineNode::SortBy { fields } => {
2243 assert_eq!(fields.len(), 2);
2244 assert_eq!(fields[0].0, "state");
2245 assert_eq!(fields[0].1, SortDirection::Desc);
2246 assert_eq!(fields[1].0, "cpu");
2247 }
2248 _ => panic!("expected SortBy"),
2249 }
2250 }
2251
2252 #[test]
2253 fn parses_distinct() {
2254 let q = parse_one("observe containers | distinct | select image");
2255 let Query::Observe(ref o) = q.query else {
2256 panic!("expected Observe")
2257 };
2258 assert!(matches!(o.pipeline[0], PipelineNode::Distinct));
2259 }
2260
2261 #[test]
2262 fn parses_compose_query() {
2263 let q = parse_one("compose myapp");
2264 assert!(matches!(q.query, Query::Compose(_)));
2265 if let Query::Compose(ref c) = q.query {
2266 assert_eq!(c.project, "myapp");
2267 assert_eq!(c.target, ComposeTarget::Containers);
2268 assert!(c.pipeline.is_empty());
2269 }
2270 }
2271
2272 #[test]
2273 fn parses_compose_services() {
2274 let q = parse_one("compose myapp services");
2275 assert!(matches!(q.query, Query::Compose(_)));
2276 if let Query::Compose(ref c) = q.query {
2277 assert_eq!(c.project, "myapp");
2278 assert_eq!(c.target, ComposeTarget::Services);
2279 }
2280 }
2281
2282 #[test]
2283 fn parses_observe_compose() {
2284 let q = parse_one("observe compose myapp");
2285 assert!(matches!(q.query, Query::Compose(_)));
2286 if let Query::Compose(ref c) = q.query {
2287 assert_eq!(c.project, "myapp");
2288 assert_eq!(c.target, ComposeTarget::Containers);
2289 }
2290 }
2291
2292 #[test]
2293 fn parses_compose_with_pipeline() {
2294 let q = parse_one("compose myapp | where cpu > 80% | select name, cpu");
2295 assert!(matches!(q.query, Query::Compose(_)));
2296 if let Query::Compose(ref c) = q.query {
2297 assert_eq!(c.project, "myapp");
2298 assert_eq!(c.pipeline.len(), 2);
2299 }
2300 }
2301
2302 #[test]
2303 fn parses_compose_containers_explicit() {
2304 let q = parse_one("compose myapp containers");
2305 if let Query::Compose(ref c) = q.query {
2306 assert_eq!(c.project, "myapp");
2307 assert_eq!(c.target, ComposeTarget::Containers);
2308 } else {
2309 panic!("expected Compose");
2310 }
2311 }
2312
2313 #[test]
2314 fn parses_observe_compose_services() {
2315 let q = parse_one("observe compose myapp services");
2316 if let Query::Compose(ref c) = q.query {
2317 assert_eq!(c.project, "myapp");
2318 assert_eq!(c.target, ComposeTarget::Services);
2319 } else {
2320 panic!("expected Compose");
2321 }
2322 }
2323
2324 #[test]
2325 fn parses_compose_with_sort_limit() {
2326 let q = parse_one("compose myapp services | sort by name asc | limit 5");
2327 if let Query::Compose(ref c) = q.query {
2328 assert_eq!(c.project, "myapp");
2329 assert_eq!(c.target, ComposeTarget::Services);
2330 assert_eq!(c.pipeline.len(), 2);
2331 } else {
2332 panic!("expected Compose");
2333 }
2334 }
2335
2336 #[test]
2337 fn parses_compose_with_group_by() {
2338 let q = parse_one("compose myapp | group by state with count(id) as cnt");
2339 if let Query::Compose(ref c) = q.query {
2340 assert_eq!(c.pipeline.len(), 1);
2341 assert!(matches!(c.pipeline[0], PipelineNode::GroupBy { .. }));
2342 } else {
2343 panic!("expected Compose");
2344 }
2345 }
2346
2347 #[test]
2348 fn parses_compose_with_where_and_select() {
2349 let q = parse_one("compose myapp | where state = \"running\" | select name, image, state");
2350 if let Query::Compose(ref c) = q.query {
2351 assert_eq!(c.pipeline.len(), 2);
2352 assert!(matches!(c.pipeline[0], PipelineNode::Where(_)));
2353 assert!(matches!(c.pipeline[1], PipelineNode::Select(_)));
2354 } else {
2355 panic!("expected Compose");
2356 }
2357 }
2358
2359 #[test]
2360 fn parses_compose_with_distinct() {
2361 let q = parse_one("compose myapp | select image | distinct");
2362 if let Query::Compose(ref c) = q.query {
2363 assert_eq!(c.pipeline.len(), 2);
2364 assert!(matches!(c.pipeline[1], PipelineNode::Distinct));
2365 } else {
2366 panic!("expected Compose");
2367 }
2368 }
2369
2370 #[test]
2371 fn parses_compose_hyphenated_project() {
2372 let q = parse_one("compose my-app-v2");
2373 if let Query::Compose(ref c) = q.query {
2374 assert_eq!(c.project, "my-app-v2");
2375 } else {
2376 panic!("expected Compose");
2377 }
2378 }
2379
2380 #[test]
2381 fn rejects_compose_without_project() {
2382 let result = parse("compose");
2383 assert!(result.is_err());
2384 }
2385
2386 #[test]
2387 fn parses_compose_networks() {
2388 let q = parse_one("compose myapp networks");
2389 if let Query::Compose(ref c) = q.query {
2390 assert_eq!(c.project, "myapp");
2391 assert_eq!(c.target, ComposeTarget::Networks);
2392 } else {
2393 panic!("expected Compose");
2394 }
2395 }
2396
2397 #[test]
2398 fn parses_compose_volumes() {
2399 let q = parse_one("compose myapp volumes");
2400 if let Query::Compose(ref c) = q.query {
2401 assert_eq!(c.project, "myapp");
2402 assert_eq!(c.target, ComposeTarget::Volumes);
2403 } else {
2404 panic!("expected Compose");
2405 }
2406 }
2407
2408 #[test]
2409 fn parses_compose_health() {
2410 let q = parse_one("compose myapp health");
2411 if let Query::Compose(ref c) = q.query {
2412 assert_eq!(c.project, "myapp");
2413 assert_eq!(c.target, ComposeTarget::Health);
2414 } else {
2415 panic!("expected Compose");
2416 }
2417 }
2418
2419 #[test]
2420 fn parses_observe_compose_networks() {
2421 let q = parse_one("observe compose myapp networks | select name, driver");
2422 if let Query::Compose(ref c) = q.query {
2423 assert_eq!(c.project, "myapp");
2424 assert_eq!(c.target, ComposeTarget::Networks);
2425 assert_eq!(c.pipeline.len(), 1);
2426 } else {
2427 panic!("expected Compose");
2428 }
2429 }
2430
2431 #[test]
2432 fn parses_observe_compose_health() {
2433 let q = parse_one("observe compose myapp health");
2434 if let Query::Compose(ref c) = q.query {
2435 assert_eq!(c.project, "myapp");
2436 assert_eq!(c.target, ComposeTarget::Health);
2437 } else {
2438 panic!("expected Compose");
2439 }
2440 }
2441
2442 #[test]
2443 fn parses_offset() {
2444 let q = parse_one("observe containers | sort by name asc | offset 10 | limit 5");
2445 let Query::Observe(ref o) = q.query else {
2446 panic!("expected Observe")
2447 };
2448 assert!(matches!(o.pipeline[1], PipelineNode::Offset(10)));
2449 }
2450
2451 #[test]
2452 fn parses_having() {
2453 let q =
2454 parse_one("observe containers | group by image with count(id) as cnt | having cnt > 3");
2455 let Query::Observe(ref o) = q.query else {
2456 panic!("expected Observe")
2457 };
2458 assert!(matches!(o.pipeline[1], PipelineNode::Having(_)));
2459 }
2460
2461 #[test]
2462 fn parses_observe_join_networks() {
2463 let q = parse_one("observe containers join networks on id = containers");
2464 let Query::Observe(ref o) = q.query else {
2465 panic!("expected Observe")
2466 };
2467 let join = o.join.as_ref().expect("expected join clause");
2468 assert_eq!(join.right, CollectionTarget::Networks);
2469 }
2470
2471 #[test]
2472 fn parses_observe_join_images() {
2473 let q = parse_one("observe containers join images on image = name");
2474 let Query::Observe(ref o) = q.query else {
2475 panic!("expected Observe")
2476 };
2477 assert!(o.join.is_some());
2478 assert_eq!(o.join.as_ref().unwrap().right, CollectionTarget::Images);
2479 }
2480
2481 #[test]
2482 fn parses_observe_join_with_pipeline() {
2483 let q = parse_one(
2484 "observe containers join networks on id = containers | select c.name, n.name",
2485 );
2486 let Query::Observe(ref o) = q.query else {
2487 panic!("expected Observe")
2488 };
2489 assert!(o.join.is_some());
2490 assert_eq!(o.pipeline.len(), 1);
2491 assert!(matches!(o.pipeline[0], PipelineNode::Select(_)));
2492 }
2493
2494 #[test]
2495 fn rejects_join_without_on() {
2496 let result = parse("observe containers join networks");
2497 assert!(result.is_err());
2498 }
2499
2500 #[test]
2501 fn parses_fill_pipeline() {
2502 let q = parse_one("observe containers | fill memory with 0");
2503 let Query::Observe(ref o) = q.query else {
2504 panic!("expected Observe")
2505 };
2506 assert!(matches!(o.pipeline[0], PipelineNode::Fill { .. }));
2507 }
2508
2509 #[test]
2510 fn parses_fill_with_expression() {
2511 let q = parse_one("observe containers | fill name with coalesce(label.name, name)");
2512 let Query::Observe(ref o) = q.query else {
2513 panic!("expected Observe")
2514 };
2515 assert!(matches!(o.pipeline[0], PipelineNode::Fill { .. }));
2516 }
2517
2518 #[test]
2519 fn parses_starts_with_operator() {
2520 let q = parse_one("observe containers where name starts_with \"api-\"");
2521 let Query::Observe(ref o) = q.query else {
2522 panic!("expected Observe")
2523 };
2524 let filter = o.filter.as_ref().expect("expected filter");
2525 assert!(matches!(
2526 filter,
2527 Expression::Comparison {
2528 operator: Operator::StartsWith,
2529 ..
2530 }
2531 ));
2532 }
2533
2534 #[test]
2535 fn parses_ends_with_operator() {
2536 let q = parse_one("observe containers where image ends_with \":latest\"");
2537 let Query::Observe(ref o) = q.query else {
2538 panic!("expected Observe")
2539 };
2540 let filter = o.filter.as_ref().expect("expected filter");
2541 assert!(matches!(
2542 filter,
2543 Expression::Comparison {
2544 operator: Operator::EndsWith,
2545 ..
2546 }
2547 ));
2548 }
2549
2550 #[test]
2551 fn parses_let_pipeline() {
2552 let q = parse_one("observe containers | let $threshold = 80");
2553 let Query::Observe(ref o) = q.query else {
2554 panic!("expected Observe")
2555 };
2556 match &o.pipeline[0] {
2557 PipelineNode::Let { name, .. } => assert_eq!(name, "threshold"),
2558 other => panic!("expected Let, got {other:?}"),
2559 }
2560 }
2561
2562 #[test]
2563 fn parses_let_without_dollar() {
2564 let q = parse_one("observe containers | let threshold = 80");
2565 let Query::Observe(ref o) = q.query else {
2566 panic!("expected Observe")
2567 };
2568 match &o.pipeline[0] {
2569 PipelineNode::Let { name, .. } => assert_eq!(name, "threshold"),
2570 other => panic!("expected Let, got {other:?}"),
2571 }
2572 }
2573
2574 #[test]
2575 fn parses_let_with_string_value() {
2576 let q = parse_one(r#"observe containers | let $app_name = "myapp""#);
2577 let Query::Observe(ref o) = q.query else {
2578 panic!("expected Observe")
2579 };
2580 match &o.pipeline[0] {
2581 PipelineNode::Let { name, value } => {
2582 assert_eq!(name, "app_name");
2583 assert!(matches!(value, Expression::Literal(Value::String(s)) if s == "myapp"));
2584 }
2585 other => panic!("expected Let, got {other:?}"),
2586 }
2587 }
2588
2589 #[test]
2590 fn parses_let_with_expression() {
2591 let q = parse_one("observe containers | let $limit = cpu * 2");
2592 let Query::Observe(ref o) = q.query else {
2593 panic!("expected Observe")
2594 };
2595 match &o.pipeline[0] {
2596 PipelineNode::Let { name, value } => {
2597 assert_eq!(name, "limit");
2598 assert!(matches!(value, Expression::Arithmetic { .. }));
2599 }
2600 other => panic!("expected Let, got {other:?}"),
2601 }
2602 }
2603
2604 #[test]
2605 fn parses_dollar_var_as_field() {
2606 let q = parse_one("observe containers where $state = running");
2607 let Query::Observe(ref o) = q.query else {
2608 panic!("expected Observe")
2609 };
2610 let filter = o.filter.as_ref().expect("expected filter");
2611 match filter {
2612 Expression::Comparison { left, .. } => {
2613 assert!(matches!(left.as_ref(), Expression::Field(f) if f == "state"));
2614 }
2615 _ => panic!("expected comparison"),
2616 }
2617 }
2618
2619 #[test]
2622 fn empty_query_has_suggestion() {
2623 let err = parse("").unwrap_err();
2624 assert_eq!(err.column, 1);
2625 assert_eq!(err.message, "empty DOL query");
2626 assert!(err.suggestion.is_some());
2627 let sug = err.suggestion.as_ref().unwrap();
2628 assert!(
2629 sug.contains("observe containers"),
2630 "suggestion should mention `observe containers`: {sug}"
2631 );
2632 }
2633
2634 #[test]
2635 fn whitespace_query_has_suggestion() {
2636 let err = parse(" ").unwrap_err();
2637 assert_eq!(err.message, "empty DOL query");
2638 assert!(err.suggestion.is_some());
2639 }
2640
2641 #[test]
2642 fn suggestion_appears_in_display() {
2643 let err = parse("").unwrap_err();
2644 let msg = err.to_string();
2645 assert!(
2646 msg.contains("help:"),
2647 "Display should include help text: {msg}"
2648 );
2649 assert!(
2650 msg.contains("observe containers"),
2651 "Display should include suggestion: {msg}"
2652 );
2653 }
2654
2655 #[test]
2656 fn format_color_contains_ansi_escapes() {
2657 let err = parse("").unwrap_err();
2658 let colored = err.format_color();
2659 assert!(
2661 colored.contains('\x1b'),
2662 "format_color should contain ANSI escapes"
2663 );
2664 assert!(
2665 colored.contains("error"),
2666 "format_color should contain error label"
2667 );
2668 assert!(
2669 colored.contains("help:"),
2670 "format_color should contain help text"
2671 );
2672 assert!(
2674 !colored.starts_with("parse error"),
2675 "format_color should not use Display's plain prefix"
2676 );
2677 }
2678
2679 #[test]
2680 fn format_color_source_pointer_has_ansi() {
2681 let err = parse("observe containers where").unwrap_err();
2682 let colored = err.format_color();
2683 assert!(colored.contains('\x1b'), "should contain ANSI");
2684 assert!(colored.contains("-->"), "should show source pointer");
2685 assert!(colored.contains('^'), "should show caret");
2686 }
2687
2688 #[test]
2689 fn parse_error_with_source_has_pointer_and_suggestion() {
2690 let err = ParseError::new(5, "test error")
2692 .with_source("observe containers".to_owned())
2693 .with_suggestion("try something else");
2694
2695 assert_eq!(err.column, 5);
2696 assert_eq!(err.message, "test error");
2697 assert_eq!(err.source.as_deref(), Some("observe containers"));
2698 assert_eq!(err.suggestion.as_deref(), Some("try something else"));
2699
2700 let msg = err.to_string();
2701 assert!(msg.contains("parse error at column 5"));
2702 assert!(msg.contains("-->"));
2703 assert!(msg.contains("help: try something else"));
2704 }
2705
2706 #[test]
2707 fn unknown_query_family_has_suggestion() {
2708 let err = parse("watc containers").unwrap_err();
2709 assert!(err.suggestion.is_some());
2710 let sug = err.suggestion.as_ref().unwrap();
2711 assert!(
2712 sug.contains("try one of"),
2713 "suggestion should list alternatives: {sug}"
2714 );
2715 assert!(
2716 sug.contains("observe"),
2717 "suggestion should mention `observe`: {sug}"
2718 );
2719 }
2720
2721 #[test]
2722 fn unknown_collection_target_has_suggestion() {
2723 let err = parse("observe containerz").unwrap_err();
2724 assert!(err.suggestion.is_some(), "should have a suggestion");
2725 let sug = err.suggestion.as_ref().unwrap();
2726 assert!(
2727 sug.contains("containers") || sug.contains("try one of"),
2728 "suggestion should guide to valid targets: {sug}"
2729 );
2730 }
2731
2732 #[test]
2733 fn partial_collection_target_gets_similar_suggestion() {
2734 let err = parse("observe conts").unwrap_err();
2736 assert!(err.suggestion.is_some());
2737 let sug = err.suggestion.as_ref().unwrap();
2738 assert!(
2739 sug.contains("containers"),
2740 "suggestion should suggest `containers`: {sug}"
2741 );
2742 }
2743
2744 #[test]
2745 fn partial_singular_target_gets_similar_suggestion() {
2746 let err = parse("inspect cont my-container").unwrap_err();
2748 assert!(err.suggestion.is_some());
2749 let sug = err.suggestion.as_ref().unwrap();
2750 assert!(
2751 sug.contains("container"),
2752 "suggestion should suggest `container`: {sug}"
2753 );
2754 }
2755
2756 #[test]
2757 fn unknown_pipeline_node_has_suggestion() {
2758 let err = parse("observe containers | fltr name = test").unwrap_err();
2759 assert!(err.suggestion.is_some());
2760 let sug = err.suggestion.as_ref().unwrap();
2761 assert!(
2762 sug.contains("where") || sug.contains("select"),
2763 "suggestion should list pipeline options: {sug}"
2764 );
2765 }
2766
2767 #[test]
2768 fn unknown_analysis_verb_has_suggestion() {
2769 let err = parse("analyze containers delet").unwrap_err();
2770 assert!(err.suggestion.is_some());
2771 let sug = err.suggestion.as_ref().unwrap();
2772 assert!(
2773 sug.contains("find") || sug.contains("correlate") || sug.contains("explain"),
2774 "suggestion should list analysis verbs: {sug}"
2775 );
2776 }
2777
2778 #[test]
2779 fn unknown_alert_action_has_suggestion() {
2780 let err = parse(r#"alert when cpu > 80% then emai "alert""#).unwrap_err();
2781 assert!(err.suggestion.is_some());
2782 let sug = err.suggestion.as_ref().unwrap();
2783 assert!(
2784 sug.contains("print") || sug.contains("webhook") || sug.contains("restart"),
2785 "suggestion should list alert actions: {sug}"
2786 );
2787 }
2788
2789 #[test]
2790 fn unterminated_string_has_suggestion() {
2791 let err = parse(r#"observe containers where name = "unfinished"#).unwrap_err();
2792 assert!(err.suggestion.is_some());
2793 let sug = err.suggestion.as_ref().unwrap();
2794 assert!(
2795 sug.contains("closing"),
2796 "suggestion should mention closing quote: {sug}"
2797 );
2798 }
2799
2800 #[test]
2801 fn unexpected_character_has_suggestion() {
2802 let err = parse("observe containers | where cpu > 80% | select name~image").unwrap_err();
2803 assert!(err.suggestion.is_some());
2804 let sug = err.suggestion.as_ref().unwrap();
2805 assert!(
2806 sug.contains("expected letters"),
2807 "suggestion should describe valid characters: {sug}"
2808 );
2809 }
2810
2811 #[test]
2812 fn unexpected_token_after_query_has_suggestion() {
2813 let err = parse("ping extra").unwrap_err();
2814 assert!(err.suggestion.is_some());
2815 let sug = err.suggestion.as_ref().unwrap();
2816 assert!(
2817 sug.contains("remove") || sug.contains("check"),
2818 "suggestion should mention remove/check: {sug}"
2819 );
2820 assert!(
2821 sug.contains("extra"),
2822 "suggestion should mention the unexpected token: {sug}"
2823 );
2824 }
2825
2826 #[test]
2827 fn multiple_suggestions_not_crashing() {
2828 let err = parse("!@#$%^&").unwrap_err();
2831 assert!(err.suggestion.is_some());
2832 let msg = err.to_string();
2833 assert!(!msg.is_empty(), "error message should not be empty");
2834 let colored = err.format_color();
2836 assert!(!colored.is_empty(), "colored output should not be empty");
2837 }
2838
2839 #[test]
2840 fn error_contains_source_column_pointer() {
2841 let err = parse("observe containers where").unwrap_err();
2843 let msg = err.to_string();
2844 assert!(
2845 msg.contains("observe containers where"),
2846 "should show source"
2847 );
2848 assert!(msg.contains("^"), "should show pointer");
2849 assert!(msg.contains("-->"), "should show arrow marker");
2851 assert!(msg.contains(" column "), "should mention column");
2852 }
2853
2854 #[test]
2855 fn error_pipe_pipeline_node_suggestion() {
2856 let err = parse("observe containers | unknown_node").unwrap_err();
2858 assert!(err.suggestion.is_some());
2859 let sug = err.suggestion.as_ref().unwrap();
2860 assert!(
2861 sug.contains("where"),
2862 "should suggest pipeline keywords: {sug}"
2863 );
2864 }
2865
2866 #[test]
2867 fn pipeline_keyword_similar_suggestion() {
2868 let err = parse("observe containers | fltr name = test").unwrap_err();
2870 let sug = err.suggestion.as_ref().expect("should have suggestion");
2871 assert!(
2872 sug.contains("fill"),
2873 "should suggest `fill` for `fltr`: {sug}"
2874 );
2875 assert!(
2876 sug.contains("did you mean"),
2877 "should contain 'did you mean': {sug}"
2878 );
2879 }
2880
2881 #[test]
2882 fn pipeline_keyword_grp_suggests_group() {
2883 let err = parse("observe containers | grp by image").unwrap_err();
2885 let sug = err.suggestion.as_ref().expect("should have suggestion");
2886 assert!(
2887 sug.contains("group"),
2888 "should suggest `group` for `grp`: {sug}"
2889 );
2890 assert!(
2891 sug.contains("did you mean"),
2892 "should contain 'did you mean': {sug}"
2893 );
2894 }
2895
2896 #[test]
2897 fn analysis_verb_similar_suggestion() {
2898 let err = parse("analyze containers delet").unwrap_err();
2900 let sug = err.suggestion.as_ref().expect("should have suggestion");
2901 assert!(
2903 sug.contains("find") || sug.contains("correlate") || sug.contains("explain"),
2904 "should list analysis verbs: {sug}"
2905 );
2906 }
2907
2908 #[test]
2909 fn alert_action_similar_suggestion() {
2910 let err = parse(r#"alert when cpu > 80% then emai "alert""#).unwrap_err();
2913 let sug = err.suggestion.as_ref().expect("should have suggestion");
2914 assert!(
2915 sug.contains("print") || sug.contains("webhook") || sug.contains("restart"),
2916 "should list alert actions: {sug}"
2917 );
2918 }
2919
2920 #[test]
2921 fn alert_action_close_match_suggestion() {
2922 let err = parse(r#"alert when cpu > 80% then prnt "alert""#).unwrap_err();
2924 let sug = err.suggestion.as_ref().expect("should have suggestion");
2925 assert!(
2926 sug.contains("print"),
2927 "should suggest `print` for `prnt`: {sug}"
2928 );
2929 assert!(
2930 sug.contains("did you mean"),
2931 "should contain 'did you mean': {sug}"
2932 );
2933 }
2934
2935 #[test]
2936 fn suggest_keyword_function_direct() {
2937 assert!(
2939 suggest_keyword("", PIPELINE_KEYWORDS).is_none(),
2940 "empty input should return None"
2941 );
2942 assert!(
2943 suggest_keyword("xyz", PIPELINE_KEYWORDS).is_none(),
2944 "no match should return None"
2945 );
2946 let result = suggest_keyword("whre", PIPELINE_KEYWORDS).expect("should find suggestion");
2947 assert!(
2948 result.contains("where"),
2949 "should suggest `where` for `whre`: {result}"
2950 );
2951 let result = suggest_keyword("slect", PIPELINE_KEYWORDS).expect("should find suggestion");
2952 assert!(
2953 result.contains("select"),
2954 "should suggest `select` for `slect`: {result}"
2955 );
2956 let result = suggest_keyword("lmit", PIPELINE_KEYWORDS).expect("should find suggestion");
2957 assert!(
2958 result.contains("limit"),
2959 "should suggest `limit` for `lmit`: {result}"
2960 );
2961 }
2962
2963 #[test]
2964 fn suggest_keyword_analysis_verbs() {
2965 let result = suggest_keyword("fnd", ANALYSIS_VERBS).expect("should find suggestion");
2966 assert!(
2967 result.contains("find"),
2968 "should suggest `find` for `del`: {result}"
2969 );
2970 let result = suggest_keyword("corr", ANALYSIS_VERBS).expect("should find suggestion");
2971 assert!(
2972 result.contains("correlate"),
2973 "should suggest `correlate` for `corr`: {result}"
2974 );
2975 let result = suggest_keyword("expln", ANALYSIS_VERBS).expect("should find suggestion");
2976 assert!(
2977 result.contains("explain"),
2978 "should suggest `explain` for `expln`: {result}"
2979 );
2980 }
2981
2982 #[test]
2983 fn suggest_keyword_alert_actions() {
2984 let result = suggest_keyword("prnt", ALERT_ACTIONS).expect("should find suggestion");
2985 assert!(
2986 result.contains("print"),
2987 "should suggest `print` for `prnt`: {result}"
2988 );
2989 let result = suggest_keyword("webhk", ALERT_ACTIONS).expect("should find suggestion");
2990 assert!(
2991 result.contains("webhook"),
2992 "should suggest `webhook` for `webhk`: {result}"
2993 );
2994 let result = suggest_keyword("restt", ALERT_ACTIONS).expect("should find suggestion");
2995 assert!(
2996 result.contains("restart"),
2997 "should suggest `restart` for `restt`: {result}"
2998 );
2999 }
3000
3001 #[test]
3002 fn error_invalid_float_displays_message() {
3003 let err = parse("observe containers | where cpu > 1.2.3").unwrap_err();
3005 let msg = err.to_string();
3008 assert!(!msg.is_empty(), "error should have a message");
3009 }
3010
3011 #[test]
3014 fn suggest_similar_target_exact_match_falls_through() {
3015 let sug = suggest_similar_target("container");
3018 assert!(sug.contains("containers") || sug.contains("try one of"));
3020 }
3021
3022 #[test]
3023 fn suggest_similar_target_empty_input() {
3024 let sug = suggest_similar_target("");
3025 assert!(
3026 sug.contains("try one of"),
3027 "empty input should show generic message: {sug}"
3028 );
3029 }
3030
3031 #[test]
3032 fn suggest_similar_target_completely_wrong() {
3033 let sug = suggest_similar_target("xyz");
3034 assert!(
3035 sug.contains("try one of"),
3036 "completely wrong input should show generic message: {sug}"
3037 );
3038 }
3039
3040 #[test]
3041 fn suggest_similar_target_close_match() {
3042 let sug = suggest_similar_target("imags");
3043 assert!(
3044 sug.contains("images"),
3045 "should suggest `images` for `imags`: {sug}"
3046 );
3047 }
3048
3049 #[test]
3050 fn str_similarity_identical() {
3051 assert_eq!(str_similarity("hello", "hello"), 0);
3052 }
3053
3054 #[test]
3055 fn str_similarity_prefix_match() {
3056 assert_eq!(str_similarity("test", "testing"), 3);
3058 }
3059
3060 #[test]
3061 fn str_similarity_no_match() {
3062 assert_eq!(str_similarity("abc", "xyz"), 3);
3064 }
3065
3066 #[test]
3069 fn parse_error_builder_chaining() {
3070 let err = ParseError::new(10, "test")
3071 .with_source("source query".to_owned())
3072 .with_suggestion("a suggestion");
3073 assert_eq!(err.column, 10);
3074 assert_eq!(err.message, "test");
3075 assert_eq!(err.source.as_deref(), Some("source query"));
3076 assert_eq!(err.suggestion.as_deref(), Some("a suggestion"));
3077 }
3078
3079 #[test]
3080 fn parse_error_without_suggestion_displays_no_help() {
3081 let err = ParseError::new(1, "simple error");
3082 assert!(err.suggestion.is_none());
3083 let msg = err.to_string();
3084 assert!(!msg.contains("help:"), "no suggestion should not show help");
3085 }
3086
3087 #[test]
3088 fn format_color_shows_suggestion() {
3089 let err = ParseError::new(5, "error")
3090 .with_source("test query".to_owned())
3091 .with_suggestion("try something");
3092 let colored = err.format_color();
3093 assert!(
3094 colored.contains("try something"),
3095 "should include suggestion text"
3096 );
3097 assert!(colored.contains('\x1b'), "should include ANSI codes");
3098 }
3099
3100 #[test]
3101 fn format_color_without_suggestion_still_has_error() {
3102 let err = ParseError::new(1, "simple error");
3103 let colored = err.format_color();
3104 assert!(
3105 colored.contains("error"),
3106 "format_color must include the word 'error': {colored}"
3107 );
3108 assert!(
3109 !colored.contains("help:"),
3110 "no suggestion should not show help"
3111 );
3112 }
3113
3114 #[test]
3115 fn source_context_shows_correct_line() {
3116 let err = parse("observe containers\n| where cpu > 80%\n| unknown\n| limit 5").unwrap_err();
3118 let msg = err.to_string();
3119 assert!(
3121 msg.contains("| unknown"),
3122 "should show the line with the error: {msg}"
3123 );
3124 assert!(
3126 !msg.lines()
3127 .any(|l| l.contains("observe containers") && l.contains("-->")),
3128 "source pointer should point to the line with unknown, not the first line"
3129 );
3130 }
3131
3132 #[test]
3135 fn parses_row_number() {
3136 let q = parse_one("observe containers | row_number as rn");
3137 let Query::Observe(ref o) = q.query else {
3138 panic!("expected Observe")
3139 };
3140 match &o.pipeline[0] {
3141 PipelineNode::RowNumber { alias } => assert_eq!(alias, "rn"),
3142 other => panic!("expected RowNumber, got {other:?}"),
3143 }
3144 }
3145
3146 #[test]
3147 fn parses_rank() {
3148 let q = parse_one("observe containers | sort by cpu desc | rank(cpu) as r");
3149 let Query::Observe(ref o) = q.query else {
3150 panic!("expected Observe")
3151 };
3152 match &o.pipeline[1] {
3153 PipelineNode::Rank { field, alias } => {
3154 assert_eq!(field, "cpu");
3155 assert_eq!(alias, "r");
3156 }
3157 other => panic!("expected Rank, got {other:?}"),
3158 }
3159 }
3160
3161 #[test]
3162 fn parses_lag() {
3163 let q = parse_one("observe containers | lag(name) as prev_name");
3164 let Query::Observe(ref o) = q.query else {
3165 panic!("expected Observe")
3166 };
3167 match &o.pipeline[0] {
3168 PipelineNode::Lag {
3169 field,
3170 alias,
3171 offset,
3172 } => {
3173 assert_eq!(field, "name");
3174 assert_eq!(alias, "prev_name");
3175 assert_eq!(*offset, 1);
3176 }
3177 other => panic!("expected Lag, got {other:?}"),
3178 }
3179 }
3180
3181 #[test]
3182 fn parses_lead_with_offset() {
3183 let q = parse_one("observe containers | lead(name, 2) as next_name");
3184 let Query::Observe(ref o) = q.query else {
3185 panic!("expected Observe")
3186 };
3187 match &o.pipeline[0] {
3188 PipelineNode::Lead {
3189 field,
3190 alias,
3191 offset,
3192 } => {
3193 assert_eq!(field, "name");
3194 assert_eq!(alias, "next_name");
3195 assert_eq!(*offset, 2);
3196 }
3197 other => panic!("expected Lead, got {other:?}"),
3198 }
3199 }
3200}