1use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub struct Expression {
10 pub source: String,
12 pub transforms: Vec<Transform>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub enum Transform {
19 Filter {
21 field: String,
23 value: String,
25 },
26 Select {
28 fields: Vec<String>,
30 },
31 Sort {
33 field: String,
35 desc: bool,
37 },
38 Limit {
40 n: usize,
42 },
43 Count,
45 Sum {
47 field: String,
49 },
50 Mean {
52 field: String,
54 },
55 Rate {
57 window: String,
59 },
60 Percentage,
62 Join {
64 other: String,
66 on: String,
68 },
69 Sample {
71 n: usize,
73 },
74 GroupBy {
76 field: String,
78 },
79 Distinct {
81 field: Option<String>,
83 },
84 Where {
86 field: String,
88 op: String,
90 value: String,
92 },
93 Offset {
95 n: usize,
97 },
98 Min {
100 field: String,
102 },
103 Max {
105 field: String,
107 },
108 First {
110 n: usize,
112 },
113 Last {
115 n: usize,
117 },
118 Flatten,
120 Reverse,
122 Map {
124 expr: String,
126 },
127 Reduce {
129 initial: String,
131 expr: String,
133 },
134 Aggregate {
136 field: String,
138 op: AggregateOp,
140 },
141 Pivot {
143 row_field: String,
145 col_field: String,
147 value_field: String,
149 },
150 CumulativeSum {
152 field: String,
154 },
155 Rank {
157 field: String,
159 method: RankMethod,
161 },
162 MovingAverage {
164 field: String,
166 window: usize,
168 },
169 PercentChange {
171 field: String,
173 },
174 Suggest {
176 prefix: String,
178 count: usize,
180 },
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185pub enum AggregateOp {
186 Sum,
188 Count,
190 Mean,
192 Min,
194 Max,
196 First,
198 Last,
200}
201
202impl AggregateOp {
203 pub fn from_str(s: &str) -> Option<Self> {
205 match s.to_lowercase().as_str() {
206 "sum" => Some(Self::Sum),
207 "count" => Some(Self::Count),
208 "mean" | "avg" | "average" => Some(Self::Mean),
209 "min" => Some(Self::Min),
210 "max" => Some(Self::Max),
211 "first" => Some(Self::First),
212 "last" => Some(Self::Last),
213 _ => None,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
220pub enum RankMethod {
221 #[default]
223 Dense,
224 Ordinal,
226 Average,
228}
229
230impl RankMethod {
231 pub fn from_str(s: &str) -> Option<Self> {
233 match s.to_lowercase().as_str() {
234 "dense" => Some(Self::Dense),
235 "ordinal" => Some(Self::Ordinal),
236 "average" | "avg" => Some(Self::Average),
237 _ => None,
238 }
239 }
240}
241
242#[derive(Debug, Default)]
244pub struct ExpressionParser;
245
246impl ExpressionParser {
247 #[must_use]
249 pub const fn new() -> Self {
250 Self
251 }
252
253 pub fn parse(&self, input: &str) -> Result<Expression, ExpressionError> {
259 let input = input.trim();
260
261 let inner = if input.starts_with("{{") && input.ends_with("}}") {
263 input[2..input.len() - 2].trim()
264 } else {
265 input
266 };
267
268 let parts: Vec<&str> = inner.split('|').map(str::trim).collect();
270
271 if parts.is_empty() {
272 return Err(ExpressionError::EmptyExpression);
273 }
274
275 let source = parts[0].to_string();
276 let mut transforms = Vec::new();
277
278 for part in &parts[1..] {
279 let transform = self.parse_transform(part)?;
280 transforms.push(transform);
281 }
282
283 Ok(Expression { source, transforms })
284 }
285
286 #[allow(clippy::too_many_lines)]
287 fn parse_transform(&self, input: &str) -> Result<Transform, ExpressionError> {
288 let input = input.trim();
289
290 if let Some(paren_pos) = input.find('(') {
292 let name = &input[..paren_pos];
293 let args_str = input[paren_pos + 1..].trim_end_matches(')').trim();
294
295 match name {
296 "filter" => {
297 let (field, value) = self.parse_key_value(args_str)?;
298 Ok(Transform::Filter { field, value })
299 }
300 "select" => {
301 let fields: Vec<String> = args_str
302 .split(',')
303 .map(|s| s.trim().to_string())
304 .filter(|s| !s.is_empty())
305 .collect();
306 Ok(Transform::Select { fields })
307 }
308 "sort" => {
309 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
310 let field = (*parts.first().unwrap_or(&"")).to_string();
311 let desc = parts.get(1).is_some_and(|s| s.contains("desc=true"));
312 Ok(Transform::Sort { field, desc })
313 }
314 "limit" => {
315 let n = args_str
316 .parse()
317 .map_err(|_| ExpressionError::InvalidArgument("limit".to_string()))?;
318 Ok(Transform::Limit { n })
319 }
320 "sum" => Ok(Transform::Sum {
321 field: args_str.to_string(),
322 }),
323 "mean" => Ok(Transform::Mean {
324 field: args_str.to_string(),
325 }),
326 "rate" => Ok(Transform::Rate {
327 window: args_str.to_string(),
328 }),
329 "join" => {
330 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
331 let other = (*parts.first().unwrap_or(&"")).to_string();
332 let on = parts
333 .get(1)
334 .and_then(|s| s.strip_prefix("on="))
335 .unwrap_or("")
336 .to_string();
337 Ok(Transform::Join { other, on })
338 }
339 "sample" => {
340 let n = args_str
341 .parse()
342 .map_err(|_| ExpressionError::InvalidArgument("sample".to_string()))?;
343 Ok(Transform::Sample { n })
344 }
345 "group_by" => Ok(Transform::GroupBy {
346 field: args_str.to_string(),
347 }),
348 "distinct" => {
349 let field = if args_str.is_empty() {
350 None
351 } else {
352 Some(args_str.to_string())
353 };
354 Ok(Transform::Distinct { field })
355 }
356 "where" => {
357 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
358 if parts.len() < 3 {
359 return Err(ExpressionError::InvalidArgument(
360 "where requires field, op, value".to_string(),
361 ));
362 }
363 Ok(Transform::Where {
364 field: parts[0].to_string(),
365 op: parts[1].to_string(),
366 value: parts[2].to_string(),
367 })
368 }
369 "offset" => {
370 let n = args_str
371 .parse()
372 .map_err(|_| ExpressionError::InvalidArgument("offset".to_string()))?;
373 Ok(Transform::Offset { n })
374 }
375 "min" => Ok(Transform::Min {
376 field: args_str.to_string(),
377 }),
378 "max" => Ok(Transform::Max {
379 field: args_str.to_string(),
380 }),
381 "first" => {
382 let n = args_str
383 .parse()
384 .map_err(|_| ExpressionError::InvalidArgument("first".to_string()))?;
385 Ok(Transform::First { n })
386 }
387 "last" => {
388 let n = args_str
389 .parse()
390 .map_err(|_| ExpressionError::InvalidArgument("last".to_string()))?;
391 Ok(Transform::Last { n })
392 }
393 "map" => Ok(Transform::Map {
394 expr: args_str.to_string(),
395 }),
396 "reduce" => {
397 let parts: Vec<&str> = args_str.splitn(2, ',').map(str::trim).collect();
398 if parts.len() < 2 {
399 return Err(ExpressionError::InvalidArgument(
400 "reduce requires initial, expr".to_string(),
401 ));
402 }
403 Ok(Transform::Reduce {
404 initial: parts[0].to_string(),
405 expr: parts[1].to_string(),
406 })
407 }
408 "agg" | "aggregate" => {
409 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
410 if parts.len() < 2 {
411 return Err(ExpressionError::InvalidArgument(
412 "agg requires field, op".to_string(),
413 ));
414 }
415 let op = AggregateOp::from_str(parts[1]).ok_or_else(|| {
416 ExpressionError::InvalidArgument(format!(
417 "unknown aggregate op: {}",
418 parts[1]
419 ))
420 })?;
421 Ok(Transform::Aggregate {
422 field: parts[0].to_string(),
423 op,
424 })
425 }
426 "pivot" => {
427 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
428 if parts.len() < 3 {
429 return Err(ExpressionError::InvalidArgument(
430 "pivot requires row_field, col_field, value_field".to_string(),
431 ));
432 }
433 Ok(Transform::Pivot {
434 row_field: parts[0].to_string(),
435 col_field: parts[1].to_string(),
436 value_field: parts[2].to_string(),
437 })
438 }
439 "cumsum" => Ok(Transform::CumulativeSum {
440 field: args_str.to_string(),
441 }),
442 "rank" => {
443 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
444 let field = (*parts.first().unwrap_or(&"")).to_string();
445 let method = parts
446 .get(1)
447 .and_then(|s| RankMethod::from_str(s))
448 .unwrap_or_default();
449 Ok(Transform::Rank { field, method })
450 }
451 "moving_avg" | "ma" => {
452 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
453 if parts.len() < 2 {
454 return Err(ExpressionError::InvalidArgument(
455 "moving_avg requires field, window".to_string(),
456 ));
457 }
458 let window = parts[1].parse().map_err(|_| {
459 ExpressionError::InvalidArgument("moving_avg window".to_string())
460 })?;
461 Ok(Transform::MovingAverage {
462 field: parts[0].to_string(),
463 window,
464 })
465 }
466 "pct_change" => Ok(Transform::PercentChange {
467 field: args_str.to_string(),
468 }),
469 "suggest" => {
470 let parts: Vec<&str> = args_str.split(',').map(str::trim).collect();
471 if parts.len() < 2 {
472 return Err(ExpressionError::InvalidArgument(
473 "suggest requires prefix, count".to_string(),
474 ));
475 }
476 let count = parts[1].parse().map_err(|_| {
477 ExpressionError::InvalidArgument("suggest count".to_string())
478 })?;
479 Ok(Transform::Suggest {
480 prefix: parts[0].to_string(),
481 count,
482 })
483 }
484 _ => Err(ExpressionError::UnknownTransform(name.to_string())),
485 }
486 } else {
487 match input {
489 "count" => Ok(Transform::Count),
490 "percentage" => Ok(Transform::Percentage),
491 "flatten" => Ok(Transform::Flatten),
492 "reverse" => Ok(Transform::Reverse),
493 "distinct" => Ok(Transform::Distinct { field: None }),
494 _ => Err(ExpressionError::UnknownTransform(input.to_string())),
495 }
496 }
497 }
498
499 fn parse_key_value(&self, input: &str) -> Result<(String, String), ExpressionError> {
500 let parts: Vec<&str> = input.splitn(2, '=').collect();
501 if parts.len() != 2 {
502 return Err(ExpressionError::InvalidArgument(input.to_string()));
503 }
504 Ok((parts[0].trim().to_string(), parts[1].trim().to_string()))
505 }
506}
507
508#[derive(Debug, Clone, PartialEq, Eq)]
510pub enum ExpressionError {
511 EmptyExpression,
513 UnknownTransform(String),
515 InvalidArgument(String),
517}
518
519impl std::fmt::Display for ExpressionError {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 match self {
522 Self::EmptyExpression => write!(f, "empty expression"),
523 Self::UnknownTransform(name) => write!(f, "unknown transform: {name}"),
524 Self::InvalidArgument(arg) => write!(f, "invalid argument: {arg}"),
525 }
526 }
527}
528
529impl std::error::Error for ExpressionError {}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 #[test]
536 fn test_parse_simple_source() {
537 let parser = ExpressionParser::new();
538 let expr = parser.parse("data.transactions").unwrap();
539 assert_eq!(expr.source, "data.transactions");
540 assert!(expr.transforms.is_empty());
541 }
542
543 #[test]
544 fn test_parse_with_braces() {
545 let parser = ExpressionParser::new();
546 let expr = parser.parse("{{ data.transactions }}").unwrap();
547 assert_eq!(expr.source, "data.transactions");
548 }
549
550 #[test]
551 fn test_parse_count() {
552 let parser = ExpressionParser::new();
553 let expr = parser.parse("{{ data.items | count }}").unwrap();
554 assert_eq!(expr.transforms, vec![Transform::Count]);
555 }
556
557 #[test]
558 fn test_parse_filter() {
559 let parser = ExpressionParser::new();
560 let expr = parser.parse("{{ data | filter(status=active) }}").unwrap();
561 assert_eq!(
562 expr.transforms,
563 vec![Transform::Filter {
564 field: "status".to_string(),
565 value: "active".to_string(),
566 }]
567 );
568 }
569
570 #[test]
571 fn test_parse_select() {
572 let parser = ExpressionParser::new();
573 let expr = parser
574 .parse("{{ data | select(id, name, email) }}")
575 .unwrap();
576 assert_eq!(
577 expr.transforms,
578 vec![Transform::Select {
579 fields: vec!["id".to_string(), "name".to_string(), "email".to_string()],
580 }]
581 );
582 }
583
584 #[test]
585 fn test_parse_sort() {
586 let parser = ExpressionParser::new();
587 let expr = parser
588 .parse("{{ data | sort(created_at, desc=true) }}")
589 .unwrap();
590 assert_eq!(
591 expr.transforms,
592 vec![Transform::Sort {
593 field: "created_at".to_string(),
594 desc: true,
595 }]
596 );
597 }
598
599 #[test]
600 fn test_parse_limit() {
601 let parser = ExpressionParser::new();
602 let expr = parser.parse("{{ data | limit(10) }}").unwrap();
603 assert_eq!(expr.transforms, vec![Transform::Limit { n: 10 }]);
604 }
605
606 #[test]
607 fn test_parse_chain() {
608 let parser = ExpressionParser::new();
609 let expr = parser
610 .parse("{{ data.transactions | filter(status=completed) | count }}")
611 .unwrap();
612
613 assert_eq!(expr.source, "data.transactions");
614 assert_eq!(expr.transforms.len(), 2);
615 assert_eq!(
616 expr.transforms[0],
617 Transform::Filter {
618 field: "status".to_string(),
619 value: "completed".to_string(),
620 }
621 );
622 assert_eq!(expr.transforms[1], Transform::Count);
623 }
624
625 #[test]
626 fn test_parse_join() {
627 let parser = ExpressionParser::new();
628 let expr = parser
629 .parse("{{ data.orders | join(data.customers, on=customer_id) }}")
630 .unwrap();
631
632 assert_eq!(
633 expr.transforms,
634 vec![Transform::Join {
635 other: "data.customers".to_string(),
636 on: "customer_id".to_string(),
637 }]
638 );
639 }
640
641 #[test]
642 fn test_parse_sample() {
643 let parser = ExpressionParser::new();
644 let expr = parser.parse("{{ data | sample(100) }}").unwrap();
645 assert_eq!(expr.transforms, vec![Transform::Sample { n: 100 }]);
646 }
647
648 #[test]
649 fn test_parse_sum() {
650 let parser = ExpressionParser::new();
651 let expr = parser.parse("{{ data | sum(amount) }}").unwrap();
652 assert_eq!(
653 expr.transforms,
654 vec![Transform::Sum {
655 field: "amount".to_string(),
656 }]
657 );
658 }
659
660 #[test]
661 fn test_parse_error_unknown_transform() {
662 let parser = ExpressionParser::new();
663 let result = parser.parse("{{ data | unknown() }}");
664 assert!(matches!(result, Err(ExpressionError::UnknownTransform(_))));
665 }
666
667 #[test]
668 fn test_expression_error_display() {
669 assert_eq!(
670 ExpressionError::EmptyExpression.to_string(),
671 "empty expression"
672 );
673 assert_eq!(
674 ExpressionError::UnknownTransform("foo".to_string()).to_string(),
675 "unknown transform: foo"
676 );
677 }
678
679 #[test]
684 fn test_parse_map() {
685 let parser = ExpressionParser::new();
686 let expr = parser.parse("{{ data | map(item.value * 2) }}").unwrap();
687 assert_eq!(
688 expr.transforms,
689 vec![Transform::Map {
690 expr: "item.value * 2".to_string(),
691 }]
692 );
693 }
694
695 #[test]
696 fn test_parse_reduce() {
697 let parser = ExpressionParser::new();
698 let expr = parser
699 .parse("{{ data | reduce(0, acc + item.value) }}")
700 .unwrap();
701 assert_eq!(
702 expr.transforms,
703 vec![Transform::Reduce {
704 initial: "0".to_string(),
705 expr: "acc + item.value".to_string(),
706 }]
707 );
708 }
709
710 #[test]
711 fn test_parse_reduce_missing_args() {
712 let parser = ExpressionParser::new();
713 let result = parser.parse("{{ data | reduce(0) }}");
714 assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
715 }
716
717 #[test]
718 fn test_parse_aggregate_sum() {
719 let parser = ExpressionParser::new();
720 let expr = parser
721 .parse("{{ data | group_by(category) | agg(amount, sum) }}")
722 .unwrap();
723 assert_eq!(
724 expr.transforms,
725 vec![
726 Transform::GroupBy {
727 field: "category".to_string()
728 },
729 Transform::Aggregate {
730 field: "amount".to_string(),
731 op: AggregateOp::Sum,
732 },
733 ]
734 );
735 }
736
737 #[test]
738 fn test_parse_aggregate_mean() {
739 let parser = ExpressionParser::new();
740 let expr = parser.parse("{{ data | agg(price, mean) }}").unwrap();
741 assert_eq!(
742 expr.transforms,
743 vec![Transform::Aggregate {
744 field: "price".to_string(),
745 op: AggregateOp::Mean,
746 }]
747 );
748 }
749
750 #[test]
751 fn test_parse_aggregate_count() {
752 let parser = ExpressionParser::new();
753 let expr = parser.parse("{{ data | agg(id, count) }}").unwrap();
754 assert_eq!(
755 expr.transforms,
756 vec![Transform::Aggregate {
757 field: "id".to_string(),
758 op: AggregateOp::Count,
759 }]
760 );
761 }
762
763 #[test]
764 fn test_parse_aggregate_alias() {
765 let parser = ExpressionParser::new();
766 let expr = parser.parse("{{ data | aggregate(value, max) }}").unwrap();
767 assert_eq!(
768 expr.transforms,
769 vec![Transform::Aggregate {
770 field: "value".to_string(),
771 op: AggregateOp::Max,
772 }]
773 );
774 }
775
776 #[test]
777 fn test_parse_aggregate_invalid_op() {
778 let parser = ExpressionParser::new();
779 let result = parser.parse("{{ data | agg(field, unknown_op) }}");
780 assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
781 }
782
783 #[test]
784 fn test_parse_pivot() {
785 let parser = ExpressionParser::new();
786 let expr = parser
787 .parse("{{ data | pivot(date, product, sales) }}")
788 .unwrap();
789 assert_eq!(
790 expr.transforms,
791 vec![Transform::Pivot {
792 row_field: "date".to_string(),
793 col_field: "product".to_string(),
794 value_field: "sales".to_string(),
795 }]
796 );
797 }
798
799 #[test]
800 fn test_parse_pivot_missing_args() {
801 let parser = ExpressionParser::new();
802 let result = parser.parse("{{ data | pivot(date, product) }}");
803 assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
804 }
805
806 #[test]
807 fn test_parse_cumsum() {
808 let parser = ExpressionParser::new();
809 let expr = parser.parse("{{ data | cumsum(balance) }}").unwrap();
810 assert_eq!(
811 expr.transforms,
812 vec![Transform::CumulativeSum {
813 field: "balance".to_string(),
814 }]
815 );
816 }
817
818 #[test]
819 fn test_parse_rank_default() {
820 let parser = ExpressionParser::new();
821 let expr = parser.parse("{{ data | rank(score) }}").unwrap();
822 assert_eq!(
823 expr.transforms,
824 vec![Transform::Rank {
825 field: "score".to_string(),
826 method: RankMethod::Dense,
827 }]
828 );
829 }
830
831 #[test]
832 fn test_parse_rank_ordinal() {
833 let parser = ExpressionParser::new();
834 let expr = parser.parse("{{ data | rank(score, ordinal) }}").unwrap();
835 assert_eq!(
836 expr.transforms,
837 vec![Transform::Rank {
838 field: "score".to_string(),
839 method: RankMethod::Ordinal,
840 }]
841 );
842 }
843
844 #[test]
845 fn test_parse_rank_average() {
846 let parser = ExpressionParser::new();
847 let expr = parser.parse("{{ data | rank(score, average) }}").unwrap();
848 assert_eq!(
849 expr.transforms,
850 vec![Transform::Rank {
851 field: "score".to_string(),
852 method: RankMethod::Average,
853 }]
854 );
855 }
856
857 #[test]
858 fn test_parse_moving_average() {
859 let parser = ExpressionParser::new();
860 let expr = parser.parse("{{ data | moving_avg(price, 5) }}").unwrap();
861 assert_eq!(
862 expr.transforms,
863 vec![Transform::MovingAverage {
864 field: "price".to_string(),
865 window: 5,
866 }]
867 );
868 }
869
870 #[test]
871 fn test_parse_moving_average_alias() {
872 let parser = ExpressionParser::new();
873 let expr = parser.parse("{{ data | ma(price, 10) }}").unwrap();
874 assert_eq!(
875 expr.transforms,
876 vec![Transform::MovingAverage {
877 field: "price".to_string(),
878 window: 10,
879 }]
880 );
881 }
882
883 #[test]
884 fn test_parse_moving_average_missing_window() {
885 let parser = ExpressionParser::new();
886 let result = parser.parse("{{ data | moving_avg(price) }}");
887 assert!(matches!(result, Err(ExpressionError::InvalidArgument(_))));
888 }
889
890 #[test]
891 fn test_parse_pct_change() {
892 let parser = ExpressionParser::new();
893 let expr = parser.parse("{{ data | pct_change(value) }}").unwrap();
894 assert_eq!(
895 expr.transforms,
896 vec![Transform::PercentChange {
897 field: "value".to_string(),
898 }]
899 );
900 }
901
902 #[test]
903 fn test_parse_complex_pipeline() {
904 let parser = ExpressionParser::new();
905 let expr = parser
906 .parse("{{ data | filter(status=active) | group_by(category) | agg(amount, sum) | sort(amount, desc=true) | limit(10) }}")
907 .unwrap();
908
909 assert_eq!(expr.transforms.len(), 5);
910 assert!(matches!(expr.transforms[0], Transform::Filter { .. }));
911 assert!(matches!(expr.transforms[1], Transform::GroupBy { .. }));
912 assert!(matches!(expr.transforms[2], Transform::Aggregate { .. }));
913 assert!(matches!(
914 expr.transforms[3],
915 Transform::Sort { desc: true, .. }
916 ));
917 assert!(matches!(expr.transforms[4], Transform::Limit { n: 10 }));
918 }
919
920 #[test]
921 fn test_parse_map_reduce_pipeline() {
922 let parser = ExpressionParser::new();
923 let expr = parser
924 .parse("{{ data | map(item.value * 2) | reduce(0, acc + item) }}")
925 .unwrap();
926
927 assert_eq!(expr.transforms.len(), 2);
928 assert!(matches!(expr.transforms[0], Transform::Map { .. }));
929 assert!(matches!(expr.transforms[1], Transform::Reduce { .. }));
930 }
931
932 #[test]
937 fn test_aggregate_op_from_str() {
938 assert_eq!(AggregateOp::from_str("sum"), Some(AggregateOp::Sum));
939 assert_eq!(AggregateOp::from_str("count"), Some(AggregateOp::Count));
940 assert_eq!(AggregateOp::from_str("mean"), Some(AggregateOp::Mean));
941 assert_eq!(AggregateOp::from_str("avg"), Some(AggregateOp::Mean));
942 assert_eq!(AggregateOp::from_str("average"), Some(AggregateOp::Mean));
943 assert_eq!(AggregateOp::from_str("min"), Some(AggregateOp::Min));
944 assert_eq!(AggregateOp::from_str("max"), Some(AggregateOp::Max));
945 assert_eq!(AggregateOp::from_str("first"), Some(AggregateOp::First));
946 assert_eq!(AggregateOp::from_str("last"), Some(AggregateOp::Last));
947 assert_eq!(AggregateOp::from_str("unknown"), None);
948 }
949
950 #[test]
951 fn test_aggregate_op_case_insensitive() {
952 assert_eq!(AggregateOp::from_str("SUM"), Some(AggregateOp::Sum));
953 assert_eq!(AggregateOp::from_str("Sum"), Some(AggregateOp::Sum));
954 assert_eq!(AggregateOp::from_str("MEAN"), Some(AggregateOp::Mean));
955 }
956
957 #[test]
962 fn test_rank_method_from_str() {
963 assert_eq!(RankMethod::from_str("dense"), Some(RankMethod::Dense));
964 assert_eq!(RankMethod::from_str("ordinal"), Some(RankMethod::Ordinal));
965 assert_eq!(RankMethod::from_str("average"), Some(RankMethod::Average));
966 assert_eq!(RankMethod::from_str("avg"), Some(RankMethod::Average));
967 assert_eq!(RankMethod::from_str("unknown"), None);
968 }
969
970 #[test]
971 fn test_rank_method_default() {
972 assert_eq!(RankMethod::default(), RankMethod::Dense);
973 }
974}