1use crate::ast::{CollectionTarget, Expression, PipelineNode, Query, SetValue, Value};
15use crate::eval::EvalError;
16use crate::target_alias;
17use std::collections::BTreeMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Type {
21 String,
22 Integer,
23 Float,
24 Boolean,
25 Percentage,
26 Duration,
27 Array,
28 Unknown,
29}
30
31impl Type {
32 #[must_use]
33 pub const fn is_numeric(&self) -> bool {
34 matches!(
35 self,
36 Self::Integer | Self::Float | Self::Percentage | Self::Duration
37 )
38 }
39
40 #[must_use]
41 pub fn is_compatible(&self, other: &Self) -> bool {
42 if *self == Self::Unknown || *other == Self::Unknown {
43 return true;
44 }
45 if self.is_numeric() && other.is_numeric() {
46 return true;
47 }
48 self == other
49 }
50}
51
52pub struct SemanticAnalyzer {
53 active_schema: BTreeMap<String, Type>,
54 target: CollectionTarget,
55}
56
57impl SemanticAnalyzer {
58 #[must_use]
59 pub fn new(target: CollectionTarget) -> Self {
60 let mut active_schema = BTreeMap::new();
61 for (field, ty) in Self::schema_for_target(target) {
62 active_schema.insert(field.to_owned(), ty);
63 }
64
65 Self {
66 active_schema,
67 target,
68 }
69 }
70
71 pub fn validate_query(&mut self, query: &Query) -> Result<(), EvalError> {
72 match query {
73 Query::Observe(q) => {
74 if let Some(join) = &q.join {
75 let right_schema = Self::schema_for_target(join.right);
76 let left_alias = target_alias(self.target);
77 let right_alias = target_alias(join.right);
78 let original_schema = std::mem::take(&mut self.active_schema);
79 for (field, ty) in original_schema {
80 self.active_schema
81 .insert(format!("{left_alias}.{field}"), ty);
82 }
83 for (field, ty) in right_schema {
84 self.active_schema
85 .insert(format!("{right_alias}.{field}"), ty);
86 }
87 }
88 if let Some(filter) = &q.filter {
89 self.validate_expression(filter)?;
90 }
91 for node in &q.pipeline {
92 self.apply_pipeline_node(node)?;
93 }
94 }
95 Query::Events(q) => {
96 if let Some(filter) = &q.filter {
97 self.validate_expression(filter)?;
98 }
99 for node in &q.pipeline {
100 self.apply_pipeline_node(node)?;
101 }
102 }
103 Query::Analyze(q) => {
104 for node in &q.pipeline {
105 self.apply_pipeline_node(node)?;
106 }
107 }
108 Query::Alert(rule) => {
109 self.validate_expression(&rule.condition)?;
110 }
111 Query::Logs(q) => {
112 if let Some(filter) = &q.filter {
113 self.validate_expression(filter)?;
114 }
115 for node in &q.pipeline {
116 self.apply_pipeline_node(node)?;
117 }
118 }
119 Query::Compose(q) => {
120 match q.target {
121 crate::ast::ComposeTarget::Services
122 | crate::ast::ComposeTarget::Ps
123 | crate::ast::ComposeTarget::Stats => {
124 self.active_schema
125 .insert("service".to_owned(), Type::String);
126 }
127 crate::ast::ComposeTarget::Health => {
128 self.active_schema
129 .insert("service".to_owned(), Type::String);
130 if !self.active_schema.contains_key("health") {
131 self.active_schema.insert("health".to_owned(), Type::String);
132 }
133 }
134 crate::ast::ComposeTarget::Projects => {
135 self.active_schema = BTreeMap::new();
136 self.active_schema
137 .insert("project".to_owned(), Type::String);
138 self.active_schema
139 .insert("containers".to_owned(), Type::Integer);
140 self.active_schema
141 .insert("running".to_owned(), Type::Integer);
142 self.active_schema
143 .insert("stopped".to_owned(), Type::Integer);
144 self.active_schema
145 .insert("networks".to_owned(), Type::Integer);
146 self.active_schema
147 .insert("volumes".to_owned(), Type::Integer);
148 self.active_schema.insert("status".to_owned(), Type::String);
149 }
150 crate::ast::ComposeTarget::Images => {
151 self.active_schema = BTreeMap::new();
152 self.active_schema.insert("id".to_owned(), Type::String);
153 self.active_schema
154 .insert("repository".to_owned(), Type::String);
155 self.active_schema.insert("name".to_owned(), Type::String);
156 self.active_schema.insert("tag".to_owned(), Type::String);
157 self.active_schema.insert("digest".to_owned(), Type::String);
158 self.active_schema.insert("size".to_owned(), Type::String);
159 self.active_schema
160 .insert("service".to_owned(), Type::String);
161 }
162 crate::ast::ComposeTarget::Events => {
163 self.active_schema = BTreeMap::new();
164 self.active_schema.insert("time".to_owned(), Type::String);
165 self.active_schema.insert("action".to_owned(), Type::String);
166 self.active_schema
167 .insert("service".to_owned(), Type::String);
168 self.active_schema
169 .insert("container".to_owned(), Type::String);
170 self.active_schema.insert("image".to_owned(), Type::String);
171 }
172 crate::ast::ComposeTarget::Port => {
173 self.active_schema = BTreeMap::new();
174 self.active_schema
175 .insert("service".to_owned(), Type::String);
176 self.active_schema
177 .insert("container".to_owned(), Type::String);
178 self.active_schema.insert("ports".to_owned(), Type::Array);
179 }
180 crate::ast::ComposeTarget::Config => {
181 self.active_schema = BTreeMap::new();
182 self.active_schema.insert("name".to_owned(), Type::String);
183 self.active_schema.insert("image".to_owned(), Type::String);
184 self.active_schema.insert("state".to_owned(), Type::String);
185 self.active_schema.insert("status".to_owned(), Type::String);
186 self.active_schema.insert("ports".to_owned(), Type::Array);
187 self.active_schema
188 .insert("restart_count".to_owned(), Type::Integer);
189 self.active_schema.insert("health".to_owned(), Type::String);
190 self.active_schema
191 .insert("depends_on".to_owned(), Type::Array);
192 self.active_schema.insert("driver".to_owned(), Type::String);
193 self.active_schema.insert("scope".to_owned(), Type::String);
194 self.active_schema
195 .insert("containers".to_owned(), Type::Integer);
196 self.active_schema
197 .insert("mountpoint".to_owned(), Type::String);
198 }
199 crate::ast::ComposeTarget::Logs => {
200 self.active_schema = BTreeMap::new();
201 self.active_schema.insert("line".to_owned(), Type::Integer);
202 self.active_schema
203 .insert("message".to_owned(), Type::String);
204 self.active_schema
205 .insert("service".to_owned(), Type::String);
206 self.active_schema
207 .insert("container".to_owned(), Type::String);
208 }
209 _ => {}
210 }
211 for node in &q.pipeline {
212 self.apply_pipeline_node(node)?;
213 }
214 }
215 Query::Ping | Query::Inspect(_) | Query::Fields(_) => {}
216 }
217 Ok(())
218 }
219
220 fn check_field_validity(&self, field: &str) -> Result<(), EvalError> {
221 if self.active_schema.contains_key(field) {
222 return Ok(());
223 }
224 if field.starts_prefix_label() {
225 if self.active_schema.contains_key("labels") {
227 return Ok(());
228 }
229 }
230 Err(EvalError::UnsupportedField {
231 field: field.to_owned(),
232 })
233 }
234
235 fn infer_expr_type(&self, expr: &Expression) -> Result<Type, EvalError> {
236 match expr {
237 Expression::Field(f) => {
238 self.check_field_validity(f)?;
239 if f.starts_prefix_label() {
240 Ok(Type::String)
241 } else {
242 Ok(*self.active_schema.get(f).unwrap_or(&Type::Unknown))
243 }
244 }
245 Expression::Literal(v) => match v {
246 Value::String(_) | Value::Identifier(_) => Ok(Type::String),
247 Value::Integer(_) => Ok(Type::Integer),
248 Value::Float(_) => Ok(Type::Float),
249 Value::Percentage(_) => Ok(Type::Percentage),
250 Value::Boolean(_) => Ok(Type::Boolean),
251 },
252 Expression::Arithmetic { left, op, right } => {
253 let lt = self.infer_expr_type(left)?;
254 let rt = self.infer_expr_type(right)?;
255 if !lt.is_numeric() || !rt.is_numeric() {
256 return Err(EvalError::Arithmetic(format!(
257 "invalid arithmetic operator '{op:?}' on types {lt:?} and {rt:?}"
258 )));
259 }
260 if lt == Type::Float || rt == Type::Float {
261 Ok(Type::Float)
262 } else {
263 Ok(Type::Integer)
264 }
265 }
266 Expression::Comparison {
267 left,
268 operator,
269 right,
270 } => {
271 let lt = self.infer_expr_type(left)?;
272 let rt = self.infer_expr_type(right)?;
273 if !lt.is_compatible(&rt) {
274 let field_name = match &**left {
275 Expression::Field(f) => f.clone(),
276 _ => "expression".to_owned(),
277 };
278 return Err(EvalError::InvalidComparison {
279 field: field_name,
280 operator: format!("{operator:?}"),
281 });
282 }
283 Ok(Type::Boolean)
284 }
285 Expression::In { expr, .. }
286 | Expression::IsNull(expr)
287 | Expression::IsNotNull(expr)
288 | Expression::Between { expr, .. } => {
289 self.infer_expr_type(expr)?;
290 Ok(Type::Boolean)
291 }
292 Expression::And(left, right) | Expression::Or(left, right) => {
293 let lt = self.infer_expr_type(left)?;
294 let rt = self.infer_expr_type(right)?;
295 if lt != Type::Boolean && lt != Type::Unknown {
296 return Err(EvalError::InvalidComparison {
297 field: "AND/OR left operand".to_owned(),
298 operator: "logical".to_owned(),
299 });
300 }
301 if rt != Type::Boolean && rt != Type::Unknown {
302 return Err(EvalError::InvalidComparison {
303 field: "AND/OR right operand".to_owned(),
304 operator: "logical".to_owned(),
305 });
306 }
307 Ok(Type::Boolean)
308 }
309 Expression::Not(expr) => {
310 let t = self.infer_expr_type(expr)?;
311 if t != Type::Boolean && t != Type::Unknown {
312 return Err(EvalError::InvalidComparison {
313 field: "NOT operand".to_owned(),
314 operator: "logical".to_owned(),
315 });
316 }
317 Ok(Type::Boolean)
318 }
319 Expression::FnCall { name, args } => {
320 match name.as_str() {
322 "upper" | "lower" | "trim" | "length" | "concat" | "substring" | "coalesce"
323 | "to_int" | "to_float" | "to_string" | "starts_with" | "ends_with"
324 | "replace" | "reverse" | "repeat" | "split_part" | "position" | "now"
325 | "date_format" | "date_diff" | "extract" => {
326 for arg in args {
327 self.infer_expr_type(arg)?;
328 }
329 match name.as_str() {
330 "to_float" => Ok(Type::Float),
331 "length" | "position" | "date_diff" | "extract" | "to_int" => {
332 Ok(Type::Integer)
333 }
334 "starts_with" | "ends_with" => Ok(Type::Boolean),
335 _ => Ok(Type::String),
336 }
337 }
338 _ => Err(EvalError::UnknownFunction { name: name.clone() }),
339 }
340 }
341 }
342 }
343
344 fn validate_expression(&self, expr: &Expression) -> Result<(), EvalError> {
345 self.infer_expr_type(expr)?;
346 Ok(())
347 }
348
349 fn validate_set_value_type(&self, value: &SetValue) -> Result<Type, EvalError> {
351 match value {
352 SetValue::Literal(v) => match v {
353 Value::String(_) | Value::Identifier(_) => Ok(Type::String),
354 Value::Integer(_) => Ok(Type::Integer),
355 Value::Float(_) => Ok(Type::Float),
356 Value::Percentage(_) => Ok(Type::Percentage),
357 Value::Boolean(_) => Ok(Type::Boolean),
358 },
359 SetValue::Expr(expr) => self.infer_expr_type(expr),
360 SetValue::Case {
361 when_clauses,
362 else_value,
363 } => {
364 for (cond, val) in when_clauses {
365 self.validate_expression(cond)?;
366 self.validate_expression(val)?;
367 }
368 if let Some(else_expr) = else_value {
369 self.validate_expression(else_expr)?;
370 }
371 Ok(Type::Unknown)
372 }
373 SetValue::IfElse {
374 condition,
375 then_value,
376 else_value,
377 } => {
378 self.validate_expression(condition)?;
379 self.validate_expression(then_value)?;
380 if let Some(else_expr) = else_value {
381 self.validate_expression(else_expr)?;
382 }
383 Ok(Type::Unknown)
384 }
385 }
386 }
387
388 fn apply_pipeline_node(&mut self, node: &PipelineNode) -> Result<(), EvalError> {
389 match node {
390 PipelineNode::Where(expr) | PipelineNode::Having(expr) => {
391 self.validate_expression(expr)?;
392 }
393 PipelineNode::Select(fields) => {
394 let mut new_schema = BTreeMap::new();
395 for f in fields {
396 self.check_field_validity(f)?;
397 let ty = if f.starts_prefix_label() {
398 Type::String
399 } else {
400 *self.active_schema.get(f).unwrap_or(&Type::Unknown)
401 };
402 new_schema.insert(f.clone(), ty);
403 }
404 self.active_schema = new_schema;
405 }
406 PipelineNode::SortBy { fields } => {
407 for (f, _) in fields {
408 self.check_field_validity(f)?;
409 }
410 }
411 PipelineNode::Limit(_)
412 | PipelineNode::Offset(_)
413 | PipelineNode::Distinct
414 | PipelineNode::Alert(_)
415 | PipelineNode::Debug
416 | PipelineNode::Assert(_) => {}
417 PipelineNode::GroupBy { fields, aggregates } => {
418 let mut new_schema = BTreeMap::new();
419 for f in fields {
420 self.check_field_validity(f)?;
421 let ty = *self.active_schema.get(f).unwrap_or(&Type::Unknown);
422 new_schema.insert(f.clone(), ty);
423 }
424 for agg in aggregates {
425 self.check_field_validity(&agg.field)?;
426 let ty = match agg.function.as_str() {
427 "count" => Type::Integer,
428 "sum" | "avg" | "min" | "max" | "median" | "percentile" => Type::Float,
429 _ => Type::Unknown,
430 };
431 new_schema.insert(agg.alias.clone(), ty);
432 }
433 self.active_schema = new_schema;
434 }
435 PipelineNode::If {
436 condition,
437 then_branch,
438 else_branch,
439 } => {
440 self.validate_expression(condition)?;
441 let mut then_analyzer = Self {
442 active_schema: self.active_schema.clone(),
443 target: self.target,
444 };
445 for node in then_branch {
446 then_analyzer.apply_pipeline_node(node)?;
447 }
448 if let Some(else_b) = else_branch {
449 let mut else_analyzer = Self {
450 active_schema: self.active_schema.clone(),
451 target: self.target,
452 };
453 for node in else_b {
454 else_analyzer.apply_pipeline_node(node)?;
455 }
456 }
457 }
458 PipelineNode::Set { field, value } => {
459 let ty = match value {
460 SetValue::Literal(v) => match v {
461 Value::String(_) | Value::Identifier(_) => Type::String,
462 Value::Integer(_) => Type::Integer,
463 Value::Float(_) => Type::Float,
464 Value::Percentage(_) => Type::Percentage,
465 Value::Boolean(_) => Type::Boolean,
466 },
467 SetValue::Expr(expr) => self.infer_expr_type(expr)?,
468 SetValue::Case {
469 when_clauses,
470 else_value,
471 } => {
472 for (cond, val) in when_clauses {
473 self.validate_expression(cond)?;
474 self.validate_expression(val)?;
475 }
476 if let Some(else_expr) = else_value {
477 self.validate_expression(else_expr)?;
478 }
479 Type::Unknown
480 }
481 SetValue::IfElse {
482 condition,
483 then_value,
484 else_value,
485 } => {
486 self.validate_expression(condition)?;
487 self.validate_expression(then_value)?;
488 if let Some(else_expr) = else_value {
489 self.validate_expression(else_expr)?;
490 }
491 Type::Unknown
492 }
493 };
494 self.active_schema.insert(field.clone(), ty);
495 }
496 PipelineNode::Fill {
497 field,
498 default,
499 condition,
500 } => {
501 if let Some(cond) = condition {
502 self.validate_expression(cond)?;
503 }
504 let ty = self.validate_set_value_type(default)?;
505 self.active_schema.insert(field.clone(), ty);
506 }
507 PipelineNode::RowNumber { alias } => {
508 self.active_schema.insert(alias.clone(), Type::Integer);
509 }
510 PipelineNode::Rank { field, alias } => {
511 self.check_field_validity(field)?;
512 self.active_schema.insert(alias.clone(), Type::Integer);
513 }
514 PipelineNode::Lag { field, alias, .. } | PipelineNode::Lead { field, alias, .. } => {
515 self.check_field_validity(field)?;
516 let ty = *self.active_schema.get(field).unwrap_or(&Type::Unknown);
517 self.active_schema.insert(alias.clone(), ty);
518 }
519 PipelineNode::Let { name, value } => {
520 let ty = self.infer_expr_type(value)?;
521 self.active_schema.insert(name.clone(), ty);
522 }
523 }
524 Ok(())
525 }
526}
527
528impl SemanticAnalyzer {
529 fn schema_for_target(target: CollectionTarget) -> Vec<(&'static str, Type)> {
530 match target {
531 CollectionTarget::Containers => vec![
532 ("id", Type::String),
533 ("name", Type::String),
534 ("image", Type::String),
535 ("status", Type::String),
536 ("state", Type::String),
537 ("ports", Type::Array),
538 ("labels", Type::Array),
539 ("compose_project", Type::String),
540 ("created_at", Type::String),
541 ("started_at", Type::String),
542 ("finished_at", Type::String),
543 ("restart_count", Type::Integer),
544 ("cpu", Type::Float),
545 ("memory", Type::Integer),
546 ("memory_limit", Type::Integer),
547 ("network_rx", Type::Integer),
548 ("network_tx", Type::Integer),
549 ("disk_read", Type::Integer),
550 ("disk_write", Type::Integer),
551 ("health", Type::String),
552 ],
553 CollectionTarget::Images => vec![
554 ("id", Type::String),
555 ("repository", Type::String),
556 ("name", Type::String),
557 ("tag", Type::String),
558 ("digest", Type::String),
559 ("size", Type::String),
560 ("created_at", Type::String),
561 ("labels", Type::Array),
562 ],
563 CollectionTarget::Networks => vec![
564 ("id", Type::String),
565 ("name", Type::String),
566 ("driver", Type::String),
567 ("scope", Type::String),
568 ("containers", Type::Array),
569 ("labels", Type::Array),
570 ],
571 CollectionTarget::Volumes => vec![
572 ("name", Type::String),
573 ("driver", Type::String),
574 ("mountpoint", Type::String),
575 ("scope", Type::String),
576 ("labels", Type::Array),
577 ],
578 }
579 }
580}
581
582trait StartsPrefixLabel {
583 fn starts_prefix_label(&self) -> bool;
584}
585
586impl StartsPrefixLabel for str {
587 fn starts_prefix_label(&self) -> bool {
588 self.starts_with("label.")
589 }
590}
591
592impl StartsPrefixLabel for String {
593 fn starts_prefix_label(&self) -> bool {
594 self.as_str().starts_prefix_label()
595 }
596}
597
598pub fn validate_semantics(query: &Query) -> Result<(), EvalError> {
599 let target = match query {
600 Query::Observe(q) => q.target,
601 Query::Events(q) => q.target,
602 Query::Analyze(q) => match &q.target {
603 crate::ast::AnalysisTarget::Collection(t) => *t,
604 crate::ast::AnalysisTarget::Singular(s) => match s.kind {
605 crate::ast::SingularTargetKind::Container => CollectionTarget::Containers,
606 crate::ast::SingularTargetKind::Image => CollectionTarget::Images,
607 crate::ast::SingularTargetKind::Network => CollectionTarget::Networks,
608 crate::ast::SingularTargetKind::Volume => CollectionTarget::Volumes,
609 },
610 },
611 Query::Alert(_) | Query::Logs(_) | Query::Ping => CollectionTarget::Containers,
612 Query::Compose(q) => match q.target {
613 crate::ast::ComposeTarget::Networks => CollectionTarget::Networks,
614 crate::ast::ComposeTarget::Volumes => CollectionTarget::Volumes,
615 _ => CollectionTarget::Containers,
616 },
617 Query::Inspect(_) | Query::Fields(_) => return Ok(()),
618 };
619
620 let mut analyzer = SemanticAnalyzer::new(target);
621 analyzer.validate_query(query)
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::parser;
628
629 #[test]
630 fn test_valid_query() {
631 let parsed =
632 parser::parse("observe containers | where cpu > 50% | select name, cpu").unwrap();
633 let res = validate_semantics(&parsed.query);
634 assert!(res.is_ok());
635 }
636
637 #[test]
638 fn test_invalid_field() {
639 let parsed = parser::parse("observe containers | where invalid_field = 1").unwrap();
640 let res = validate_semantics(&parsed.query);
641 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
642 }
643
644 #[test]
645 fn test_invalid_comparison() {
646 let parsed = parser::parse("observe containers | where state > 50").unwrap();
647 let res = validate_semantics(&parsed.query);
648 assert!(matches!(res, Err(EvalError::InvalidComparison { .. })));
649 }
650
651 #[test]
652 fn test_invalid_arithmetic() {
653 let parsed = parser::parse("observe containers | set val = state + 1").unwrap();
654 let res = validate_semantics(&parsed.query);
655 assert!(matches!(res, Err(EvalError::Arithmetic(_))));
656 }
657
658 #[test]
659 fn test_unknown_function() {
660 let parsed =
661 parser::parse("observe containers | where invalid_fn(name) = \"test\"").unwrap();
662 let res = validate_semantics(&parsed.query);
663 assert!(matches!(res, Err(EvalError::UnknownFunction { .. })));
664 }
665
666 #[test]
667 fn test_set_field_inheritance() {
668 let parsed =
669 parser::parse("observe containers | set tier = \"prod\" | select name, tier").unwrap();
670 let res = validate_semantics(&parsed.query);
671 assert!(res.is_ok());
672 }
673
674 #[test]
675 fn test_label_lookup() {
676 let parsed =
677 parser::parse("observe containers | where label.env = \"production\"").unwrap();
678 let res = validate_semantics(&parsed.query);
679 assert!(res.is_ok());
680 }
681
682 #[test]
683 fn test_nested_if_branch() {
684 let parsed = parser::parse(
685 "observe containers | if cpu > 90% then set high = true else set high = false",
686 )
687 .unwrap();
688 let res = validate_semantics(&parsed.query);
689 assert!(res.is_ok());
690 }
691
692 #[test]
695 fn test_if_without_else() {
696 let parsed =
697 parser::parse("observe containers | if cpu > 90% then alert \"high\"").unwrap();
698 let res = validate_semantics(&parsed.query);
699 assert!(res.is_ok());
700 }
701
702 #[test]
703 fn test_if_complex_condition() {
704 let parsed = parser::parse(
705 "observe containers | if cpu > 80% and memory > 500 then alert \"high\" else alert \"low\"",
706 )
707 .unwrap();
708 let res = validate_semantics(&parsed.query);
709 assert!(res.is_ok());
710 }
711
712 #[test]
713 fn test_if_function_condition() {
714 let parsed =
715 parser::parse("observe containers | if upper(name) = \"API\" then set found = true")
716 .unwrap();
717 let res = validate_semantics(&parsed.query);
718 assert!(res.is_ok());
719 }
720
721 #[test]
722 fn test_else_if_chain() {
723 let parsed = parser::parse(
724 "observe containers | if cpu > 90% then alert \"critical\" else if cpu > 70% then alert \"warning\" else alert \"ok\"",
725 )
726 .unwrap();
727 let res = validate_semantics(&parsed.query);
728 assert!(res.is_ok());
729 }
730
731 #[test]
732 fn test_if_with_select_in_branch() {
733 let parsed =
734 parser::parse("observe containers | if state = running then select name, state, cpu")
735 .unwrap();
736 let res = validate_semantics(&parsed.query);
737 assert!(res.is_ok());
738 }
739
740 #[test]
741 fn test_if_with_sort_limit_in_branch() {
742 let parsed = parser::parse(
743 "observe containers | if state = running then sort by cpu desc | limit 10",
744 )
745 .unwrap();
746 let res = validate_semantics(&parsed.query);
747 assert!(res.is_ok());
748 }
749
750 #[test]
751 fn test_if_with_distinct_in_branch() {
752 let parsed =
753 parser::parse("observe containers | if state = running then distinct").unwrap();
754 let res = validate_semantics(&parsed.query);
755 assert!(res.is_ok());
756 }
757
758 #[test]
759 fn test_multiple_if_nodes() {
760 let parsed = parser::parse(
761 "observe containers | if cpu > 90% then alert \"high\" | if memory > 500 then alert \"high_mem\"",
762 )
763 .unwrap();
764 let res = validate_semantics(&parsed.query);
765 assert!(res.is_ok());
766 }
767
768 #[test]
769 fn test_if_with_expr_set_in_branch() {
770 let parsed = parser::parse(
771 "observe containers | if state = running then set val = restart_count + 1 else set val = restart_count",
772 )
773 .unwrap();
774 let res = validate_semantics(&parsed.query);
775 assert!(res.is_ok());
776 }
777
778 #[test]
779 fn test_if_with_where_in_branch() {
780 let parsed = parser::parse(
781 "observe containers | if state = running then where cpu > 80% | select name, cpu",
782 )
783 .unwrap();
784 let res = validate_semantics(&parsed.query);
785 assert!(res.is_ok());
786 }
787
788 #[test]
789 fn test_nested_if_inside_then() {
790 let parsed = parser::parse(
791 "observe containers | if cpu > 90% then if memory > 500 then alert \"high\"",
792 )
793 .unwrap();
794 let res = validate_semantics(&parsed.query);
795 assert!(res.is_ok());
796 }
797
798 #[test]
799 fn test_nested_if_inside_else() {
800 let parsed = parser::parse(
801 "observe containers | if cpu > 90% then set critical = true else if memory > 500 then set high_mem = true",
802 )
803 .unwrap();
804 let res = validate_semantics(&parsed.query);
805 assert!(res.is_ok());
806 }
807
808 #[test]
811 fn test_if_invalid_field_in_condition() {
812 let parsed =
813 parser::parse("observe containers | if nonexistent > 50 then alert \"bad\"").unwrap();
814 let res = validate_semantics(&parsed.query);
815 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
816 }
817
818 #[test]
819 fn test_if_invalid_comparison_in_condition() {
820 let parsed =
821 parser::parse("observe containers | if state > 50 then alert \"bad\"").unwrap();
822 let res = validate_semantics(&parsed.query);
823 assert!(matches!(res, Err(EvalError::InvalidComparison { .. })));
824 }
825
826 #[test]
827 fn test_if_set_then_select_in_same_branch() {
828 let parsed = parser::parse(
830 "observe containers | if cpu > 90% then set tier = \"high\" | select name, tier",
831 )
832 .unwrap();
833 let res = validate_semantics(&parsed.query);
834 assert!(res.is_ok());
835 }
836
837 #[test]
838 fn test_if_select_nonexistent_in_branch() {
839 let parsed =
841 parser::parse("observe containers | if cpu > 90% then select name, nonexistent")
842 .unwrap();
843 let res = validate_semantics(&parsed.query);
844 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
845 }
846
847 #[test]
848 fn test_if_invalid_field_in_branch() {
849 let parsed =
851 parser::parse("observe containers | if cpu > 90% then where nonexistent = 1").unwrap();
852 let res = validate_semantics(&parsed.query);
853 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
854 }
855
856 #[test]
857 fn test_if_invalid_arithmetic_in_branch_set() {
858 let parsed =
860 parser::parse("observe containers | if cpu > 90% then set val = state + 1").unwrap();
861 let res = validate_semantics(&parsed.query);
862 assert!(matches!(res, Err(EvalError::Arithmetic(_))));
863 }
864
865 #[test]
866 fn test_if_unknown_function_in_branch_where() {
867 let parsed = parser::parse(
869 "observe containers | if cpu > 90% then alert \"high\" else where bogus_fn(name) = \"x\"",
870 )
871 .unwrap();
872 let res = validate_semantics(&parsed.query);
873 assert!(matches!(res, Err(EvalError::UnknownFunction { .. })));
874 }
875
876 #[test]
877 fn test_invalid_and_or_types() {
878 let parsed = parser::parse("observe containers | where state and name").unwrap();
879 let res = validate_semantics(&parsed.query);
880 assert!(res.is_err());
881 }
882
883 #[test]
884 fn test_distinct_and_limit() {
885 let parsed = parser::parse("observe containers | distinct | limit 5").unwrap();
886 let res = validate_semantics(&parsed.query);
887 assert!(res.is_ok());
888 }
889
890 #[test]
891 fn test_group_by_fields() {
892 let parsed =
893 parser::parse("observe containers | group by state with count(id) as cnt").unwrap();
894 let res = validate_semantics(&parsed.query);
895 assert!(res.is_ok());
896 }
897
898 #[test]
899 fn test_other_collection_targets() {
900 let parsed = parser::parse("observe images | select name, size").unwrap();
901 let res = validate_semantics(&parsed.query);
902 assert!(res.is_ok());
903
904 let parsed = parser::parse("observe networks | select name, scope").unwrap();
905 let res = validate_semantics(&parsed.query);
906 assert!(res.is_ok());
907
908 let parsed = parser::parse("observe volumes | select name, scope").unwrap();
909 let res = validate_semantics(&parsed.query);
910 assert!(res.is_ok());
911 }
912
913 #[test]
914 fn test_compose_valid_query() {
915 let parsed = parser::parse("compose myapp | where cpu > 50% | select name, cpu").unwrap();
916 let res = validate_semantics(&parsed.query);
917 assert!(res.is_ok());
918 }
919
920 #[test]
921 fn test_compose_invalid_field() {
922 let parsed = parser::parse("compose myapp | where nonexistent_field = 1").unwrap();
923 let res = validate_semantics(&parsed.query);
924 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
925 }
926
927 #[test]
928 fn test_compose_services_service_field_available() {
929 let parsed = parser::parse("compose myapp services | select name, service").unwrap();
930 let res = validate_semantics(&parsed.query);
931 assert!(res.is_ok());
932 }
933
934 #[test]
935 fn test_compose_services_where_service() {
936 let parsed = parser::parse("compose myapp services | where service = \"api\"").unwrap();
937 let res = validate_semantics(&parsed.query);
938 assert!(res.is_ok());
939 }
940
941 #[test]
942 fn test_compose_set_and_select() {
943 let parsed =
944 parser::parse("compose myapp | set tier = \"prod\" | select name, tier").unwrap();
945 let res = validate_semantics(&parsed.query);
946 assert!(res.is_ok());
947 }
948
949 #[test]
950 fn test_compose_group_by() {
951 let parsed = parser::parse("compose myapp | group by state with count(id) as cnt").unwrap();
952 let res = validate_semantics(&parsed.query);
953 assert!(res.is_ok());
954 }
955
956 #[test]
957 fn test_compose_invalid_comparison() {
958 let parsed = parser::parse("compose myapp | where state > 50").unwrap();
959 let res = validate_semantics(&parsed.query);
960 assert!(matches!(res, Err(EvalError::InvalidComparison { .. })));
961 }
962
963 #[test]
964 fn test_compose_networks_valid_fields() {
965 let parsed = parser::parse("compose myapp networks | select name, driver").unwrap();
966 let res = validate_semantics(&parsed.query);
967 assert!(res.is_ok());
968 }
969
970 #[test]
971 fn test_compose_volumes_valid_fields() {
972 let parsed = parser::parse("compose myapp volumes | select name, driver").unwrap();
973 let res = validate_semantics(&parsed.query);
974 assert!(res.is_ok());
975 }
976
977 #[test]
978 fn test_compose_health_service_and_health_fields() {
979 let parsed = parser::parse("compose myapp health | select name, service, health").unwrap();
980 let res = validate_semantics(&parsed.query);
981 assert!(res.is_ok());
982 }
983
984 #[test]
985 fn test_compose_networks_invalid_field() {
986 let parsed = parser::parse("compose myapp networks | where cpu > 50").unwrap();
987 let res = validate_semantics(&parsed.query);
988 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
989 }
990
991 #[test]
992 fn test_compose_volumes_invalid_field() {
993 let parsed = parser::parse("compose myapp volumes | where state = \"running\"").unwrap();
994 let res = validate_semantics(&parsed.query);
995 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
996 }
997
998 #[test]
999 fn test_join_prefixed_fields_valid() {
1000 let parsed = parser::parse("observe containers join images on id = id | where c.image = \"nginx:latest\" | select c.name, i.name").unwrap();
1001 let res = validate_semantics(&parsed.query);
1002 assert!(res.is_ok());
1003 }
1004
1005 #[test]
1006 fn test_join_prefixed_field_rejects_non_prefixed() {
1007 let parsed =
1008 parser::parse("observe containers join images on id = id | where name = \"web\"")
1009 .unwrap();
1010 let res = validate_semantics(&parsed.query);
1011 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
1012 }
1013
1014 #[test]
1015 fn test_join_where_clause_invalid_field() {
1016 let parsed =
1017 parser::parse("observe containers join images on id = id | where nonexistent = \"x\"")
1018 .unwrap();
1019 let res = validate_semantics(&parsed.query);
1020 assert!(matches!(res, Err(EvalError::UnsupportedField { .. })));
1021 }
1022
1023 #[test]
1024 fn test_join_no_pipeline() {
1025 let parsed = parser::parse("observe containers join images on id = id").unwrap();
1026 let res = validate_semantics(&parsed.query);
1027 assert!(res.is_ok());
1028 }
1029}