1use nom::{
37 branch::alt,
38 bytes::complete::{tag, take_until, take_while, take_while1},
39 character::complete::{char, digit1, multispace0, multispace1, not_line_ending},
40 combinator::{map, opt, recognize, value},
41 multi::{many0, separated_list1},
42 sequence::{delimited, 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
84 if action == Action::Index {
86 return parse_index_command(input);
87 }
88
89 let (input, table) = parse_identifier(input)?;
90 let (input, joins) = parse_joins(input)?;
91 let (input, _) = ws_or_comment(input)?;
92 let (input, _) = opt(char(':'))(input)?;
94 let (input, _) = ws_or_comment(input)?;
95 let (input, columns) = parse_columns(input)?;
96 let (input, _) = ws_or_comment(input)?;
97
98 let (input, table_constraints) = if action == Action::Make {
100 parse_table_constraints(input)?
101 } else {
102 (input, vec![])
103 };
104
105 let (input, _) = ws_or_comment(input)?;
106 let (input, cages) = parse_unified_blocks(input)?;
107
108 Ok((
109 input,
110 QailCmd {
111 action,
112 table: table.to_string(),
113 joins,
114 columns,
115 cages,
116 distinct,
117 index_def: None,
118 table_constraints,
119 },
120 ))
121}
122
123fn parse_action(input: &str) -> IResult<&str, Action> {
125 alt((
126 value(Action::Get, tag("get")),
127 value(Action::Set, tag("set")),
128 value(Action::Del, tag("del")),
129 value(Action::Add, tag("add")),
130 value(Action::Gen, tag("gen")),
131 value(Action::Make, tag("make")),
132 value(Action::Mod, tag("mod")),
133 value(Action::Over, tag("over")),
134 value(Action::With, tag("with")),
135 value(Action::Index, tag("index")),
136 ))(input)
137}
138
139fn parse_identifier(input: &str) -> IResult<&str, &str> {
141 take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
142}
143
144fn parse_joins(input: &str) -> IResult<&str, Vec<Join>> {
145 many0(parse_single_join)(input)
146}
147
148fn parse_single_join(input: &str) -> IResult<&str, Join> {
150 let (input, _) = ws_or_comment(input)?;
151
152 if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("->>") (input) {
154 let (remaining, _) = ws_or_comment(remaining)?;
155 let (remaining, table) = parse_identifier(remaining)?;
156 return Ok((remaining, Join {
157 table: table.to_string(),
158 kind: JoinKind::Right,
159 }));
160 }
161
162 if let Ok((remaining, _)) = tag::<_, _, nom::error::Error<&str>>("<-") (input) {
164 let (remaining, _) = ws_or_comment(remaining)?;
165 let (remaining, table) = parse_identifier(remaining)?;
166 return Ok((remaining, Join {
167 table: table.to_string(),
168 kind: JoinKind::Left,
169 }));
170 }
171
172 let (input, _) = tag("->")(input)?;
174 let (input, _) = ws_or_comment(input)?;
175 let (input, table) = parse_identifier(input)?;
176 Ok((input, Join {
177 table: table.to_string(),
178 kind: JoinKind::Inner,
179 }))
180}
181
182fn parse_columns(input: &str) -> IResult<&str, Vec<Column>> {
184 many0(preceded(ws_or_comment, parse_any_column))(input)
185}
186
187fn parse_any_column(input: &str) -> IResult<&str, Column> {
188 alt((
189 preceded(char('\''), parse_label_column),
191 preceded(char('@'), parse_at_column),
193 ))(input)
194}
195
196fn parse_label_column(input: &str) -> IResult<&str, Column> {
198 alt((
199 value(Column::Star, char('_')),
201 parse_column_full_def_or_named,
203 ))(input)
204}
205
206fn parse_at_column(input: &str) -> IResult<&str, Column> {
208 alt((
209 value(Column::Star, char('*')),
210 map(preceded(char('-'), parse_identifier), |name| Column::Mod {
212 kind: ModKind::Drop,
213 col: Box::new(Column::Named(name.to_string()))
214 }),
215 parse_column_full_def_or_named,
216 ))(input)
217}
218
219fn parse_column_full_def_or_named(input: &str) -> IResult<&str, Column> {
220 let (input, name) = parse_identifier(input)?;
222
223 if let Ok((input, Some(func))) = opt(preceded(char('#'), parse_agg_func))(input) {
225 return Ok((input, Column::Aggregate {
226 col: name.to_string(),
227 func
228 }));
229 }
230
231 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(':')(input) {
233 let (input, type_or_func) = parse_identifier(input)?;
235
236 let (input, _) = ws_or_comment(input)?;
237
238 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>('(')(input) {
240 let (input, _) = ws_or_comment(input)?;
242 let (input, args) = opt(tuple((
243 parse_value,
244 many0(preceded(
245 tuple((ws_or_comment, char(','), ws_or_comment)),
246 parse_value
247 ))
248 )))(input)?;
249 let (input, _) = ws_or_comment(input)?;
250 let (input, _) = char(')')(input)?;
251
252 let params = match args {
253 Some((first, mut rest)) => {
254 let mut v = vec![first];
255 v.append(&mut rest);
256 v
257 },
258 None => vec![],
259 };
260
261 let (input, sorts) = many0(parse_legacy_sort_cage)(input)?;
263
264 let (input, partitions) = opt(parse_partition_block)(input)?;
266 let partition = partitions.unwrap_or_default();
267
268 return Ok((input, Column::Window {
269 name: name.to_string(),
270 func: type_or_func.to_string(),
271 params,
272 partition,
273 order: sorts,
274 }));
275 } else {
276 let (input, constraints) = parse_constraints(input)?;
278
279 return Ok((input, Column::Def {
280 name: name.to_string(),
281 data_type: type_or_func.to_string(),
282 constraints
283 }));
284 }
285 }
286
287 let (input, constraints) = parse_constraints(input)?;
289 if !constraints.is_empty() {
290 Ok((input, Column::Def {
291 name: name.to_string(),
292 data_type: "str".to_string(),
293 constraints
294 }))
295 } else {
296 Ok((input, Column::Named(name.to_string())))
298 }
299}
300
301fn parse_constraints(input: &str) -> IResult<&str, Vec<Constraint>> {
302 many0(alt((
303 map(
305 tuple((tag("^pk"), nom::combinator::not(char('(')))),
306 |_| Constraint::PrimaryKey
307 ),
308 map(
310 tuple((tag("^uniq"), nom::combinator::not(tag("ue(")))),
311 |_| Constraint::Unique
312 ),
313 value(Constraint::Nullable, char('?')),
314 parse_default_constraint,
315 parse_check_constraint,
316 parse_comment_constraint,
317 )))(input)
318}
319
320fn parse_default_constraint(input: &str) -> IResult<&str, Constraint> {
322 let (input, _) = preceded(multispace0, char('='))(input)?;
323 let (input, _) = multispace0(input)?;
324
325 let (input, value) = alt((
327 map(
329 pair(
330 take_while1(|c: char| c.is_alphanumeric() || c == '_'),
331 tag("()")
332 ),
333 |(name, parens): (&str, &str)| format!("{}{}", name, parens)
334 ),
335 map(
337 take_while1(|c: char| c.is_numeric() || c == '.' || c == '-'),
338 |s: &str| s.to_string()
339 ),
340 map(
342 delimited(char('"'), take_until("\""), char('"')),
343 |s: &str| format!("'{}'", s)
344 ),
345 ))(input)?;
346
347 Ok((input, Constraint::Default(value)))
348}
349
350fn parse_check_constraint(input: &str) -> IResult<&str, Constraint> {
352 let (input, _) = tag("^check(")(input)?;
353 let (input, values) = separated_list1(
354 char(','),
355 delimited(
356 multispace0,
357 delimited(char('"'), take_until("\""), char('"')),
358 multispace0
359 )
360 )(input)?;
361 let (input, _) = char(')')(input)?;
362
363 Ok((input, Constraint::Check(values.into_iter().map(|s| s.to_string()).collect())))
364}
365
366fn parse_comment_constraint(input: &str) -> IResult<&str, Constraint> {
368 let (input, _) = tag("^comment(\"")(input)?;
369 let (input, text) = take_until("\"")(input)?;
370 let (input, _) = tag("\")")(input)?;
371 Ok((input, Constraint::Comment(text.to_string())))
372}
373
374fn parse_index_command(input: &str) -> IResult<&str, QailCmd> {
377 let (input, name) = parse_identifier(input)?;
379
380 let (input, _) = tag("^on(")(input)?;
382 let (input, table) = parse_identifier(input)?;
383 let (input, _) = char(':')(input)?;
384 let (input, columns) = parse_index_columns(input)?;
385 let (input, _) = char(')')(input)?;
386
387 let (input, unique) = opt(tag("^unique"))(input)?;
389
390 Ok((input, QailCmd {
391 action: Action::Index,
392 table: table.to_string(),
393 columns: vec![],
394 joins: vec![],
395 cages: vec![],
396 distinct: false,
397 index_def: Some(IndexDef {
398 name: name.to_string(),
399 table: table.to_string(),
400 columns,
401 unique: unique.is_some(),
402 }),
403 table_constraints: vec![],
404 }))
405}
406
407fn parse_index_columns(input: &str) -> IResult<&str, Vec<String>> {
409 let (input, _) = char('\'')(input)?;
410 let (input, first) = parse_identifier(input)?;
411 let (input, rest) = many0(preceded(char('-'), parse_identifier))(input)?;
412
413 let mut cols = vec![first.to_string()];
414 cols.extend(rest.iter().map(|s| s.to_string()));
415 Ok((input, cols))
416}
417
418fn parse_table_constraints(input: &str) -> IResult<&str, Vec<TableConstraint>> {
420 many0(alt((
421 parse_table_unique,
422 parse_table_pk,
423 )))(input)
424}
425
426fn parse_table_unique(input: &str) -> IResult<&str, TableConstraint> {
428 let (input, _) = tag("^unique(")(input)?;
429 let (input, cols) = parse_constraint_columns(input)?;
430 let (input, _) = char(')')(input)?;
431 Ok((input, TableConstraint::Unique(cols)))
432}
433
434fn parse_table_pk(input: &str) -> IResult<&str, TableConstraint> {
436 let (input, _) = tag("^pk(")(input)?;
437 let (input, cols) = parse_constraint_columns(input)?;
438 let (input, _) = char(')')(input)?;
439 Ok((input, TableConstraint::PrimaryKey(cols)))
440}
441
442fn parse_constraint_columns(input: &str) -> IResult<&str, Vec<String>> {
444 let (input, _) = multispace0(input)?;
445 let (input, first) = parse_identifier(input)?;
446 let (input, rest) = many0(preceded(
447 tuple((multispace0, char(','), multispace0)),
448 parse_identifier
449 ))(input)?;
450 let (input, _) = multispace0(input)?;
451
452 let mut cols = vec![first.to_string()];
453 cols.extend(rest.iter().map(|s| s.to_string()));
454 Ok((input, cols))
455}
456
457fn parse_agg_func(input: &str) -> IResult<&str, AggregateFunc> {
458 alt((
459 value(AggregateFunc::Count, tag("count")),
460 value(AggregateFunc::Sum, tag("sum")),
461 value(AggregateFunc::Avg, tag("avg")),
462 value(AggregateFunc::Min, tag("min")),
463 value(AggregateFunc::Max, tag("max")),
464 ))(input)
465}
466
467fn parse_unified_blocks(input: &str) -> IResult<&str, Vec<Cage>> {
470 many0(preceded(ws_or_comment, parse_unified_block))(input)
471}
472
473fn parse_unified_block(input: &str) -> IResult<&str, Cage> {
476 let (input, _) = char('[')(input)?;
477 let (input, _) = ws_or_comment(input)?;
478
479 let (input, items) = parse_block_items(input)?;
481
482 let (input, _) = ws_or_comment(input)?;
483 let (input, _) = char(']')(input)?;
484
485 items_to_cage(items, input)
488}
489
490#[derive(Debug)]
492enum BlockItem {
493 Filter(Condition, LogicalOp),
494 Sort(String, SortOrder),
495 Range(usize, Option<usize>), LegacyLimit(usize),
497 LegacyOffset(usize),
498}
499
500fn parse_block_items(input: &str) -> IResult<&str, Vec<BlockItem>> {
503 let (input, first) = opt(parse_block_item)(input)?;
504
505 match first {
506 None => Ok((input, vec![])),
507 Some(mut item) => {
508 let mut items = vec![];
509 let mut remaining = input;
510
511 loop {
512 let (input, _) = ws_or_comment(remaining)?;
513
514 if let Ok((input, _)) = char::<_, nom::error::Error<&str>>(',')(input) {
516 items.push(item);
518 let (input, _) = ws_or_comment(input)?;
519 let (input, next_item) = parse_block_item(input)?;
520 item = next_item;
521 remaining = input;
522 } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('|')(input) {
523 if let BlockItem::Filter(cond, _) = item {
525 items.push(BlockItem::Filter(cond, LogicalOp::Or));
526 } else {
527 items.push(item);
528 }
529 let (new_input, _) = ws_or_comment(new_input)?;
530 let (new_input, next_item) = parse_filter_item(new_input)?;
531 if let BlockItem::Filter(cond, _) = next_item {
533 item = BlockItem::Filter(cond, LogicalOp::Or);
534 } else {
535 item = next_item;
536 }
537 remaining = new_input;
538 } else if let Ok((new_input, _)) = char::<_, nom::error::Error<&str>>('&')(input) {
539 items.push(item);
541 let (new_input, _) = ws_or_comment(new_input)?;
542 let (new_input, next_item) = parse_filter_item(new_input)?;
543 item = next_item;
544 remaining = new_input;
545 } else {
546 items.push(item);
547 remaining = input;
548 break;
549 }
550 }
551
552 Ok((remaining, items))
553 }
554 }
555}
556
557fn parse_block_item(input: &str) -> IResult<&str, BlockItem> {
559 alt((
560 parse_range_item,
562 parse_sort_item,
564 parse_legacy_limit_item,
566 parse_legacy_offset_item,
568 parse_legacy_sort_item,
570 parse_filter_item,
572 ))(input)
573}
574
575fn parse_range_item(input: &str) -> IResult<&str, BlockItem> {
577 let (input, start) = digit1(input)?;
578 let (input, _) = tag("..")(input)?;
579 let (input, end) = opt(digit1)(input)?;
580
581 let start_num: usize = start.parse().unwrap_or(0);
582 let end_num = end.map(|e| e.parse().unwrap_or(0));
583
584 Ok((input, BlockItem::Range(start_num, end_num)))
585}
586
587fn parse_sort_item(input: &str) -> IResult<&str, BlockItem> {
589 alt((
590 map(preceded(char('+'), parse_identifier), |col| {
591 BlockItem::Sort(col.to_string(), SortOrder::Asc)
592 }),
593 map(preceded(char('-'), parse_identifier), |col| {
594 BlockItem::Sort(col.to_string(), SortOrder::Desc)
595 }),
596 ))(input)
597}
598
599fn parse_legacy_limit_item(input: &str) -> IResult<&str, BlockItem> {
601 let (input, _) = tag("lim")(input)?;
602 let (input, _) = ws_or_comment(input)?;
603 let (input, _) = char('=')(input)?;
604 let (input, _) = ws_or_comment(input)?;
605 let (input, n) = digit1(input)?;
606 Ok((input, BlockItem::LegacyLimit(n.parse().unwrap_or(10))))
607}
608
609fn parse_legacy_offset_item(input: &str) -> IResult<&str, BlockItem> {
611 let (input, _) = tag("off")(input)?;
612 let (input, _) = ws_or_comment(input)?;
613 let (input, _) = char('=')(input)?;
614 let (input, _) = ws_or_comment(input)?;
615 let (input, n) = digit1(input)?;
616 Ok((input, BlockItem::LegacyOffset(n.parse().unwrap_or(0))))
617}
618
619fn parse_legacy_sort_item(input: &str) -> IResult<&str, BlockItem> {
621 let (input, _) = char('^')(input)?;
622 let (input, desc) = opt(char('!'))(input)?;
623 let (input, col) = parse_identifier(input)?;
624
625 let order = if desc.is_some() {
626 SortOrder::Desc
627 } else {
628 SortOrder::Asc
629 };
630
631 Ok((input, BlockItem::Sort(col.to_string(), order)))
632}
633
634fn parse_filter_item(input: &str) -> IResult<&str, BlockItem> {
636 let (input, _) = opt(char('\''))(input)?;
638 let (input, column) = parse_identifier(input)?;
639
640 let (input, is_array_unnest) = if input.starts_with("[*]") {
642 (&input[3..], true)
643 } else {
644 (input, false)
645 };
646
647 let (input, _) = ws_or_comment(input)?;
648 let (input, (op, val)) = parse_operator_and_value(input)?;
649
650 Ok((input, BlockItem::Filter(
651 Condition {
652 column: column.to_string(),
653 op,
654 value: val,
655 is_array_unnest,
656 },
657 LogicalOp::And, )))
659}
660
661fn items_to_cage(items: Vec<BlockItem>, input: &str) -> IResult<&str, Cage> {
663 let mut conditions = Vec::new();
665 let mut logical_op = LogicalOp::And;
666
667 for item in &items {
669 match item {
670 BlockItem::Range(start, end) => {
671 if let Some(e) = end {
676 let limit = e - start;
677 let offset = *start;
678 if offset == 0 {
682 return Ok((input, Cage {
683 kind: CageKind::Limit(limit),
684 conditions: vec![],
685 logical_op: LogicalOp::And,
686 }));
687 } else {
688 return Ok((input, Cage {
694 kind: CageKind::Limit(limit),
695 conditions: vec![Condition {
696 column: "__offset__".to_string(),
697 op: Operator::Eq,
698 value: Value::Int(offset as i64),
699 is_array_unnest: false,
700 }],
701 logical_op: LogicalOp::And,
702 }));
703 }
704 } else {
705 return Ok((input, Cage {
707 kind: CageKind::Offset(*start),
708 conditions: vec![],
709 logical_op: LogicalOp::And,
710 }));
711 }
712 }
713 BlockItem::Sort(col, order) => {
714 return Ok((input, Cage {
715 kind: CageKind::Sort(*order),
716 conditions: vec![Condition {
717 column: col.clone(),
718 op: Operator::Eq,
719 value: Value::Null,
720 is_array_unnest: false,
721 }],
722 logical_op: LogicalOp::And,
723 }));
724 }
725 BlockItem::LegacyLimit(n) => {
726 return Ok((input, Cage {
727 kind: CageKind::Limit(*n),
728 conditions: vec![],
729 logical_op: LogicalOp::And,
730 }));
731 }
732 BlockItem::LegacyOffset(n) => {
733 return Ok((input, Cage {
734 kind: CageKind::Offset(*n),
735 conditions: vec![],
736 logical_op: LogicalOp::And,
737 }));
738 }
739 BlockItem::Filter(cond, op) => {
740 conditions.push(cond.clone());
741 logical_op = *op;
742 }
743 }
744 }
745
746 if !conditions.is_empty() {
748 Ok((input, Cage {
749 kind: CageKind::Filter,
750 conditions,
751 logical_op,
752 }))
753 } else {
754 Ok((input, Cage {
756 kind: CageKind::Filter,
757 conditions: vec![],
758 logical_op: LogicalOp::And,
759 }))
760 }
761}
762
763fn parse_operator_and_value(input: &str) -> IResult<&str, (Operator, Value)> {
765 alt((
766 map(preceded(char('~'), preceded(ws_or_comment, parse_value)), |v| (Operator::Fuzzy, v)),
768 map(preceded(tag("=="), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
770 map(preceded(tag(">="), preceded(ws_or_comment, parse_value)), |v| (Operator::Gte, v)),
772 map(preceded(tag("<="), preceded(ws_or_comment, parse_value)), |v| (Operator::Lte, v)),
774 map(preceded(tag("!="), preceded(ws_or_comment, parse_value)), |v| (Operator::Ne, v)),
776 map(preceded(char('>'), preceded(ws_or_comment, parse_value)), |v| (Operator::Gt, v)),
778 map(preceded(char('<'), preceded(ws_or_comment, parse_value)), |v| (Operator::Lt, v)),
780 map(preceded(char('='), preceded(ws_or_comment, parse_value)), |v| (Operator::Eq, v)),
782 ))(input)
783}
784
785fn parse_value(input: &str) -> IResult<&str, Value> {
787 let (input, _) = ws_or_comment(input)?;
788
789 alt((
790 map(preceded(char('$'), digit1), |n: &str| {
792 Value::Param(n.parse().unwrap_or(1))
793 }),
794 value(Value::Bool(true), tag("true")),
796 value(Value::Bool(false), tag("false")),
797 parse_function_call,
799 map(tag("now"), |_| Value::Function("now".to_string())),
801 parse_number,
803 parse_double_quoted_string,
805 parse_quoted_string,
807 map(parse_identifier, |s| Value::String(s.to_string())),
809 ))(input)
810}
811
812fn parse_function_call(input: &str) -> IResult<&str, Value> {
814 let (input, name) = parse_identifier(input)?;
815 let (input, _) = char('(')(input)?;
816 let (input, _) = ws_or_comment(input)?;
817 let (input, args) = opt(tuple((
818 parse_value,
819 many0(preceded(
820 tuple((ws_or_comment, char(','), ws_or_comment)),
821 parse_value
822 ))
823 )))(input)?;
824 let (input, _) = ws_or_comment(input)?;
825 let (input, _) = char(')')(input)?;
826
827 let params = match args {
828 Some((first, mut rest)) => {
829 let mut v = vec![first];
830 v.append(&mut rest);
831 v
832 },
833 None => vec![],
834 };
835
836 Ok((input, Value::Function(format!("{}({})", name, params.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(", ")))))
837}
838
839fn parse_number(input: &str) -> IResult<&str, Value> {
841 let (input, num_str) = recognize(tuple((
842 opt(char('-')),
843 digit1,
844 opt(pair(char('.'), digit1)),
845 )))(input)?;
846
847 if num_str.contains('.') {
848 Ok((input, Value::Float(num_str.parse().unwrap_or(0.0))))
849 } else {
850 Ok((input, Value::Int(num_str.parse().unwrap_or(0))))
851 }
852}
853
854fn parse_quoted_string(input: &str) -> IResult<&str, Value> {
856 let (input, _) = char('\'')(input)?;
857 let (input, content) = take_while(|c| c != '\'')(input)?;
858 let (input, _) = char('\'')(input)?;
859
860 Ok((input, Value::String(content.to_string())))
861}
862
863fn parse_double_quoted_string(input: &str) -> IResult<&str, Value> {
865 let (input, _) = char('"')(input)?;
866 let (input, content) = take_while(|c| c != '"')(input)?;
867 let (input, _) = char('"')(input)?;
868
869 Ok((input, Value::String(content.to_string())))
870}
871
872fn parse_legacy_sort_cage(input: &str) -> IResult<&str, Cage> {
874 let (input, _) = char('^')(input)?;
875 let (input, desc) = opt(char('!'))(input)?;
876 let (input, col) = parse_identifier(input)?;
877
878 let order = if desc.is_some() {
879 SortOrder::Desc
880 } else {
881 SortOrder::Asc
882 };
883
884 Ok((
885 input,
886 Cage {
887 kind: CageKind::Sort(order),
888 conditions: vec![Condition {
889 column: col.to_string(),
890 op: Operator::Eq,
891 value: Value::Null,
892 is_array_unnest: false,
893 }],
894 logical_op: LogicalOp::And,
895 },
896 ))
897}
898
899fn parse_partition_block(input: &str) -> IResult<&str, Vec<String>> {
900 let (input, _) = char('{')(input)?;
901 let (input, _) = ws_or_comment(input)?;
902 let (input, _) = tag("Part")(input)?;
903 let (input, _) = ws_or_comment(input)?;
904 let (input, _) = char('=')(input)?;
905 let (input, _) = ws_or_comment(input)?;
906
907 let (input, first) = parse_identifier(input)?;
908 let (input, rest) = many0(preceded(
909 tuple((ws_or_comment, char(','), ws_or_comment)),
910 parse_identifier
911 ))(input)?;
912
913 let (input, _) = ws_or_comment(input)?;
914 let (input, _) = char('}')(input)?;
915
916 let mut cols = vec![first.to_string()];
917 cols.append(&mut rest.iter().map(|s| s.to_string()).collect());
918 Ok((input, cols))
919}
920
921#[cfg(test)]
926mod tests {
927 use super::*;
928
929 #[test]
934 fn test_v2_simple_get() {
935 let cmd = parse("get::users:'_").unwrap();
936 assert_eq!(cmd.action, Action::Get);
937 assert_eq!(cmd.table, "users");
938 assert_eq!(cmd.columns, vec![Column::Star]);
939 }
940
941 #[test]
942 fn test_v2_get_with_columns() {
943 let cmd = parse("get::users:'id'email").unwrap();
944 assert_eq!(cmd.action, Action::Get);
945 assert_eq!(cmd.table, "users");
946 assert_eq!(
947 cmd.columns,
948 vec![
949 Column::Named("id".to_string()),
950 Column::Named("email".to_string()),
951 ]
952 );
953 }
954
955 #[test]
956 fn test_v2_get_with_filter() {
957 let cmd = parse("get::users:'_ [ 'active == true ]").unwrap();
958 assert_eq!(cmd.cages.len(), 1);
959 assert_eq!(cmd.cages[0].kind, CageKind::Filter);
960 assert_eq!(cmd.cages[0].conditions.len(), 1);
961 assert_eq!(cmd.cages[0].conditions[0].column, "active");
962 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
963 assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
964 }
965
966 #[test]
967 fn test_v2_get_with_range_limit() {
968 let cmd = parse("get::users:'_ [ 0..10 ]").unwrap();
969 assert_eq!(cmd.cages.len(), 1);
970 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
971 }
972
973 #[test]
974 fn test_v2_get_with_range_offset() {
975 let cmd = parse("get::users:'_ [ 20..30 ]").unwrap();
976 assert_eq!(cmd.cages.len(), 1);
977 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
979 assert_eq!(cmd.cages[0].conditions[0].column, "__offset__");
981 assert_eq!(cmd.cages[0].conditions[0].value, Value::Int(20));
982 }
983
984 #[test]
985 fn test_v2_get_with_sort_desc() {
986 let cmd = parse("get::users:'_ [ -created_at ]").unwrap();
987 assert_eq!(cmd.cages.len(), 1);
988 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
989 assert_eq!(cmd.cages[0].conditions[0].column, "created_at");
990 }
991
992 #[test]
993 fn test_v2_get_with_sort_asc() {
994 let cmd = parse("get::users:'_ [ +id ]").unwrap();
995 assert_eq!(cmd.cages.len(), 1);
996 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Asc));
997 assert_eq!(cmd.cages[0].conditions[0].column, "id");
998 }
999
1000 #[test]
1001 fn test_v2_fuzzy_match() {
1002 let cmd = parse("get::users:'id [ 'name ~ \"john\" ]").unwrap();
1003 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
1004 assert_eq!(cmd.cages[0].conditions[0].value, Value::String("john".to_string()));
1005 }
1006
1007 #[test]
1008 fn test_v2_param_in_filter() {
1009 let cmd = parse("get::users:'id [ 'email == $1 ]").unwrap();
1010 assert_eq!(cmd.cages.len(), 1);
1011 assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
1012 }
1013
1014 #[test]
1015 fn test_v2_left_join() {
1016 let cmd = parse("get::users<-posts:'id'title").unwrap();
1018 assert_eq!(cmd.joins.len(), 1);
1019 assert_eq!(cmd.joins[0].table, "posts");
1020 assert_eq!(cmd.joins[0].kind, JoinKind::Left);
1021 }
1022
1023 #[test]
1024 fn test_v2_inner_join() {
1025 let cmd = parse("get::users->posts:'id'title").unwrap();
1026 assert_eq!(cmd.joins.len(), 1);
1027 assert_eq!(cmd.joins[0].table, "posts");
1028 assert_eq!(cmd.joins[0].kind, JoinKind::Inner);
1029 }
1030
1031 #[test]
1032 fn test_v2_right_join() {
1033 let cmd = parse("get::orders->>customers:'_").unwrap();
1034 assert_eq!(cmd.joins.len(), 1);
1035 assert_eq!(cmd.joins[0].table, "customers");
1036 assert_eq!(cmd.joins[0].kind, JoinKind::Right);
1037 }
1038
1039 #[test]
1044 fn test_legacy_simple_get() {
1045 let cmd = parse("get::users:@*").unwrap();
1046 assert_eq!(cmd.action, Action::Get);
1047 assert_eq!(cmd.table, "users");
1048 assert_eq!(cmd.columns, vec![Column::Star]);
1049 }
1050
1051 #[test]
1052 fn test_legacy_get_with_columns() {
1053 let cmd = parse("get::users:@id@email@role").unwrap();
1054 assert_eq!(cmd.action, Action::Get);
1055 assert_eq!(cmd.table, "users");
1056 assert_eq!(
1057 cmd.columns,
1058 vec![
1059 Column::Named("id".to_string()),
1060 Column::Named("email".to_string()),
1061 Column::Named("role".to_string()),
1062 ]
1063 );
1064 }
1065
1066 #[test]
1067 fn test_legacy_get_with_filter() {
1068 let cmd = parse("get::users:@*[active=true]").unwrap();
1069 assert_eq!(cmd.cages.len(), 1);
1070 assert_eq!(cmd.cages[0].kind, CageKind::Filter);
1071 assert_eq!(cmd.cages[0].conditions.len(), 1);
1072 assert_eq!(cmd.cages[0].conditions[0].column, "active");
1073 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Eq);
1074 assert_eq!(cmd.cages[0].conditions[0].value, Value::Bool(true));
1075 }
1076
1077 #[test]
1078 fn test_legacy_get_with_limit() {
1079 let cmd = parse("get::users:@*[lim=10]").unwrap();
1080 assert_eq!(cmd.cages.len(), 1);
1081 assert_eq!(cmd.cages[0].kind, CageKind::Limit(10));
1082 }
1083
1084 #[test]
1085 fn test_legacy_get_with_sort_desc() {
1086 let cmd = parse("get::users:@*[^!created_at]").unwrap();
1087 assert_eq!(cmd.cages.len(), 1);
1088 assert_eq!(cmd.cages[0].kind, CageKind::Sort(SortOrder::Desc));
1089 }
1090
1091 #[test]
1092 fn test_set_command() {
1093 let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
1094 assert_eq!(cmd.action, Action::Set);
1095 assert_eq!(cmd.table, "users");
1096 assert_eq!(cmd.cages.len(), 2);
1097 }
1098
1099 #[test]
1100 fn test_del_command() {
1101 let cmd = parse("del::sessions:[expired_at<now]").unwrap();
1102 assert_eq!(cmd.action, Action::Del);
1103 assert_eq!(cmd.table, "sessions");
1104 }
1105
1106 #[test]
1107 fn test_legacy_fuzzy_match() {
1108 let cmd = parse("get::users:@*[name~$1]").unwrap();
1109 assert_eq!(cmd.cages[0].conditions[0].op, Operator::Fuzzy);
1110 }
1111
1112 #[test]
1113 fn test_legacy_complex_query() {
1114 let cmd = parse("get::users:@id@email@role[active=true][lim=10]").unwrap();
1115 assert_eq!(cmd.action, Action::Get);
1116 assert_eq!(cmd.table, "users");
1117 assert_eq!(cmd.columns.len(), 3);
1118 assert_eq!(cmd.cages.len(), 2);
1119 }
1120
1121 #[test]
1122 fn test_legacy_param_in_filter() {
1123 let cmd = parse("get::users:@*[id=$1]").unwrap();
1124 assert_eq!(cmd.cages.len(), 1);
1125 assert_eq!(cmd.cages[0].conditions[0].value, Value::Param(1));
1126 }
1127
1128 #[test]
1129 fn test_legacy_param_in_update() {
1130 let cmd = parse("set::users:[verified=true][id=$1]").unwrap();
1131 assert_eq!(cmd.action, Action::Set);
1132 assert_eq!(cmd.cages.len(), 2);
1133 assert_eq!(cmd.cages[1].conditions[0].value, Value::Param(1));
1134 }
1135
1136 #[test]
1141 fn test_make_with_default_uuid() {
1142 let cmd = parse("make::users:'id:uuid^pk = uuid()").unwrap();
1143 assert_eq!(cmd.action, Action::Make);
1144 assert_eq!(cmd.table, "users");
1145 assert_eq!(cmd.columns.len(), 1);
1146 if let Column::Def { name, data_type, constraints } = &cmd.columns[0] {
1147 assert_eq!(name, "id");
1148 assert_eq!(data_type, "uuid");
1149 assert!(constraints.contains(&Constraint::PrimaryKey));
1150 assert!(constraints.iter().any(|c| matches!(c, Constraint::Default(v) if v == "uuid()")));
1151 } else {
1152 panic!("Expected Column::Def");
1153 }
1154 }
1155
1156 #[test]
1157 fn test_make_with_default_numeric() {
1158 let cmd = parse("make::stats:'count:bigint = 0").unwrap();
1159 assert_eq!(cmd.action, Action::Make);
1160 if let Column::Def { constraints, .. } = &cmd.columns[0] {
1161 assert!(constraints.iter().any(|c| matches!(c, Constraint::Default(v) if v == "0")));
1162 } else {
1163 panic!("Expected Column::Def");
1164 }
1165 }
1166
1167 #[test]
1168 fn test_make_with_check_constraint() {
1169 let cmd = parse(r#"make::orders:'status:varchar^check("pending","paid","cancelled")"#).unwrap();
1170 assert_eq!(cmd.action, Action::Make);
1171 if let Column::Def { name, constraints, .. } = &cmd.columns[0] {
1172 assert_eq!(name, "status");
1173 let check = constraints.iter().find(|c| matches!(c, Constraint::Check(_)));
1174 assert!(check.is_some());
1175 if let Some(Constraint::Check(vals)) = check {
1176 assert_eq!(vals, &vec!["pending".to_string(), "paid".to_string(), "cancelled".to_string()]);
1177 }
1178 } else {
1179 panic!("Expected Column::Def");
1180 }
1181 }
1182
1183 #[test]
1188 fn test_index_basic() {
1189 let cmd = parse("index::idx_users_email^on(users:'email)").unwrap();
1190 assert_eq!(cmd.action, Action::Index);
1191 let idx = cmd.index_def.expect("index_def should be Some");
1192 assert_eq!(idx.name, "idx_users_email");
1193 assert_eq!(idx.table, "users");
1194 assert_eq!(idx.columns, vec!["email".to_string()]);
1195 assert!(!idx.unique);
1196 }
1197
1198 #[test]
1199 fn test_index_composite() {
1200 let cmd = parse("index::idx_lookup^on(orders:'user_id-created_at)").unwrap();
1201 assert_eq!(cmd.action, Action::Index);
1202 let idx = cmd.index_def.expect("index_def should be Some");
1203 assert_eq!(idx.name, "idx_lookup");
1204 assert_eq!(idx.table, "orders");
1205 assert_eq!(idx.columns, vec!["user_id".to_string(), "created_at".to_string()]);
1206 }
1207
1208 #[test]
1209 fn test_index_unique() {
1210 let cmd = parse("index::idx_phone^on(users:'phone)^unique").unwrap();
1211 assert_eq!(cmd.action, Action::Index);
1212 let idx = cmd.index_def.expect("index_def should be Some");
1213 assert_eq!(idx.name, "idx_phone");
1214 assert!(idx.unique);
1215 }
1216
1217 #[test]
1222 fn test_make_composite_unique() {
1223 let cmd = parse("make::bookings:'user_id:uuid'schedule_id:uuid^unique(user_id, schedule_id)").unwrap();
1224 assert_eq!(cmd.action, Action::Make);
1225 assert_eq!(cmd.table_constraints.len(), 1);
1226 if let TableConstraint::Unique(cols) = &cmd.table_constraints[0] {
1227 assert_eq!(cols, &vec!["user_id".to_string(), "schedule_id".to_string()]);
1228 } else {
1229 panic!("Expected TableConstraint::Unique");
1230 }
1231 }
1232
1233 #[test]
1234 fn test_make_composite_pk() {
1235 let cmd = parse("make::order_items:'order_id:uuid'product_id:uuid^pk(order_id, product_id)").unwrap();
1236 assert_eq!(cmd.action, Action::Make);
1237 assert_eq!(cmd.table_constraints.len(), 1);
1238 if let TableConstraint::PrimaryKey(cols) = &cmd.table_constraints[0] {
1239 assert_eq!(cols, &vec!["order_id".to_string(), "product_id".to_string()]);
1240 } else {
1241 panic!("Expected TableConstraint::PrimaryKey");
1242 }
1243 }
1244}