1pub mod ast;
6pub mod string_escapes;
7
8use pest::Parser as PestParser;
9use pest_derive::Parser;
10
11pub use string_escapes::{escape_string_literal, unescape_string_literal};
12
13pub use ast::{
14 Aggregation, AstNode, CollectionOpNode, ComparisonNode, GeoExprNode, GroupBy, LogicalOpNode,
15 Mutator, NslookupExprNode, QueryWithStatsNode, StatsNode, UnaryOpNode, Value, VizParamValue,
16};
17
18use crate::error::{Result, TqlError};
19
20#[derive(Parser)]
22#[grammar = "parser/grammar.pest"]
23pub struct TqlPestParser;
24
25pub struct TqlParser {
27 max_depth: usize,
29}
30
31impl Default for TqlParser {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl TqlParser {
38 pub const MAX_QUERY_DEPTH: usize = 50;
40
41 pub fn new() -> Self {
43 Self {
44 max_depth: Self::MAX_QUERY_DEPTH,
45 }
46 }
47
48 pub fn with_max_depth(max_depth: usize) -> Self {
50 Self { max_depth }
51 }
52
53 pub fn parse(&self, query: &str) -> Result<AstNode> {
76 if query.trim().is_empty() {
78 return Ok(AstNode::MatchAll);
79 }
80
81 let pairs = TqlPestParser::parse(Rule::query, query).map_err(|e| {
83 let location = match &e.location {
84 pest::error::InputLocation::Pos(pos) => *pos,
85 pest::error::InputLocation::Span((start, _)) => *start,
86 };
87
88 TqlError::ParseError {
89 message: format!("Parse error: {}", e),
90 position: location,
91 query: Some(query.to_string()),
92 }
93 })?;
94
95 self.build_ast_from_pairs(pairs, 0)
97 }
98
99 fn build_ast_from_pairs(
101 &self,
102 mut pairs: pest::iterators::Pairs<Rule>,
103 depth: usize,
104 ) -> Result<AstNode> {
105 if depth > self.max_depth {
107 return Err(TqlError::SyntaxError {
108 message: format!(
109 "Query depth exceeds maximum allowed depth of {}",
110 self.max_depth
111 ),
112 position: Some(0),
113 query: None,
114 suggestions: vec![
115 "Reduce query nesting depth".to_string(),
116 "Split into multiple simpler queries".to_string(),
117 ],
118 });
119 }
120
121 if let Some(pair) = pairs.next() {
123 match pair.as_rule() {
124 Rule::query => {
125 let inner = pair.into_inner();
127 return self.build_ast_from_pairs(inner, depth);
128 }
129 Rule::query_with_stats => {
130 return self.parse_query_with_stats(pair, depth + 1);
131 }
132 Rule::stats_expr => {
133 return self.parse_stats_expr(pair, depth + 1);
134 }
135 Rule::logical_expr => {
136 return self.parse_logical_expr(pair, depth + 1);
137 }
138 _ => {
139 return Err(TqlError::ParseError {
140 message: format!("Unexpected rule: {:?}", pair.as_rule()),
141 position: 0,
142 query: None,
143 });
144 }
145 }
146 }
147
148 Ok(AstNode::MatchAll)
150 }
151
152 fn parse_query_with_stats(
154 &self,
155 pair: pest::iterators::Pair<Rule>,
156 depth: usize,
157 ) -> Result<AstNode> {
158 let mut inner = pair.into_inner();
159
160 let filter_pair = inner.next().ok_or_else(|| TqlError::ParseError {
162 message: "Missing filter expression in query_with_stats".to_string(),
163 position: 0,
164 query: None,
165 })?;
166 let filter = Box::new(self.parse_logical_expr(filter_pair, depth + 1)?);
167
168 let stats_pair = inner.next().ok_or_else(|| TqlError::ParseError {
170 message: "Missing stats expression in query_with_stats".to_string(),
171 position: 0,
172 query: None,
173 })?;
174
175 match self.parse_stats_expr(stats_pair, depth + 1)? {
177 AstNode::StatsExpr(stats) => Ok(AstNode::QueryWithStats(QueryWithStatsNode {
178 filter,
179 stats,
180 })),
181 _ => Err(TqlError::ParseError {
182 message: "Expected stats expression".to_string(),
183 position: 0,
184 query: None,
185 }),
186 }
187 }
188
189 fn parse_stats_expr(
191 &self,
192 pair: pest::iterators::Pair<Rule>,
193 _depth: usize,
194 ) -> Result<AstNode> {
195 let mut aggregations = Vec::new();
196 let mut group_by = Vec::new();
197 let mut viz_hint = None;
198 let mut viz_params = None;
199
200 for inner_pair in pair.into_inner() {
201 match inner_pair.as_rule() {
202 Rule::aggregation => {
203 aggregations.push(self.parse_aggregation(inner_pair)?);
204 }
205 Rule::group_by_list => {
206 group_by = self.parse_group_by_list(inner_pair)?;
207 }
208 Rule::viz_hint => {
209 let mut viz_inner = inner_pair.into_inner();
210 viz_hint = Some(
212 viz_inner
213 .next()
214 .ok_or_else(|| TqlError::ParseError {
215 message: "Missing viz hint identifier".to_string(),
216 position: 0,
217 query: None,
218 })?
219 .as_str()
220 .to_string(),
221 );
222 if let Some(params_pair) = viz_inner.next() {
224 if params_pair.as_rule() == Rule::viz_params {
225 let mut params = std::collections::HashMap::new();
226 for param_pair in params_pair.into_inner() {
227 if param_pair.as_rule() == Rule::viz_param {
228 let mut param_inner = param_pair.into_inner();
229 let key = param_inner
230 .next()
231 .ok_or_else(|| TqlError::ParseError {
232 message: "Missing viz param key".to_string(),
233 position: 0,
234 query: None,
235 })?
236 .as_str()
237 .to_string();
238 let value_pair =
239 param_inner.next().ok_or_else(|| TqlError::ParseError {
240 message: format!(
241 "Missing viz param value for key '{}'",
242 key
243 ),
244 position: 0,
245 query: None,
246 })?;
247 let value = Self::parse_viz_value(value_pair)?;
248 params.insert(key, value);
249 }
250 }
251 if !params.is_empty() {
252 viz_params = Some(params);
253 }
254 }
255 }
256 }
257 _ => {}
258 }
259 }
260
261 Ok(AstNode::StatsExpr(StatsNode {
262 aggregations,
263 group_by,
264 viz_hint,
265 viz_params,
266 }))
267 }
268
269 fn normalise_agg_alias(name: &str) -> String {
291 match name {
292 "avg" => "average".to_string(),
293 "med" => "median".to_string(),
294 other => other.to_string(),
295 }
296 }
297
298 fn parse_aggregation(&self, pair: pest::iterators::Pair<Rule>) -> Result<Aggregation> {
300 let mut function = String::new();
301 let mut field = None;
302 let mut alias = None;
303 let mut modifier = None;
304 let mut limit = None;
305 let mut percentile_values = None;
306 let mut rank_values = None;
307 let mut field_mutators = None;
308
309 for inner_pair in pair.into_inner() {
310 match inner_pair.as_rule() {
311 Rule::agg_func_name => {
312 function = Self::normalise_agg_alias(&inner_pair.as_str().to_lowercase());
313 }
314 Rule::agg_field => {
315 let field_str = inner_pair.as_str();
316 if field_str != "*" {
317 let mut field_name = String::new();
319 let mut mutators = Vec::new();
320
321 for field_inner in inner_pair.into_inner() {
340 match field_inner.as_rule() {
341 Rule::field_with_mutators => {
342 for fwm_inner in field_inner.into_inner() {
343 match fwm_inner.as_rule() {
344 Rule::field_name => {
345 field_name = fwm_inner.as_str().to_string();
346 }
347 Rule::mutator => {
348 mutators.push(self.parse_mutator(fwm_inner)?);
349 }
350 _ => {}
351 }
352 }
353 }
354 Rule::field_name => {
355 field_name = field_inner.as_str().to_string();
356 }
357 Rule::mutator => {
358 mutators.push(self.parse_mutator(field_inner)?);
359 }
360 _ => {}
361 }
362 }
363
364 field = Some(field_name);
365 if !mutators.is_empty() {
366 field_mutators = Some(mutators);
367 }
368 } else {
369 field = Some("*".to_string());
370 }
371 }
372 Rule::field_with_mutators => {
373 let (f, fm, _) = self.parse_field_with_mutators(inner_pair)?;
395 field = Some(f);
396 if fm.is_some() {
397 field_mutators = fm;
398 }
399 }
400 Rule::agg_modifier => {
401 let mod_text = inner_pair.as_str().to_lowercase();
411 if mod_text.starts_with("top") {
412 modifier = Some("top".to_string());
413 } else if mod_text.starts_with("bottom") {
414 modifier = Some("bottom".to_string());
415 }
416 for mod_inner in inner_pair.into_inner() {
447 if mod_inner.as_rule() == Rule::integer {
448 let raw = mod_inner.as_str();
449 let position = mod_inner.as_span().start();
450 limit =
451 Some(raw.parse::<usize>().map_err(|_| TqlError::ParseError {
452 message: format!(
453 "'{raw}' is not a usable bucket count for a \
454 'top'/'bottom' modifier: the count must be a whole \
455 number from 0 to {}, so it can be neither negative nor \
456 larger than this engine can index. Write a positive \
457 count, or drop the modifier.",
458 usize::MAX
459 ),
460 position,
461 query: None,
462 })?);
463 }
464 }
465 }
466 Rule::percentile_values => {
467 let values = inner_pair
482 .into_inner()
483 .filter(|p| p.as_rule() == Rule::number)
484 .map(|p| {
485 p.as_str().parse::<f64>().map_err(|_| TqlError::ParseError {
486 message: format!("Invalid number in value list: {}", p.as_str()),
487 position: p.as_span().start(),
488 query: None,
489 })
490 })
491 .collect::<Result<Vec<f64>>>()?;
492 percentile_values = Some(values);
493 }
494 Rule::identifier => {
495 alias = Some(inner_pair.as_str().to_string());
497 }
498 _ => {}
499 }
500 }
501
502 if matches!(
511 function.as_str(),
512 "percentile_rank" | "percentile_ranks" | "pct_rank" | "pct_ranks"
513 ) {
514 rank_values = percentile_values.take();
515 }
516
517 Ok(Aggregation {
518 function,
519 field,
520 alias,
521 modifier,
522 limit,
523 percentile_values,
524 rank_values,
525 field_mutators,
526 })
527 }
528
529 fn parse_group_by_list(&self, pair: pest::iterators::Pair<Rule>) -> Result<Vec<GroupBy>> {
531 let mut group_by = Vec::new();
532
533 for inner_pair in pair.into_inner() {
534 if inner_pair.as_rule() == Rule::group_by_field {
535 let mut field = String::new();
536 let mut bucket_size = None;
537
538 for field_inner in inner_pair.into_inner() {
539 match field_inner.as_rule() {
540 Rule::field_with_mutators => {
541 for fwm_inner in field_inner.into_inner() {
543 if fwm_inner.as_rule() == Rule::field_name {
544 field = fwm_inner.as_str().to_string();
545 break;
546 }
547 }
548 }
549 Rule::integer => {
550 let raw = field_inner.as_str();
566 let position = field_inner.as_span().start();
567 let whose = if field.is_empty() {
568 "a group-by field".to_string()
569 } else {
570 format!("group-by field '{field}'")
571 };
572 bucket_size =
573 Some(raw.parse::<usize>().map_err(|_| TqlError::ParseError {
574 message: format!(
575 "'top {raw}' on {whose} is not a usable bucket count: the \
576 count must be a whole number from 0 to {}, so it can be \
577 neither negative nor larger than this engine can index. \
578 Write a positive count, or drop the modifier.",
579 usize::MAX
580 ),
581 position,
582 query: None,
583 })?);
584 }
585 _ => {}
586 }
587 }
588
589 group_by.push(GroupBy { field, bucket_size });
590 }
591 }
592
593 Ok(group_by)
594 }
595
596 fn parse_logical_expr(
598 &self,
599 pair: pest::iterators::Pair<Rule>,
600 depth: usize,
601 ) -> Result<AstNode> {
602 let mut inner = pair.into_inner();
603
604 let first_term_pair = inner.next().ok_or_else(|| TqlError::ParseError {
606 message: "Missing term in logical expression".to_string(),
607 position: 0,
608 query: None,
609 })?;
610 let mut terms = vec![self.parse_term(first_term_pair, depth + 1)?];
613 let mut operators: Vec<String> = Vec::new();
614
615 while let Some(op_pair) = inner.next() {
616 if op_pair.as_rule() != Rule::logical_op {
617 return Err(TqlError::ParseError {
618 message: "Expected logical operator".to_string(),
619 position: 0,
620 query: None,
621 });
622 }
623
624 let operator = self.normalize_operator(op_pair.as_str());
630
631 let right_pair = inner.next().ok_or_else(|| TqlError::ParseError {
632 message: "Missing right operand after logical operator".to_string(),
633 position: 0,
634 query: None,
635 })?;
636 terms.push(self.parse_term(right_pair, depth + 1)?);
637 operators.push(operator);
638 }
639
640 Ok(Self::fold_with_precedence(terms, operators))
641 }
642
643 fn fold_with_precedence(terms: Vec<AstNode>, operators: Vec<String>) -> AstNode {
664 debug_assert_eq!(terms.len(), operators.len() + 1);
665
666 let is_and = |op: &str| matches!(op, "and" | "&&");
667
668 let mut iter = terms.into_iter();
670 let mut current = iter.next().expect("logical_expr always has one term");
671 let mut or_operands: Vec<AstNode> = Vec::new();
672 let mut or_operators: Vec<String> = Vec::new();
673
674 for (operator, term) in operators.into_iter().zip(iter) {
675 if is_and(&operator) {
676 current = AstNode::LogicalOp(LogicalOpNode {
677 operator,
678 left: Box::new(current),
679 right: Box::new(term),
680 });
681 } else {
682 or_operands.push(current);
683 or_operators.push(operator);
684 current = term;
685 }
686 }
687 or_operands.push(current);
688
689 let mut result_iter = or_operands.into_iter();
691 let mut result = result_iter.next().expect("at least one operand");
692 for (operator, operand) in or_operators.into_iter().zip(result_iter) {
693 result = AstNode::LogicalOp(LogicalOpNode {
694 operator,
695 left: Box::new(result),
696 right: Box::new(operand),
697 });
698 }
699 result
700 }
701
702 fn parse_term(&self, pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<AstNode> {
704 match pair.as_rule() {
705 Rule::term => {
706 let inner_pair = pair
708 .into_inner()
709 .next()
710 .ok_or_else(|| TqlError::ParseError {
711 message: "Empty term".to_string(),
712 position: 0,
713 query: None,
714 })?;
715 self.parse_term(inner_pair, depth + 1)
716 }
717 Rule::not_expr => {
718 let mut inner = pair.into_inner();
719 let _op = inner.next(); let operand_pair = inner.next().ok_or_else(|| TqlError::ParseError {
721 message: "Missing operand after NOT".to_string(),
722 position: 0,
723 query: None,
724 })?;
725 let operand = self.parse_term(operand_pair, depth + 1)?;
726
727 Ok(AstNode::UnaryOp(UnaryOpNode {
728 operator: "not".to_string(),
729 operand: Box::new(operand),
730 }))
731 }
732 Rule::primary => self.parse_primary(pair, depth + 1),
733 _ => Err(TqlError::ParseError {
734 message: format!("Unexpected rule in term: {:?}", pair.as_rule()),
735 position: 0,
736 query: None,
737 }),
738 }
739 }
740
741 fn parse_primary(&self, pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<AstNode> {
743 match pair.as_rule() {
744 Rule::primary => {
745 let inner_pair = pair
746 .into_inner()
747 .next()
748 .ok_or_else(|| TqlError::ParseError {
749 message: "Empty primary".to_string(),
750 position: 0,
751 query: None,
752 })?;
753 self.parse_primary(inner_pair, depth + 1)
754 }
755 Rule::paren_expr => {
756 let inner_pair = pair
757 .into_inner()
758 .next()
759 .ok_or_else(|| TqlError::ParseError {
760 message: "Empty parenthesized expression".to_string(),
761 position: 0,
762 query: None,
763 })?;
764 self.parse_logical_expr(inner_pair, depth + 1)
765 }
766 Rule::comparison => self.parse_comparison(pair, depth + 1),
767 _ => Err(TqlError::ParseError {
768 message: format!("Unexpected rule in primary: {:?}", pair.as_rule()),
769 position: 0,
770 query: None,
771 }),
772 }
773 }
774
775 fn parse_comparison(
777 &self,
778 pair: pest::iterators::Pair<Rule>,
779 _depth: usize,
780 ) -> Result<AstNode> {
781 let inner_pair = pair
782 .into_inner()
783 .next()
784 .ok_or_else(|| TqlError::ParseError {
785 message: "Empty comparison".to_string(),
786 position: 0,
787 query: None,
788 })?;
789
790 match inner_pair.as_rule() {
791 Rule::collection_comparison => self.parse_collection_comparison(inner_pair),
792 Rule::between_comparison => self.parse_between_comparison(inner_pair),
793 Rule::in_fields_comparison | Rule::in_field_comparison => {
794 self.parse_in_fields_comparison(inner_pair)
795 }
796 Rule::is_null_comparison => self.parse_is_null_comparison(inner_pair),
797 Rule::unary_comparison => self.parse_unary_comparison(inner_pair),
798 Rule::binary_comparison => self.parse_binary_comparison(inner_pair),
799 Rule::field_only_expression => self.parse_field_only_expression(inner_pair),
800 _ => Err(TqlError::ParseError {
801 message: format!("Unknown comparison type: {:?}", inner_pair.as_rule()),
802 position: 0,
803 query: None,
804 }),
805 }
806 }
807
808 fn parse_collection_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
810 let mut inner = pair.into_inner();
811
812 let first = inner.next().ok_or_else(|| TqlError::ParseError {
813 message: "Missing collection comparison element".to_string(),
814 position: 0,
815 query: None,
816 })?;
817
818 let (operator, field, field_mutators, type_hint) = match first.as_rule() {
820 Rule::collection_op => {
821 let op = self.normalize_operator(first.as_str());
823 let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
824 message: "Missing field in collection comparison".to_string(),
825 position: 0,
826 query: None,
827 })?;
828 let (f, fm, th) = self.parse_field_with_mutators(field_pair)?;
829 (op, f, fm, th)
830 }
831 Rule::field_with_mutators => {
832 let (f, fm, th) = self.parse_field_with_mutators(first)?;
834 let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
835 message: "Missing collection operator".to_string(),
836 position: 0,
837 query: None,
838 })?;
839 let op = self.normalize_operator(op_pair.as_str());
840 (op, f, fm, th)
841 }
842 _ => {
843 return Err(TqlError::ParseError {
844 message: format!(
845 "Unexpected rule in collection comparison: {:?}",
846 first.as_rule()
847 ),
848 position: 0,
849 query: None,
850 });
851 }
852 };
853
854 let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
856 message: "Missing comparison operator or value in collection comparison".to_string(),
857 position: 0,
858 query: None,
859 })?;
860
861 let (comparison_operator, value) = match next_pair.as_rule() {
862 Rule::comparison_op => {
863 let comp_op = self.normalize_operator(next_pair.as_str());
864 let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
865 message: "Missing value in collection comparison".to_string(),
866 position: 0,
867 query: None,
868 })?;
869 let (val, _value_mutators) = self.parse_value_with_mutators(value_pair)?;
870 (comp_op, val)
871 }
872 Rule::value_with_mutators => {
873 let (val, _value_mutators) = self.parse_value_with_mutators(next_pair)?;
875 ("eq".to_string(), val)
876 }
877 _ => {
878 return Err(TqlError::ParseError {
879 message: format!(
880 "Unexpected rule in collection comparison: {:?}",
881 next_pair.as_rule()
882 ),
883 position: 0,
884 query: None,
885 });
886 }
887 };
888
889 Ok(AstNode::CollectionOp(CollectionOpNode {
890 operator,
891 field,
892 comparison_operator,
893 value,
894 field_mutators,
895 type_hint,
896 }))
897 }
898
899 fn parse_between_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
901 let mut inner = pair.into_inner();
902
903 let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
904 message: "Missing field in between comparison".to_string(),
905 position: 0,
906 query: None,
907 })?;
908 let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
909
910 let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
911 message: "Missing operator in between comparison".to_string(),
912 position: 0,
913 query: None,
914 })?;
915 let operator = self.normalize_operator(op_pair.as_str());
916
917 let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
918 message: "Missing value in between comparison".to_string(),
919 position: 0,
920 query: None,
921 })?;
922
923 let value = match next_pair.as_rule() {
925 Rule::list_value => self.parse_list(next_pair)?,
926 Rule::value => {
927 let first = self.parse_value(next_pair)?;
929 let second_pair = inner.next().ok_or_else(|| TqlError::ParseError {
930 message: "Missing second value in between X and Y".to_string(),
931 position: 0,
932 query: None,
933 })?;
934 let second = self.parse_value(second_pair)?;
935 Value::List(vec![first, second])
936 }
937 _ => {
938 return Err(TqlError::ParseError {
939 message: format!(
940 "Unexpected rule in between comparison: {:?}",
941 next_pair.as_rule()
942 ),
943 position: 0,
944 query: None,
945 });
946 }
947 };
948
949 Ok(AstNode::Comparison(ComparisonNode {
950 field,
951 operator,
952 value: Some(value),
953 field_mutators,
954 value_mutators: None,
955 type_hint,
956 }))
957 }
958
959 fn parse_in_fields_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
961 let mut inner = pair.into_inner();
962
963 let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
964 message: "Missing value in 'value in field' comparison".to_string(),
965 position: 0,
966 query: None,
967 })?;
968 let (value, value_mutators) = self.parse_value_with_mutators(value_pair)?;
969
970 let _op_pair = inner.next(); let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
973 message: "Missing field/list in 'value in ...' comparison".to_string(),
974 position: 0,
975 query: None,
976 })?;
977
978 match next_pair.as_rule() {
979 Rule::in_fields_list => {
980 let fields: Vec<String> = next_pair
983 .into_inner()
984 .filter(|p| p.as_rule() == Rule::field_name)
985 .map(|p| p.as_str().to_string())
986 .collect();
987
988 if fields.is_empty() {
989 return Ok(AstNode::MatchAll);
990 }
991
992 let mut result = AstNode::Comparison(ComparisonNode {
994 field: fields[0].clone(),
995 operator: "eq".to_string(),
996 value: Some(value.clone()),
997 field_mutators: None,
998 value_mutators: value_mutators.clone(),
999 type_hint: None,
1000 });
1001
1002 for field in &fields[1..] {
1003 let right = AstNode::Comparison(ComparisonNode {
1004 field: field.clone(),
1005 operator: "eq".to_string(),
1006 value: Some(value.clone()),
1007 field_mutators: None,
1008 value_mutators: value_mutators.clone(),
1009 type_hint: None,
1010 });
1011 result = AstNode::LogicalOp(LogicalOpNode {
1012 operator: "or".to_string(),
1013 left: Box::new(result),
1014 right: Box::new(right),
1015 });
1016 }
1017
1018 Ok(result)
1019 }
1020 Rule::field_with_mutators => {
1021 let (field, field_mutators, type_hint) =
1023 self.parse_field_with_mutators(next_pair)?;
1024 Ok(AstNode::Comparison(ComparisonNode {
1025 field,
1026 operator: "contains".to_string(),
1027 value: Some(value),
1028 field_mutators,
1029 value_mutators,
1030 type_hint,
1031 }))
1032 }
1033 _ => Err(TqlError::ParseError {
1034 message: format!(
1035 "Unexpected rule in in_fields_comparison: {:?}",
1036 next_pair.as_rule()
1037 ),
1038 position: 0,
1039 query: None,
1040 }),
1041 }
1042 }
1043
1044 fn parse_is_null_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
1046 let mut inner = pair.into_inner();
1047
1048 let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1049 message: "Missing field in is null comparison".to_string(),
1050 position: 0,
1051 query: None,
1052 })?;
1053 let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
1054
1055 let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1056 message: "Missing operator in is null comparison".to_string(),
1057 position: 0,
1058 query: None,
1059 })?;
1060 let operator = self.normalize_operator(op_pair.as_str());
1061
1062 Ok(AstNode::Comparison(ComparisonNode {
1063 field,
1064 operator,
1065 value: Some(Value::Null),
1066 field_mutators,
1067 value_mutators: None,
1068 type_hint,
1069 }))
1070 }
1071
1072 fn parse_unary_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
1074 let mut inner = pair.into_inner();
1075
1076 let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1077 message: "Missing field in unary comparison".to_string(),
1078 position: 0,
1079 query: None,
1080 })?;
1081 let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
1082
1083 let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1084 message: "Missing operator in unary comparison".to_string(),
1085 position: 0,
1086 query: None,
1087 })?;
1088 let operator = self.normalize_operator(op_pair.as_str());
1089
1090 Ok(AstNode::Comparison(ComparisonNode {
1091 field,
1092 operator,
1093 value: None,
1094 field_mutators,
1095 value_mutators: None,
1096 type_hint,
1097 }))
1098 }
1099
1100 fn parse_binary_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
1102 let mut inner = pair.into_inner();
1103
1104 let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1105 message: "Missing field in binary comparison".to_string(),
1106 position: 0,
1107 query: None,
1108 })?;
1109 let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
1110
1111 let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1112 message: "Missing operator in binary comparison".to_string(),
1113 position: 0,
1114 query: None,
1115 })?;
1116 let operator = self.normalize_operator(op_pair.as_str());
1117
1118 let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1119 message: "Missing value in binary comparison".to_string(),
1120 position: 0,
1121 query: None,
1122 })?;
1123 let (value, value_mutators) = self.parse_value_with_mutators(value_pair)?;
1124
1125 Ok(AstNode::Comparison(ComparisonNode {
1126 field,
1127 operator,
1128 value: Some(value),
1129 field_mutators,
1130 value_mutators,
1131 type_hint,
1132 }))
1133 }
1134
1135 fn parse_field_only_expression(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
1138 let field_pair = pair
1140 .into_inner()
1141 .next()
1142 .ok_or_else(|| TqlError::ParseError {
1143 message: "Missing field in field-only expression".to_string(),
1144 position: 0,
1145 query: None,
1146 })?;
1147
1148 let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
1149
1150 let last_is_predicate = field_mutators
1168 .as_ref()
1169 .and_then(|m| m.last())
1170 .is_some_and(|m| crate::mutators::returns_boolean(&m.name));
1171
1172 let (operator, value) = if last_is_predicate {
1173 ("eq".to_string(), Some(Value::Boolean(true)))
1174 } else {
1175 ("exists".to_string(), None)
1176 };
1177
1178 Ok(AstNode::Comparison(ComparisonNode {
1179 field,
1180 operator,
1181 value,
1182 field_mutators,
1183 value_mutators: None,
1184 type_hint,
1185 }))
1186 }
1187
1188 fn parse_field_with_mutators(
1190 &self,
1191 pair: pest::iterators::Pair<Rule>,
1192 ) -> Result<(String, Option<Vec<Mutator>>, Option<String>)> {
1193 let mut field = String::new();
1194 let mut mutators = Vec::new();
1195 let mut type_hint = None;
1196
1197 for inner_pair in pair.into_inner() {
1198 match inner_pair.as_rule() {
1199 Rule::field_name => {
1200 field = inner_pair.as_str().to_string();
1201 }
1202 Rule::mutator => {
1203 mutators.push(self.parse_mutator(inner_pair)?);
1204 }
1205 Rule::type_hint => {
1206 for type_inner in inner_pair.into_inner() {
1207 if type_inner.as_rule() == Rule::type_name {
1208 type_hint = Some(type_inner.as_str().to_lowercase());
1209 }
1210 }
1211 }
1212 _ => {}
1213 }
1214 }
1215
1216 let field_mutators = if mutators.is_empty() {
1217 None
1218 } else {
1219 Some(mutators)
1220 };
1221 Ok((field, field_mutators, type_hint))
1222 }
1223
1224 fn parse_value_with_mutators(
1226 &self,
1227 pair: pest::iterators::Pair<Rule>,
1228 ) -> Result<(Value, Option<Vec<Mutator>>)> {
1229 let mut value = Value::Null;
1230 let mut mutators = Vec::new();
1231
1232 for inner_pair in pair.into_inner() {
1233 match inner_pair.as_rule() {
1234 Rule::value => {
1235 value = self.parse_value(inner_pair)?;
1236 }
1237 Rule::mutator => {
1238 mutators.push(self.parse_mutator(inner_pair)?);
1239 }
1240 _ => {}
1241 }
1242 }
1243
1244 let value_mutators = if mutators.is_empty() {
1245 None
1246 } else {
1247 Some(mutators)
1248 };
1249 Ok((value, value_mutators))
1250 }
1251
1252 fn parse_value(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
1254 let inner_pair = pair
1255 .into_inner()
1256 .next()
1257 .ok_or_else(|| TqlError::ParseError {
1258 message: "Empty value".to_string(),
1259 position: 0,
1260 query: None,
1261 })?;
1262
1263 match inner_pair.as_rule() {
1264 Rule::string => self.parse_string(inner_pair),
1265 Rule::cidr_value | Rule::ip_value => {
1266 Ok(Value::String(inner_pair.as_str().to_string()))
1268 }
1269 Rule::number => self.parse_number(inner_pair),
1270 Rule::boolean => Ok(Value::Boolean(inner_pair.as_str().to_lowercase() == "true")),
1271 Rule::null => Ok(Value::Null),
1272 Rule::list_value => self.parse_list(inner_pair),
1273 Rule::identifier => {
1274 Ok(Value::String(inner_pair.as_str().to_string()))
1277 }
1278 _ => Err(TqlError::ParseError {
1279 message: format!("Unknown value type: {:?}", inner_pair.as_rule()),
1280 position: 0,
1281 query: None,
1282 }),
1283 }
1284 }
1285
1286 fn parse_string(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
1288 let string_pair = pair
1290 .into_inner()
1291 .next()
1292 .ok_or_else(|| TqlError::ParseError {
1293 message: "Empty string rule".to_string(),
1294 position: 0,
1295 query: None,
1296 })?;
1297
1298 let inner_pair = string_pair
1300 .into_inner()
1301 .next()
1302 .ok_or_else(|| TqlError::ParseError {
1303 message: "No inner string content".to_string(),
1304 position: 0,
1305 query: None,
1306 })?;
1307
1308 let content = inner_pair.as_str();
1310
1311 Ok(Value::String(unescape_string_literal(content)))
1312 }
1313
1314 fn parse_number(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
1316 let inner_pair = pair
1317 .into_inner()
1318 .next()
1319 .ok_or_else(|| TqlError::ParseError {
1320 message: "Empty number".to_string(),
1321 position: 0,
1322 query: None,
1323 })?;
1324
1325 match inner_pair.as_rule() {
1326 Rule::float => {
1327 let f = inner_pair
1328 .as_str()
1329 .parse::<f64>()
1330 .map_err(|_| TqlError::ParseError {
1331 message: format!("Invalid float: {}", inner_pair.as_str()),
1332 position: 0,
1333 query: None,
1334 })?;
1335 Ok(Value::Float(f))
1336 }
1337 Rule::integer => {
1338 let i = inner_pair
1339 .as_str()
1340 .parse::<i64>()
1341 .map_err(|_| TqlError::ParseError {
1342 message: format!("Invalid integer: {}", inner_pair.as_str()),
1343 position: 0,
1344 query: None,
1345 })?;
1346 Ok(Value::Integer(i))
1347 }
1348 _ => Err(TqlError::ParseError {
1349 message: format!("Unknown number type: {:?}", inner_pair.as_rule()),
1350 position: 0,
1351 query: None,
1352 }),
1353 }
1354 }
1355
1356 fn parse_list(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
1358 let mut values = Vec::new();
1359
1360 for inner_pair in pair.into_inner() {
1361 if inner_pair.as_rule() == Rule::value {
1362 values.push(self.parse_value(inner_pair)?);
1363 }
1364 }
1365
1366 Ok(Value::List(values))
1367 }
1368
1369 fn parse_mutator(&self, pair: pest::iterators::Pair<Rule>) -> Result<Mutator> {
1371 let mut name = String::new();
1372 let mut args = Vec::new();
1373 let mut named_args = std::collections::HashMap::new();
1374
1375 for inner_pair in pair.into_inner() {
1376 match inner_pair.as_rule() {
1377 Rule::mutator_name => {
1378 name = inner_pair.as_str().to_string();
1379 }
1380 Rule::mutator_args => {
1381 for arg_pair in inner_pair.into_inner() {
1382 if arg_pair.as_rule() == Rule::mutator_arg {
1383 let mut inner = arg_pair.into_inner();
1385 let first = inner.next().ok_or_else(|| TqlError::ParseError {
1386 message: "Empty mutator argument".to_string(),
1387 position: 0,
1388 query: None,
1389 })?;
1390 if first.as_rule() == Rule::mutator_named_arg {
1391 let (key, val) = self.parse_mutator_named_arg(first)?;
1392 named_args.insert(key, val);
1393 } else {
1394 args.push(self.parse_value_from_rule(first)?);
1395 }
1396 }
1397 }
1398 }
1399 _ => {}
1400 }
1401 }
1402
1403 Ok(Mutator {
1404 name,
1405 args,
1406 named_args,
1407 })
1408 }
1409
1410 fn parse_mutator_named_arg(
1412 &self,
1413 pair: pest::iterators::Pair<Rule>,
1414 ) -> Result<(String, Value)> {
1415 let mut inner = pair.into_inner();
1416 let key = inner
1417 .next()
1418 .ok_or_else(|| TqlError::ParseError {
1419 message: "Missing named arg key".to_string(),
1420 position: 0,
1421 query: None,
1422 })?
1423 .as_str()
1424 .to_string();
1425 let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
1426 message: "Missing named arg value".to_string(),
1427 position: 0,
1428 query: None,
1429 })?;
1430 let value = self.parse_value_from_rule(value_pair)?;
1431 Ok((key, value))
1432 }
1433
1434 fn parse_value_from_rule(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
1436 match pair.as_rule() {
1437 Rule::string => self.parse_string(pair),
1438 Rule::number => self.parse_number(pair),
1439 Rule::boolean => Ok(Value::Boolean(pair.as_str().to_lowercase() == "true")),
1440 Rule::null => Ok(Value::Null),
1441 Rule::identifier => Ok(Value::String(pair.as_str().to_string())),
1442 _ => Err(TqlError::ParseError {
1443 message: format!("Invalid mutator argument type: {:?}", pair.as_rule()),
1444 position: 0,
1445 query: None,
1446 }),
1447 }
1448 }
1449
1450 fn normalize_operator(&self, op: &str) -> String {
1452 let normalized = op.to_lowercase().replace(' ', "_");
1453
1454 match normalized.as_str() {
1456 "=" => return "eq".to_string(),
1457 "!=" => return "ne".to_string(),
1458 ">" => return "gt".to_string(),
1459 ">=" => return "gte".to_string(),
1460 "<" => return "lt".to_string(),
1461 "<=" => return "lte".to_string(),
1462 "&&" => return "and".to_string(),
1463 "||" => return "or".to_string(),
1464 "!" => return "not".to_string(),
1465 _ => {}
1466 }
1467
1468 let (negated, base) = if let Some(rest) = normalized.strip_prefix('!') {
1470 (true, rest.to_string())
1471 } else if let Some(rest) = normalized.strip_prefix("not_") {
1472 (true, rest.to_string())
1473 } else {
1474 (false, normalized)
1475 };
1476
1477 let base = match base.as_str() {
1479 "regex" | "regexp" => "matches".to_string(),
1480 _ => base,
1481 };
1482
1483 if negated {
1484 format!("not_{}", base)
1485 } else {
1486 base
1487 }
1488 }
1489
1490 pub fn extract_fields(&self, query: &str) -> Result<Vec<String>> {
1508 let ast = self.parse(query)?;
1509 let mut fields = Vec::new();
1510 self.collect_fields(&ast, &mut fields);
1511 fields.sort();
1512 fields.dedup();
1513 Ok(fields)
1514 }
1515
1516 #[allow(clippy::only_used_in_recursion)]
1518 fn collect_fields(&self, node: &AstNode, fields: &mut Vec<String>) {
1519 match node {
1520 AstNode::Comparison(comp) => {
1521 fields.push(comp.field.clone());
1522 }
1523 AstNode::LogicalOp(logical) => {
1524 self.collect_fields(&logical.left, fields);
1525 self.collect_fields(&logical.right, fields);
1526 }
1527 AstNode::UnaryOp(unary) => {
1528 self.collect_fields(&unary.operand, fields);
1529 }
1530 AstNode::CollectionOp(coll) => {
1531 fields.push(coll.field.clone());
1532 }
1533 AstNode::GeoExpr(geo) => {
1534 fields.push(geo.field.clone());
1535 if let Some(ref cond) = geo.conditions {
1536 self.collect_fields(cond, fields);
1537 }
1538 }
1539 AstNode::NslookupExpr(nslookup) => {
1540 fields.push(nslookup.field.clone());
1541 if let Some(ref cond) = nslookup.conditions {
1542 self.collect_fields(cond, fields);
1543 }
1544 }
1545 AstNode::QueryWithStats(qws) => {
1546 self.collect_fields(&qws.filter, fields);
1547 for agg in &qws.stats.aggregations {
1548 if let Some(ref field) = agg.field {
1549 if field != "*" {
1550 fields.push(field.clone());
1551 }
1552 }
1553 }
1554 for group_by in &qws.stats.group_by {
1555 fields.push(group_by.field.clone());
1556 }
1557 }
1558 AstNode::StatsExpr(stats) => {
1559 for agg in &stats.aggregations {
1560 if let Some(ref field) = agg.field {
1561 if field != "*" {
1562 fields.push(field.clone());
1563 }
1564 }
1565 }
1566 for group_by in &stats.group_by {
1567 fields.push(group_by.field.clone());
1568 }
1569 }
1570 AstNode::MatchAll => {}
1571 }
1572 }
1573
1574 fn parse_viz_value(pair: pest::iterators::Pair<Rule>) -> Result<VizParamValue> {
1576 let inner = pair
1578 .into_inner()
1579 .next()
1580 .ok_or_else(|| TqlError::ParseError {
1581 message: "Empty viz value".to_string(),
1582 position: 0,
1583 query: None,
1584 })?;
1585 match inner.as_rule() {
1586 Rule::string => {
1587 let string_inner = inner.into_inner().next().unwrap();
1589 let content = string_inner
1590 .into_inner()
1591 .next()
1592 .map(|p| p.as_str().to_string())
1593 .unwrap_or_default();
1594 Ok(VizParamValue::String(content))
1595 }
1596 Rule::number => {
1597 let num_inner = inner.into_inner().next().unwrap();
1598 match num_inner.as_rule() {
1599 Rule::float => {
1600 let f: f64 =
1601 num_inner
1602 .as_str()
1603 .parse()
1604 .map_err(|_| TqlError::ParseError {
1605 message: format!("Invalid float: {}", num_inner.as_str()),
1606 position: 0,
1607 query: None,
1608 })?;
1609 Ok(VizParamValue::Float(f))
1610 }
1611 Rule::integer => {
1612 let i: i64 =
1613 num_inner
1614 .as_str()
1615 .parse()
1616 .map_err(|_| TqlError::ParseError {
1617 message: format!("Invalid integer: {}", num_inner.as_str()),
1618 position: 0,
1619 query: None,
1620 })?;
1621 Ok(VizParamValue::Integer(i))
1622 }
1623 _ => Err(TqlError::ParseError {
1624 message: format!("Unexpected number type: {:?}", num_inner.as_rule()),
1625 position: 0,
1626 query: None,
1627 }),
1628 }
1629 }
1630 Rule::boolean => {
1631 let b = inner.as_str().eq_ignore_ascii_case("true");
1632 Ok(VizParamValue::Boolean(b))
1633 }
1634 Rule::identifier => {
1635 Ok(VizParamValue::String(inner.as_str().to_string()))
1637 }
1638 _ => Err(TqlError::ParseError {
1639 message: format!("Unexpected viz value type: {:?}", inner.as_rule()),
1640 position: 0,
1641 query: None,
1642 }),
1643 }
1644 }
1645}
1646
1647#[cfg(test)]
1648mod tests {
1649 use super::*;
1650
1651 #[test]
1652 fn test_parser_creation() {
1653 let parser = TqlParser::new();
1654 assert_eq!(parser.max_depth, TqlParser::MAX_QUERY_DEPTH);
1655 }
1656
1657 #[test]
1658 fn test_empty_query() {
1659 let parser = TqlParser::new();
1660 let result = parser.parse("").unwrap();
1661 assert!(matches!(result, AstNode::MatchAll));
1662 }
1663
1664 #[test]
1665 fn test_whitespace_only_query() {
1666 let parser = TqlParser::new();
1667 let result = parser.parse(" \t\n ").unwrap();
1668 assert!(matches!(result, AstNode::MatchAll));
1669 }
1670
1671 #[test]
1672 fn test_custom_max_depth() {
1673 let parser = TqlParser::with_max_depth(100);
1674 assert_eq!(parser.max_depth, 100);
1675 }
1676
1677 #[test]
1678 fn test_hyphenated_field_name_eq() {
1679 let parser = TqlParser::new();
1680 let ast = parser.parse("event-code eq 5").unwrap();
1681 match ast {
1682 AstNode::Comparison(comp) => {
1683 assert_eq!(comp.field, "event-code");
1684 assert_eq!(comp.operator, "eq");
1685 }
1686 other => panic!("Expected Comparison, got {:?}", other),
1687 }
1688 }
1689
1690 #[test]
1691 fn test_hyphenated_field_name_contains() {
1692 let parser = TqlParser::new();
1693 let ast = parser.parse("user-agent contains 'Mozilla'").unwrap();
1694 match ast {
1695 AstNode::Comparison(comp) => {
1696 assert_eq!(comp.field, "user-agent");
1697 assert_eq!(comp.operator, "contains");
1698 }
1699 other => panic!("Expected Comparison, got {:?}", other),
1700 }
1701 }
1702
1703 #[test]
1704 fn test_hyphenated_nested_field_name() {
1705 let parser = TqlParser::new();
1706 let ast = parser.parse("http.x-forwarded-for eq '10.0.0.1'").unwrap();
1707 match ast {
1708 AstNode::Comparison(comp) => {
1709 assert_eq!(comp.field, "http.x-forwarded-for");
1710 }
1711 other => panic!("Expected Comparison, got {:?}", other),
1712 }
1713 }
1714}