1use nom::{
37 branch::alt,
38 bytes::complete::{tag, take_while, take_while1},
39 character::complete::{char, digit1, multispace1, not_line_ending},
40 combinator::{map, opt, recognize, value},
41 multi::many0,
42 sequence::{pair, preceded, tuple},
43 IResult,
44};
45
46use crate::ast::*;
47use crate::error::{QailError, QailResult};
48
49fn ws_or_comment(input: &str) -> IResult<&str, ()> {
51 value((), many0(alt((
52 value((), multispace1),
53 parse_comment,
54 ))))(input)
55}
56
57fn parse_comment(input: &str) -> IResult<&str, ()> {
59 value((), pair(alt((tag("//"), tag("--"))), not_line_ending))(input)
60}
61
62pub fn parse(input: &str) -> QailResult<QailCmd> {
64 let input = input.trim();
65
66 match parse_qail_cmd(input) {
67 Ok(("", cmd)) => Ok(cmd),
68 Ok((remaining, _)) => Err(QailError::parse(
69 input.len() - remaining.len(),
70 format!("Unexpected trailing content: '{}'", remaining),
71 )),
72 Err(e) => Err(QailError::parse(0, format!("Parse failed: {:?}", e))),
73 }
74}
75
76fn parse_qail_cmd(input: &str) -> IResult<&str, QailCmd> {
78 let (input, action) = parse_action(input)?;
79 let (input, distinct_marker) = opt(char('!'))(input)?;
81 let distinct = distinct_marker.is_some();
82 let (input, _) = tag("::")(input)?;
83 let (input, table) = parse_identifier(input)?;
84 let (input, joins) = parse_joins(input)?;
85 let (input, _) = ws_or_comment(input)?;
86 let (input, _) = opt(char(':'))(input)?;
88 let (input, _) = ws_or_comment(input)?;
89 let (input, columns) = parse_columns(input)?;
90 let (input, _) = ws_or_comment(input)?;
91 let (input, cages) = parse_unified_blocks(input)?;
92
93 Ok((
94 input,
95 QailCmd {
96 action,
97 table: table.to_string(),
98 joins,
99 columns,
100 cages,
101 distinct,
102 },
103 ))
104}
105
106fn parse_action(input: &str) -> IResult<&str, Action> {
108 alt((
109 value(Action::Get, tag("get")),
110 value(Action::Set, tag("set")),
111 value(Action::Del, tag("del")),
112 value(Action::Add, tag("add")),
113 value(Action::Gen, tag("gen")),
114 value(Action::Make, tag("make")),
115 value(Action::Mod, tag("mod")),
116 value(Action::Over, tag("over")),
117 value(Action::With, tag("with")),
118 ))(input)
119}
120
121fn parse_identifier(input: &str) -> IResult<&str, &str> {
123 take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
124}
125
126fn parse_joins(input: &str) -> IResult<&str, Vec<Join>> {
127 many0(parse_single_join)(input)
128}
129
130fn parse_single_join(input: &str) -> IResult<&str, Join> {
132 let (input, _) = ws_or_comment(input)?;
133
134 if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("->>") (input) {
136 let (remaining, _) = ws_or_comment(remaining)?;
137 let (remaining, table) = parse_identifier(remaining)?;
138 return Ok((remaining, Join {
139 table: table.to_string(),
140 kind: JoinKind::Right,
141 }));
142 }
143
144 if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("<-") (input) {
146 let (remaining, _) = ws_or_comment(remaining)?;
147 let (remaining, table) = parse_identifier(remaining)?;
148 return Ok((remaining, Join {
149 table: table.to_string(),
150 kind: JoinKind::Left,
151 }));
152 }
153
154 let (input, _) = tag("->")(input)?;
156 let (input, _) = ws_or_comment(input)?;
157 let (input, table) = parse_identifier(input)?;
158 Ok((input, Join {
159 table: table.to_string(),
160 kind: JoinKind::Inner,
161 }))
162}
163
164fn parse_columns(input: &str) -> IResult<&str, Vec<Column>> {
166 many0(preceded(ws_or_comment, parse_any_column))(input)
167}
168
169fn parse_any_column(input: &str) -> IResult<&str, Column> {
170 alt((
171 preceded(char('\''), parse_label_column),
173 preceded(char('@'), parse_at_column),
175 ))(input)
176}
177
178fn parse_label_column(input: &str) -> IResult<&str, Column> {
180 alt((
181 value(Column::Star, char('_')),
183 parse_column_full_def_or_named,
185 ))(input)
186}
187
188fn parse_at_column(input: &str) -> IResult<&str, Column> {
190 alt((
191 value(Column::Star, char('*')),
192 map(preceded(char('-'), parse_identifier), |name| Column::Mod {
194 kind: ModKind::Drop,
195 col: Box::new(Column::Named(name.to_string()))
196 }),
197 parse_column_full_def_or_named,
198 ))(input)
199}
200
201fn parse_column_full_def_or_named(input: &str) -> IResult<&str, Column> {
202 let (input, name) = parse_identifier(input)?;
204
205 if let Ok((input, Some(func))) = opt(preceded(char('#'), parse_agg_func))(input) {
207 return Ok((input, Column::Aggregate {
208 col: name.to_string(),
209 func
210 }));
211 }
212
213 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(':')(input) {
215 let (input, type_or_func) = parse_identifier(input)?;
217
218 let (input, _) = ws_or_comment(input)?;
219
220 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>('(')(input) {
222 let (input, _) = ws_or_comment(input)?;
224 let (input, args) = opt(tuple((
225 parse_value,
226 many0(preceded(
227 tuple((ws_or_comment, char(','), ws_or_comment)),
228 parse_value
229 ))
230 )))(input)?;
231 let (input, _) = ws_or_comment(input)?;
232 let (input, _) = char(')')(input)?;
233
234 let params = match args {
235 Some((first, mut rest)) => {
236 let mut v = vec![first];
237 v.append(&mut rest);
238 v
239 },
240 None => vec![],
241 };
242
243 let (input, sorts) = many0(parse_legacy_sort_cage)(input)?;
245
246 let (input, partitions) = opt(parse_partition_block)(input)?;
248 let partition = partitions.unwrap_or_default();
249
250 return Ok((input, Column::Window {
251 name: name.to_string(),
252 func: type_or_func.to_string(),
253 params,
254 partition,
255 order: sorts,
256 }));
257 } else {
258 let (input, constraints) = parse_constraints(input)?;
260
261 return Ok((input, Column::Def {
262 name: name.to_string(),
263 data_type: type_or_func.to_string(),
264 constraints
265 }));
266 }
267 }
268
269 let (input, constraints) = parse_constraints(input)?;
271 if !constraints.is_empty() {
272 Ok((input, Column::Def {
273 name: name.to_string(),
274 data_type: "str".to_string(),
275 constraints
276 }))
277 } else {
278 Ok((input, Column::Named(name.to_string())))
280 }
281}
282
283fn parse_constraints(input: &str) -> IResult<&str, Vec<Constraint>> {
284 many0(alt((
285 value(Constraint::PrimaryKey, tag("^pk")),
286 value(Constraint::Unique, tag("^uniq")),
287 value(Constraint::Nullable, char('?')),
288 )))(input)
289}
290
291fn parse_agg_func(input: &str) -> IResult<&str, AggregateFunc> {
292 alt((
293 value(AggregateFunc::Count, tag("count")),
294 value(AggregateFunc::Sum, tag("sum")),
295 value(AggregateFunc::Avg, tag("avg")),
296 value(AggregateFunc::Min, tag("min")),
297 value(AggregateFunc::Max, tag("max")),
298 ))(input)
299}
300
301fn parse_unified_blocks(input: &str) -> IResult<&str, Vec<Cage>> {
304 many0(preceded(ws_or_comment, parse_unified_block))(input)
305}
306
307fn parse_unified_block(input: &str) -> IResult<&str, Cage> {
310 let (input, _) = char('[')(input)?;
311 let (input, _) = ws_or_comment(input)?;
312
313 let (input, items) = parse_block_items(input)?;
315
316 let (input, _) = ws_or_comment(input)?;
317 let (input, _) = char(']')(input)?;
318
319 items_to_cage(items, input)
322}
323
324#[derive(Debug)]
326enum BlockItem {
327 Filter(Condition, LogicalOp),
328 Sort(String, SortOrder),
329 Range(usize, Option<usize>), LegacyLimit(usize),
331 LegacyOffset(usize),
332}
333
334fn parse_block_items(input: &str) -> IResult<&str, Vec<BlockItem>> {
337 let (input, first) = opt(parse_block_item)(input)?;
338
339 match first {
340 None => Ok((input, vec![])),
341 Some(mut item) => {
342 let mut items = vec![];
343 let mut remaining = input;
344
345 loop {
346 let (input, _) = ws_or_comment(remaining)?;
347
348 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(',')(input) {
350 items.push(item);
352 let (input, _) = ws_or_comment(input)?;
353 let (input, next_item) = parse_block_item(input)?;
354 item = next_item;
355 remaining = input;
356 } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('|')(input) {
357 if let BlockItem::Filter(cond, _) = item {
359 items.push(BlockItem::Filter(cond, LogicalOp::Or));
360 } else {
361 items.push(item);
362 }
363 let (new_input, _) = ws_or_comment(new_input)?;
364 let (new_input, next_item) = parse_filter_item(new_input)?;
365 if let BlockItem::Filter(cond, _) = next_item {
367 item = BlockItem::Filter(cond, LogicalOp::Or);
368 } else {
369 item = next_item;
370 }
371 remaining = new_input;
372 } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('&')(input) {
373 items.push(item);
375 let (new_input, _) = ws_or_comment(new_input)?;
376 let (new_input, next_item) = parse_filter_item(new_input)?;
377 item = next_item;
378 remaining = new_input;
379 } else {
380 items.push(item);
381 remaining = input;
382 break;
383 }
384 }
385
386 Ok((remaining, items))
387 }
388 }
389}
390
391fn parse_block_item(input: &str) -> IResult<&str, BlockItem> {
393 alt((
394 parse_range_item,
396 parse_sort_item,
398 parse_legacy_limit_item,
400 parse_legacy_offset_item,
402 parse_legacy_sort_item,
404 parse_filter_item,
406 ))(input)
407}
408
409fn parse_range_item(input: &str) -> IResult<&str, BlockItem> {
411 let (input, start) = digit1(input)?;
412 let (input, _) = tag("..")(input)?;
413 let (input, end) = opt(digit1)(input)?;
414
415 let start_num: usize = start.parse().unwrap_or(0);
416 let end_num = end.map(|e| e.parse().unwrap_or(0));
417
418 Ok((input, BlockItem::Range(start_num, end_num)))
419}
420
421fn parse_sort_item(input: &str) -> IResult<&str, BlockItem> {
423 alt((
424 map(preceded(char('+'), parse_identifier), |col| {
425 BlockItem::Sort(col.to_string(), SortOrder::Asc)
426 }),
427 map(preceded(char('-'), parse_identifier), |col| {
428 BlockItem::Sort(col.to_string(), SortOrder::Desc)
429 }),
430 ))(input)
431}
432
433fn parse_legacy_limit_item(input: &str) -> IResult<&str, BlockItem> {
435 let (input, _) = tag("lim")(input)?;
436 let (input, _) = ws_or_comment(input)?;
437 let (input, _) = char('=')(input)?;
438 let (input, _) = ws_or_comment(input)?;
439 let (input, n) = digit1(input)?;
440 Ok((input, BlockItem::LegacyLimit(n.parse().unwrap_or(10))))
441}
442
443fn parse_legacy_offset_item(input: &str) -> IResult<&str, BlockItem> {
445 let (input, _) = tag("off")(input)?;
446 let (input, _) = ws_or_comment(input)?;
447 let (input, _) = char('=')(input)?;
448 let (input, _) = ws_or_comment(input)?;
449 let (input, n) = digit1(input)?;
450 Ok((input, BlockItem::LegacyOffset(n.parse().unwrap_or(0))))
451}
452
453fn parse_legacy_sort_item(input: &str) -> IResult<&str, BlockItem> {
455 let (input, _) = char('^')(input)?;
456 let (input, desc) = opt(char('!'))(input)?;
457 let (input, col) = parse_identifier(input)?;
458
459 let order = if desc.is_some() {
460 SortOrder::Desc
461 } else {
462 SortOrder::Asc
463 };
464
465 Ok((input, BlockItem::Sort(col.to_string(), order)))
466}
467
468fn parse_filter_item(input: &str) -> IResult<&str, BlockItem> {
470 let (input, _) = opt(char('\''))(input)?;
472 let (input, column) = parse_identifier(input)?;
473
474 let (input, is_array_unnest) = if input.starts_with("[*]") {
476 (&input[3..], true)
477 } else {
478 (input, false)
479 };
480
481 let (input, _) = ws_or_comment(input)?;
482 let (input, (op, val)) = parse_operator_and_value(input)?;
483
484 Ok((input, BlockItem::Filter(
485 Condition {
486 column: column.to_string(),
487 op,
488 value: val,
489 is_array_unnest,
490 },
491 LogicalOp::And, )))
493}
494
495fn items_to_cage(items: Vec<BlockItem>, input: &str) -> IResult<&str, Cage> {
497 let mut conditions = Vec::new();
499 let mut logical_op = LogicalOp::And;
500
501 for item in &items {
503 match item {
504 BlockItem::Range(start, end) => {
505 if let Some(e) = end {
510 let limit = e - start;
511 let offset = *start;
512 if offset == 0 {
516 return Ok((input, Cage {
517 kind: CageKind::Limit(limit),
518 conditions: vec![],
519 logical_op: LogicalOp::And,
520 }));
521 } else {
522 return Ok((input, Cage {
528 kind: CageKind::Limit(limit),
529 conditions: vec![Condition {
530 column: "__offset__".to_string(),
531 op: Operator::Eq,
532 value: Value::Int(offset as i64),
533 is_array_unnest: false,
534 }],
535 logical_op: LogicalOp::And,
536 }));
537 }
538 } else {
539 return Ok((input, Cage {
541 kind: CageKind::Offset(*start),
542 conditions: vec![],
543 logical_op: LogicalOp::And,
544 }));
545 }
546 }
547 BlockItem::Sort(col, order) => {
548 return Ok((input, Cage {
549 kind: CageKind::Sort(*order),
550 conditions: vec![Condition {
551 column: col.clone(),
552 op: Operator::Eq,
553 value: Value::Null,
554 is_array_unnest: false,
555 }],
556 logical_op: LogicalOp::And,
557 }));
558 }
559 BlockItem::LegacyLimit(n) => {
560 return Ok((input, Cage {
561 kind: CageKind::Limit(*n),
562 conditions: vec![],
563 logical_op: LogicalOp::And,
564 }));
565 }
566 BlockItem::LegacyOffset(n) => {
567 return Ok((input, Cage {
568 kind: CageKind::Offset(*n),
569 conditions: vec![],
570 logical_op: LogicalOp::And,
571 }));
572 }
573 BlockItem::Filter(cond, op) => {
574 conditions.push(cond.clone());
575 logical_op = *op;
576 }
577 }
578 }
579
580 if !conditions.is_empty() {
582 Ok((input, Cage {
583 kind: CageKind::Filter,
584 conditions,
585 logical_op,
586 }))
587 } else {
588 Ok((input, Cage {
590 kind: CageKind::Filter,
591 conditions: vec![],
592 logical_op: LogicalOp::And,
593 }))
594 }
595}
596
597fn parse_operator_and_value(input: &str) -> IResult<&str, (Operator, Value)> {
599 alt((
600 map(preceded(char('~'), preceded(ws_or_comment, parse_value)), |v| (Operator::Fuzzy, v)),
602 map(preceded(tag("=="), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
604 map(preceded(tag(">="), preceded(ws_or_comment, parse_value)), |v| (Operator::Gte, v)),
606 map(preceded(tag("<="), preceded(ws_or_comment, parse_value)), |v| (Operator::Lte, v)),
608 map(preceded(tag("!="), preceded(ws_or_comment, parse_value)), |v| (Operator::Ne, v)),
610 map(preceded(char('>'), preceded(ws_or_comment, parse_value)), |v| (Operator::Gt, v)),
612 map(preceded(char('<'), preceded(ws_or_comment, parse_value)), |v| (Operator::Lt, v)),
614 map(preceded(char('='), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
616 ))(input)
617}
618
619fn parse_value(input: &str) -> IResult<&str, Value> {
621 let (input, _) = ws_or_comment(input)?;
622
623 alt((
624 map(preceded(char('$'), digit1), |n: &str| {
626 Value::Param(n.parse().unwrap_or(1))
627 }),
628 value(Value::Bool(true), tag("true")),
630 value(Value::Bool(false), tag("false")),
631 parse_function_call,
633 map(tag("now"), |_| Value::Function("now".to_string())),
635 parse_number,
637 parse_double_quoted_string,
639 parse_quoted_string,
641 map(parse_identifier, |s| Value::String(s.to_string())),
643 ))(input)
644}
645
646fn parse_function_call(input: &str) -> IResult<&str, Value> {
648 let (input, name) = parse_identifier(input)?;
649 let (input, _) = char('(')(input)?;
650 let (input, _) = ws_or_comment(input)?;
651 let (input, args) = opt(tuple((
652 parse_value,
653 many0(preceded(
654 tuple((ws_or_comment, char(','), ws_or_comment)),
655 parse_value
656 ))
657 )))(input)?;
658 let (input, _) = ws_or_comment(input)?;
659 let (input, _) = char(')')(input)?;
660
661 let params = match args {
662 Some((first, mut rest)) => {
663 let mut v = vec![first];
664 v.append(&mut rest);
665 v
666 },
667 None => vec![],
668 };
669
670 Ok((input, Value::Function(format!("{}({})", name, params.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")))))
671}
672
673fn parse_number(input: &str) -> IResult<&str, Value> {
675 let (input, num_str) = recognize(tuple((
676 opt(char('-')),
677 digit1,
678 opt(pair(char('.'), digit1)),
679 )))(input)?;
680
681 if num_str.contains('.') {
682 Ok((input, Value::Float(num_str.parse().unwrap_or(0.0))))
683 } else {
684 Ok((input, Value::Int(num_str.parse().unwrap_or(0))))
685 }
686}
687
688fn parse_quoted_string(input: &str) -> IResult<&str, Value> {
690 let (input, _) = char('\'')(input)?;
691 let (input, content) = take_while(|c| c != '\'')(input)?;
692 let (input, _) = char('\'')(input)?;
693
694 Ok((input, Value::String(content.to_string())))
695}
696
697fn parse_double_quoted_string(input: &str) -> IResult<&str, Value> {
699 let (input, _) = char('"')(input)?;
700 let (input, content) = take_while(|c| c != '"')(input)?;
701 let (input, _) = char('"')(input)?;
702
703 Ok((input, Value::String(content.to_string())))
704}
705
706fn parse_legacy_sort_cage(input: &str) -> IResult<&str, Cage> {
708 let (input, _) = char('^')(input)?;
709 let (input, desc) = opt(char('!'))(input)?;
710 let (input, col) = parse_identifier(input)?;
711
712 let order = if desc.is_some() {
713 SortOrder::Desc
714 } else {
715 SortOrder::Asc
716 };
717
718 Ok((
719 input,
720 Cage {
721 kind: CageKind::Sort(order),
722 conditions: vec![Condition {
723 column: col.to_string(),
724 op: Operator::Eq,
725 value: Value::Null,
726 is_array_unnest: false,
727 }],
728 logical_op: LogicalOp::And,
729 },
730 ))
731}
732
733fn parse_partition_block(input: &str) -> IResult<&str, Vec<String>> {
734 let (input, _) = char('{')(input)?;
735 let (input, _) = ws_or_comment(input)?;
736 let (input, _) = tag("Part")(input)?;
737 let (input, _) = ws_or_comment(input)?;
738 let (input, _) = char('=')(input)?;
739 let (input, _) = ws_or_comment(input)?;
740
741 let (input, first) = parse_identifier(input)?;
742 let (input, rest) = many0(preceded(
743 tuple((ws_or_comment, char(','), ws_or_comment)),
744 parse_identifier
745 ))(input)?;
746
747 let (input, _) = ws_or_comment(input)?;
748 let (input, _) = char('}')(input)?;
749
750 let mut cols = vec![first.to_string()];
751 cols.append(&mut rest.iter().map(|s| s.to_string()).collect());
752 Ok((input, cols))
753}
754
755#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[test]
768 fn test_v2_simple_get() {
769 let cmd = parse("get::users:'_").unwrap();
770 assert_eq!(cmd.action, Action::Get);
771 assert_eq!(cmd.table, "users");
772 assert_eq!(cmd.columns, vec![Column::Star]);
773 }
774
775 #[test]
776 fn test_v2_get_with_columns() {
777 let cmd = parse("get::users:'id'email").unwrap();
778 assert_eq!(cmd.action, Action::Get);
779 assert_eq!(cmd.table, "users");
780 assert_eq!(
781 cmd.columns,
782 vec![
783 Column::Named("id".to_string()),
784 Column::Named("email".to_string()),
785 ]
786 );
787 }
788
789 #[test]
790 fn test_v2_get_with_filter() {
791 let cmd = parse("get::users:'_ [ 'active == true ]").unwrap();
792 assert_eq!(cmd.cages.len(), 1);
793 assert_eq!(cmd.cages[0].kind, CageKind::Filter);
794 assert_eq!(cmd.cages[0].conditions.len(), 1);
795 assert_eq!(cmd.cages[0].conditions[0].column, "active");
796 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
797 assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
798 }
799
800 #[test]
801 fn test_v2_get_with_range_limit() {
802 let cmd = parse("get::users:'_ [ 0..10 ]").unwrap();
803 assert_eq!(cmd.cages.len(), 1);
804 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
805 }
806
807 #[test]
808 fn test_v2_get_with_range_offset() {
809 let cmd = parse("get::users:'_ [ 20..30 ]").unwrap();
810 assert_eq!(cmd.cages.len(), 1);
811 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
813 assert_eq!(cmd.cages[0].conditions[0].column, "__offset__");
815 assert_eq!(cmd.cages[0].conditions[0].value, Value::Int(20));
816 }
817
818 #[test]
819 fn test_v2_get_with_sort_desc() {
820 let cmd = parse("get::users:'_ [ -created_at ]").unwrap();
821 assert_eq!(cmd.cages.len(), 1);
822 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
823 assert_eq!(cmd.cages[0].conditions[0].column, "created_at");
824 }
825
826 #[test]
827 fn test_v2_get_with_sort_asc() {
828 let cmd = parse("get::users:'_ [ +id ]").unwrap();
829 assert_eq!(cmd.cages.len(), 1);
830 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Asc));
831 assert_eq!(cmd.cages[0].conditions[0].column, "id");
832 }
833
834 #[test]
835 fn test_v2_fuzzy_match() {
836 let cmd = parse("get::users:'id [ 'name ~ \"john\" ]").unwrap();
837 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
838 assert_eq!(cmd.cages[0].conditions[0].value, Value::String("john".to_string()));
839 }
840
841 #[test]
842 fn test_v2_param_in_filter() {
843 let cmd = parse("get::users:'id [ 'email == $1 ]").unwrap();
844 assert_eq!(cmd.cages.len(), 1);
845 assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
846 }
847
848 #[test]
849 fn test_v2_left_join() {
850 let cmd = parse("get::users<-posts:'id'title").unwrap();
852 assert_eq!(cmd.joins.len(), 1);
853 assert_eq!(cmd.joins[0].table, "posts");
854 assert_eq!(cmd.joins[0].kind, JoinKind::Left);
855 }
856
857 #[test]
858 fn test_v2_inner_join() {
859 let cmd = parse("get::users->posts:'id'title").unwrap();
860 assert_eq!(cmd.joins.len(), 1);
861 assert_eq!(cmd.joins[0].table, "posts");
862 assert_eq!(cmd.joins[0].kind, JoinKind::Inner);
863 }
864
865 #[test]
866 fn test_v2_right_join() {
867 let cmd = parse("get::orders->>customers:'_").unwrap();
868 assert_eq!(cmd.joins.len(), 1);
869 assert_eq!(cmd.joins[0].table, "customers");
870 assert_eq!(cmd.joins[0].kind, JoinKind::Right);
871 }
872
873 #[test]
878 fn test_legacy_simple_get() {
879 let cmd = parse("get::users:@*").unwrap();
880 assert_eq!(cmd.action, Action::Get);
881 assert_eq!(cmd.table, "users");
882 assert_eq!(cmd.columns, vec![Column::Star]);
883 }
884
885 #[test]
886 fn test_legacy_get_with_columns() {
887 let cmd = parse("get::users:@id@email@role").unwrap();
888 assert_eq!(cmd.action, Action::Get);
889 assert_eq!(cmd.table, "users");
890 assert_eq!(
891 cmd.columns,
892 vec![
893 Column::Named("id".to_string()),
894 Column::Named("email".to_string()),
895 Column::Named("role".to_string()),
896 ]
897 );
898 }
899
900 #[test]
901 fn test_legacy_get_with_filter() {
902 let cmd = parse("get::users:@*[active=true]").unwrap();
903 assert_eq!(cmd.cages.len(), 1);
904 assert_eq!(cmd.cages[0].kind, CageKind::Filter);
905 assert_eq!(cmd.cages[0].conditions.len(), 1);
906 assert_eq!(cmd.cages[0].conditions[0].column, "active");
907 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
908 assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
909 }
910
911 #[test]
912 fn test_legacy_get_with_limit() {
913 let cmd = parse("get::users:@*[lim=10]").unwrap();
914 assert_eq!(cmd.cages.len(), 1);
915 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
916 }
917
918 #[test]
919 fn test_legacy_get_with_sort_desc() {
920 let cmd = parse("get::users:@*[^!created_at]").unwrap();
921 assert_eq!(cmd.cages.len(), 1);
922 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
923 }
924
925 #[test]
926 fn test_set_command() {
927 let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
928 assert_eq!(cmd.action, Action::Set);
929 assert_eq!(cmd.table, "users");
930 assert_eq!(cmd.cages.len(), 2);
931 }
932
933 #[test]
934 fn test_del_command() {
935 let cmd = parse("del::sessions:[expired_at<now]").unwrap();
936 assert_eq!(cmd.action, Action::Del);
937 assert_eq!(cmd.table, "sessions");
938 }
939
940 #[test]
941 fn test_legacy_fuzzy_match() {
942 let cmd = parse("get::users:@*[name~$1]").unwrap();
943 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
944 }
945
946 #[test]
947 fn test_legacy_complex_query() {
948 let cmd = parse("get::users:@id@email@role[active=true][lim=10]").unwrap();
949 assert_eq!(cmd.action, Action::Get);
950 assert_eq!(cmd.table, "users");
951 assert_eq!(cmd.columns.len(), 3);
952 assert_eq!(cmd.cages.len(), 2);
953 }
954
955 #[test]
956 fn test_legacy_param_in_filter() {
957 let cmd = parse("get::users:@*[id=$1]").unwrap();
958 assert_eq!(cmd.cages.len(), 1);
959 assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
960 }
961
962 #[test]
963 fn test_legacy_param_in_update() {
964 let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
965 assert_eq!(cmd.action, Action::Set);
966 assert_eq!(cmd.cages.len(), 2);
967 assert_eq!(cmd.cages[1].conditions[0].value, Value::Param(1));
968 }
969}