1use crate::ast::*;
10use crate::error::{GraphError, Result};
11use nom::{
12 branch::alt,
13 bytes::complete::{tag, tag_no_case, take_while1},
14 character::complete::{char, multispace0, multispace1},
15 combinator::{map, opt, recognize},
16 multi::{many0, separated_list0, separated_list1},
17 sequence::{delimited, pair, preceded, tuple},
18 IResult,
19};
20use std::collections::HashMap;
21
22pub fn parse_cypher_query(input: &str) -> Result<CypherQuery> {
24 let (remaining, query) = cypher_query(input).map_err(|e| GraphError::ParseError {
25 message: format!("Failed to parse Cypher query: {}", e),
26 position: 0,
27 location: snafu::Location::new(file!(), line!(), column!()),
28 })?;
29
30 if !remaining.trim().is_empty() {
31 return Err(GraphError::ParseError {
32 message: format!("Unexpected input after query: {}", remaining),
33 position: input.len() - remaining.len(),
34 location: snafu::Location::new(file!(), line!(), column!()),
35 });
36 }
37
38 Ok(query)
39}
40
41fn cypher_query(input: &str) -> IResult<&str, CypherQuery> {
43 let (input, _) = multispace0(input)?;
44 let (input, match_clauses) = many0(match_clause)(input)?;
45 let (input, where_clause) = opt(where_clause)(input)?;
46 let (input, return_clause) = return_clause(input)?;
47 let (input, order_by) = opt(order_by_clause)(input)?;
48 let (input, (skip, limit)) = pagination_clauses(input)?;
49 let (input, _) = multispace0(input)?;
50
51 Ok((
52 input,
53 CypherQuery {
54 match_clauses,
55 where_clause,
56 return_clause,
57 limit,
58 order_by,
59 skip,
60 },
61 ))
62}
63
64fn match_clause(input: &str) -> IResult<&str, MatchClause> {
66 let (input, _) = multispace0(input)?;
67 let (input, _) = tag_no_case("MATCH")(input)?;
68 let (input, _) = multispace1(input)?;
69 let (input, patterns) = separated_list0(comma_ws, graph_pattern)(input)?;
70
71 Ok((input, MatchClause { patterns }))
72}
73
74fn graph_pattern(input: &str) -> IResult<&str, GraphPattern> {
76 alt((
77 map(path_pattern, GraphPattern::Path),
78 map(node_pattern, GraphPattern::Node),
79 ))(input)
80}
81
82fn path_pattern(input: &str) -> IResult<&str, PathPattern> {
84 let (input, start_node) = node_pattern(input)?;
85 let (input, segments) = many0(path_segment)(input)?;
86
87 if segments.is_empty() {
89 return Err(nom::Err::Error(nom::error::Error::new(
90 input,
91 nom::error::ErrorKind::Tag,
92 )));
93 }
94
95 Ok((
96 input,
97 PathPattern {
98 start_node,
99 segments,
100 },
101 ))
102}
103
104fn path_segment(input: &str) -> IResult<&str, PathSegment> {
106 let (input, relationship) = relationship_pattern(input)?;
107 let (input, end_node) = node_pattern(input)?;
108
109 Ok((
110 input,
111 PathSegment {
112 relationship,
113 end_node,
114 },
115 ))
116}
117
118fn node_pattern(input: &str) -> IResult<&str, NodePattern> {
120 let (input, _) = multispace0(input)?;
121 let (input, _) = char('(')(input)?;
122 let (input, _) = multispace0(input)?;
123 let (input, variable) = opt(identifier)(input)?;
124 let (input, labels) = many0(preceded(char(':'), identifier))(input)?;
125 let (input, _) = multispace0(input)?;
126 let (input, properties) = opt(property_map)(input)?;
127 let (input, _) = multispace0(input)?;
128 let (input, _) = char(')')(input)?;
129
130 Ok((
131 input,
132 NodePattern {
133 variable: variable.map(|s| s.to_string()),
134 labels: labels.into_iter().map(|s| s.to_string()).collect(),
135 properties: properties.unwrap_or_default(),
136 },
137 ))
138}
139
140fn relationship_pattern(input: &str) -> IResult<&str, RelationshipPattern> {
142 let (input, _) = multispace0(input)?;
143
144 let (input, (direction, content)) = alt((
146 map(
148 tuple((
149 char('-'),
150 delimited(char('['), relationship_content, char(']')),
151 tag("->"),
152 )),
153 |(_, content, _)| (RelationshipDirection::Outgoing, content),
154 ),
155 map(
157 tuple((
158 tag("<-"),
159 delimited(char('['), relationship_content, char(']')),
160 char('-'),
161 )),
162 |(_, content, _)| (RelationshipDirection::Incoming, content),
163 ),
164 map(
166 tuple((
167 char('-'),
168 delimited(char('['), relationship_content, char(']')),
169 char('-'),
170 )),
171 |(_, content, _)| (RelationshipDirection::Undirected, content),
172 ),
173 ))(input)?;
174
175 let (variable, types, properties, length) = content;
176
177 Ok((
178 input,
179 RelationshipPattern {
180 variable: variable.map(|s| s.to_string()),
181 types: types.into_iter().map(|s| s.to_string()).collect(),
182 direction,
183 properties: properties.unwrap_or_default(),
184 length,
185 },
186 ))
187}
188
189type RelationshipContentResult<'a> = (
191 Option<&'a str>,
192 Vec<&'a str>,
193 Option<HashMap<String, PropertyValue>>,
194 Option<LengthRange>,
195);
196
197fn relationship_content(input: &str) -> IResult<&str, RelationshipContentResult<'_>> {
199 let (input, _) = multispace0(input)?;
200 let (input, variable) = opt(identifier)(input)?;
201 let (input, types) = many0(preceded(char(':'), identifier))(input)?;
202 let (input, _) = multispace0(input)?;
203 let (input, length) = opt(length_range)(input)?;
204 let (input, _) = multispace0(input)?;
205 let (input, properties) = opt(property_map)(input)?;
206 let (input, _) = multispace0(input)?;
207
208 Ok((input, (variable, types, properties, length)))
209}
210
211fn property_map(input: &str) -> IResult<&str, HashMap<String, PropertyValue>> {
213 let (input, _) = multispace0(input)?;
214 let (input, _) = char('{')(input)?;
215 let (input, _) = multispace0(input)?;
216 let (input, pairs) = separated_list0(comma_ws, property_pair)(input)?;
217 let (input, _) = multispace0(input)?;
218 let (input, _) = char('}')(input)?;
219
220 Ok((input, pairs.into_iter().collect()))
221}
222
223fn property_pair(input: &str) -> IResult<&str, (String, PropertyValue)> {
225 let (input, _) = multispace0(input)?;
226 let (input, key) = identifier(input)?;
227 let (input, _) = multispace0(input)?;
228 let (input, _) = char(':')(input)?;
229 let (input, _) = multispace0(input)?;
230 let (input, value) = property_value(input)?;
231
232 Ok((input, (key.to_string(), value)))
233}
234
235fn property_value(input: &str) -> IResult<&str, PropertyValue> {
237 alt((
238 map(string_literal, PropertyValue::String),
239 map(integer_literal, PropertyValue::Integer),
240 map(float_literal, PropertyValue::Float),
241 map(boolean_literal, PropertyValue::Boolean),
242 map(tag("null"), |_| PropertyValue::Null),
243 map(parameter, PropertyValue::Parameter),
244 ))(input)
245}
246
247fn where_clause(input: &str) -> IResult<&str, WhereClause> {
249 let (input, _) = multispace0(input)?;
250 let (input, _) = tag_no_case("WHERE")(input)?;
251 let (input, _) = multispace1(input)?;
252 let (input, expression) = boolean_expression(input)?;
253
254 Ok((input, WhereClause { expression }))
255}
256
257fn boolean_expression(input: &str) -> IResult<&str, BooleanExpression> {
259 boolean_or_expression(input)
260}
261
262fn boolean_or_expression(input: &str) -> IResult<&str, BooleanExpression> {
263 let (input, first) = boolean_and_expression(input)?;
264 let (input, rest) = many0(preceded(
265 tuple((multispace0, tag_no_case("OR"), multispace1)),
266 boolean_and_expression,
267 ))(input)?;
268 let expr = rest.into_iter().fold(first, |acc, item| {
269 BooleanExpression::Or(Box::new(acc), Box::new(item))
270 });
271 Ok((input, expr))
272}
273
274fn boolean_and_expression(input: &str) -> IResult<&str, BooleanExpression> {
275 let (input, first) = boolean_not_expression(input)?;
276 let (input, rest) = many0(preceded(
277 tuple((multispace0, tag_no_case("AND"), multispace1)),
278 boolean_not_expression,
279 ))(input)?;
280 let expr = rest.into_iter().fold(first, |acc, item| {
281 BooleanExpression::And(Box::new(acc), Box::new(item))
282 });
283 Ok((input, expr))
284}
285
286fn boolean_not_expression(input: &str) -> IResult<&str, BooleanExpression> {
287 let (input, _) = multispace0(input)?;
288 alt((
289 map(
290 preceded(
291 tuple((tag_no_case("NOT"), multispace1)),
292 boolean_not_expression,
293 ),
294 |expr| BooleanExpression::Not(Box::new(expr)),
295 ),
296 boolean_primary_expression,
297 ))(input)
298}
299
300fn boolean_primary_expression(input: &str) -> IResult<&str, BooleanExpression> {
301 let (input, _) = multispace0(input)?;
302 alt((
303 map(
304 delimited(
305 tuple((char('('), multispace0)),
306 boolean_expression,
307 tuple((multispace0, char(')'))),
308 ),
309 |expr| expr,
310 ),
311 comparison_expression,
312 ))(input)
313}
314
315fn comparison_expression(input: &str) -> IResult<&str, BooleanExpression> {
316 let (input, _) = multispace0(input)?;
317 let (input, left) = value_expression(input)?;
318 let (input, _) = multispace0(input)?;
319 let left_clone = left.clone();
320
321 if let Ok((input_after_in, (_, _, list))) =
322 tuple((tag_no_case("IN"), multispace0, value_expression_list))(input)
323 {
324 return Ok((
325 input_after_in,
326 BooleanExpression::In {
327 expression: left,
328 list,
329 },
330 ));
331 }
332 if let Ok((rest, ())) = is_null_comparison(input) {
334 return Ok((rest, BooleanExpression::IsNull(left_clone)));
335 }
336 if let Ok((rest, ())) = is_not_null_comparison(input) {
338 return Ok((rest, BooleanExpression::IsNotNull(left_clone)));
339 }
340
341 let (input, operator) = comparison_operator(input)?;
342 let (input, _) = multispace0(input)?;
343 let (input, right) = value_expression(input)?;
344
345 Ok((
346 input,
347 BooleanExpression::Comparison {
348 left: left_clone,
349 operator,
350 right,
351 },
352 ))
353}
354
355fn comparison_operator(input: &str) -> IResult<&str, ComparisonOperator> {
357 alt((
358 map(tag("="), |_| ComparisonOperator::Equal),
359 map(tag("<>"), |_| ComparisonOperator::NotEqual),
360 map(tag("!="), |_| ComparisonOperator::NotEqual),
361 map(tag("<="), |_| ComparisonOperator::LessThanOrEqual),
362 map(tag(">="), |_| ComparisonOperator::GreaterThanOrEqual),
363 map(tag("<"), |_| ComparisonOperator::LessThan),
364 map(tag(">"), |_| ComparisonOperator::GreaterThan),
365 ))(input)
366}
367
368fn value_expression(input: &str) -> IResult<&str, ValueExpression> {
370 alt((
371 function_call,
372 map(property_reference, ValueExpression::Property),
373 map(property_value, ValueExpression::Literal),
374 map(identifier, |id| ValueExpression::Variable(id.to_string())),
375 ))(input)
376}
377
378fn function_call(input: &str) -> IResult<&str, ValueExpression> {
380 let (input, name) = identifier(input)?;
381 let (input, _) = multispace0(input)?;
382 let (input, _) = char('(')(input)?;
383 let (input, _) = multispace0(input)?;
384
385 if let Ok((input_after_star, _)) = char::<_, nom::error::Error<&str>>('*')(input) {
387 if name.to_lowercase() == "count" {
389 let (input, _) = multispace0(input_after_star)?;
390 let (input, _) = char(')')(input)?;
391 return Ok((
392 input,
393 ValueExpression::Function {
394 name: name.to_string(),
395 args: vec![ValueExpression::Variable("*".to_string())],
396 },
397 ));
398 } else {
399 }
402 }
403
404 let (input, args) = separated_list0(
406 tuple((multispace0, char(','), multispace0)),
407 value_expression,
408 )(input)?;
409 let (input, _) = multispace0(input)?;
410 let (input, _) = char(')')(input)?;
411
412 Ok((
413 input,
414 ValueExpression::Function {
415 name: name.to_string(),
416 args,
417 },
418 ))
419}
420
421fn value_expression_list(input: &str) -> IResult<&str, Vec<ValueExpression>> {
422 delimited(
423 tuple((char('['), multispace0)),
424 separated_list1(
425 tuple((multispace0, char(','), multispace0)),
426 value_expression,
427 ),
428 tuple((multispace0, char(']'))),
429 )(input)
430}
431
432fn property_reference(input: &str) -> IResult<&str, PropertyRef> {
434 let (input, variable) = identifier(input)?;
435 let (input, _) = char('.')(input)?;
436 let (input, property) = identifier(input)?;
437
438 Ok((
439 input,
440 PropertyRef {
441 variable: variable.to_string(),
442 property: property.to_string(),
443 },
444 ))
445}
446
447fn return_clause(input: &str) -> IResult<&str, ReturnClause> {
449 let (input, _) = multispace0(input)?;
450 let (input, _) = tag_no_case("RETURN")(input)?;
451 let (input, _) = multispace1(input)?;
452 let (input, distinct) = opt(tag_no_case("DISTINCT"))(input)?;
453 let (input, _) = if distinct.is_some() {
454 multispace1(input)?
455 } else {
456 (input, "")
457 };
458 let (input, items) = separated_list0(comma_ws, return_item)(input)?;
459
460 Ok((
461 input,
462 ReturnClause {
463 distinct: distinct.is_some(),
464 items,
465 },
466 ))
467}
468
469fn return_item(input: &str) -> IResult<&str, ReturnItem> {
471 let (input, expression) = value_expression(input)?;
472 let (input, _) = multispace0(input)?;
473 let (input, alias) = opt(preceded(
474 tuple((tag_no_case("AS"), multispace1)),
475 identifier,
476 ))(input)?;
477
478 Ok((
479 input,
480 ReturnItem {
481 expression,
482 alias: alias.map(|s| s.to_string()),
483 },
484 ))
485}
486
487fn is_null_comparison(input: &str) -> IResult<&str, ()> {
489 let (input, _) = multispace0(input)?;
490 let (input, _) = tag_no_case("IS")(input)?;
491 let (input, _) = multispace1(input)?;
492 let (input, _) = tag_no_case("NULL")(input)?;
493 let (input, _) = multispace0(input)?;
494
495 Ok((input, ()))
496}
497
498fn is_not_null_comparison(input: &str) -> IResult<&str, ()> {
500 let (input, _) = multispace0(input)?;
501 let (input, _) = tag_no_case("IS")(input)?;
502 let (input, _) = multispace1(input)?;
503 let (input, _) = tag_no_case("NOT")(input)?;
504 let (input, _) = multispace1(input)?;
505 let (input, _) = tag_no_case("NULL")(input)?;
506 let (input, _) = multispace0(input)?;
507
508 Ok((input, ()))
509}
510
511fn order_by_clause(input: &str) -> IResult<&str, OrderByClause> {
513 let (input, _) = multispace0(input)?;
514 let (input, _) = tag_no_case("ORDER")(input)?;
515 let (input, _) = multispace1(input)?;
516 let (input, _) = tag_no_case("BY")(input)?;
517 let (input, _) = multispace1(input)?;
518 let (input, items) = separated_list0(comma_ws, order_by_item)(input)?;
519
520 Ok((input, OrderByClause { items }))
521}
522
523fn order_by_item(input: &str) -> IResult<&str, OrderByItem> {
525 let (input, expression) = value_expression(input)?;
526 let (input, _) = multispace0(input)?;
527 let (input, direction) = opt(alt((
528 map(tag_no_case("ASC"), |_| SortDirection::Ascending),
529 map(tag_no_case("DESC"), |_| SortDirection::Descending),
530 )))(input)?;
531
532 Ok((
533 input,
534 OrderByItem {
535 expression,
536 direction: direction.unwrap_or(SortDirection::Ascending),
537 },
538 ))
539}
540
541fn limit_clause(input: &str) -> IResult<&str, u64> {
543 let (input, _) = multispace0(input)?;
544 let (input, _) = tag_no_case("LIMIT")(input)?;
545 let (input, _) = multispace1(input)?;
546 let (input, limit) = integer_literal(input)?;
547
548 Ok((input, limit as u64))
549}
550
551fn skip_clause(input: &str) -> IResult<&str, u64> {
553 let (input, _) = multispace0(input)?;
554 let (input, _) = tag_no_case("SKIP")(input)?;
555 let (input, _) = multispace1(input)?;
556 let (input, skip) = integer_literal(input)?;
557
558 Ok((input, skip as u64))
559}
560
561fn pagination_clauses(input: &str) -> IResult<&str, (Option<u64>, Option<u64>)> {
563 let (mut remaining, _) = multispace0(input)?;
564 let mut skip: Option<u64> = None;
565 let mut limit: Option<u64> = None;
566
567 loop {
568 let before = remaining;
569
570 if skip.is_none() {
571 if let Ok((i, s)) = skip_clause(remaining) {
572 skip = Some(s);
573 remaining = i;
574 continue;
575 }
576 }
577
578 if limit.is_none() {
579 if let Ok((i, l)) = limit_clause(remaining) {
580 limit = Some(l);
581 remaining = i;
582 continue;
583 }
584 }
585
586 if before == remaining {
587 break;
588 }
589 }
590
591 Ok((remaining, (skip, limit)))
592}
593
594fn identifier(input: &str) -> IResult<&str, &str> {
598 take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
599}
600
601fn string_literal(input: &str) -> IResult<&str, String> {
603 alt((double_quoted_string, single_quoted_string))(input)
604}
605
606fn double_quoted_string(input: &str) -> IResult<&str, String> {
607 let (input, _) = char('"')(input)?;
608 let (input, content) = take_while1(|c| c != '"')(input)?;
609 let (input, _) = char('"')(input)?;
610 Ok((input, content.to_string()))
611}
612
613fn single_quoted_string(input: &str) -> IResult<&str, String> {
614 let (input, _) = char('\'')(input)?;
615 let (input, content) = take_while1(|c| c != '\'')(input)?;
616 let (input, _) = char('\'')(input)?;
617 Ok((input, content.to_string()))
618}
619
620fn integer_literal(input: &str) -> IResult<&str, i64> {
622 let (input, digits) = recognize(pair(
623 opt(char('-')),
624 take_while1(|c: char| c.is_ascii_digit()),
625 ))(input)?;
626
627 Ok((input, digits.parse().unwrap()))
628}
629
630fn float_literal(input: &str) -> IResult<&str, f64> {
632 let (input, number) = recognize(tuple((
633 opt(char('-')),
634 take_while1(|c: char| c.is_ascii_digit()),
635 char('.'),
636 take_while1(|c: char| c.is_ascii_digit()),
637 )))(input)?;
638
639 Ok((input, number.parse().unwrap()))
640}
641
642fn boolean_literal(input: &str) -> IResult<&str, bool> {
644 alt((
645 map(tag_no_case("true"), |_| true),
646 map(tag_no_case("false"), |_| false),
647 ))(input)
648}
649
650fn parameter(input: &str) -> IResult<&str, String> {
652 let (input, _) = char('$')(input)?;
653 let (input, name) = identifier(input)?;
654 Ok((input, name.to_string()))
655}
656
657fn comma_ws(input: &str) -> IResult<&str, ()> {
659 let (input, _) = multispace0(input)?;
660 let (input, _) = char(',')(input)?;
661 let (input, _) = multispace0(input)?;
662 Ok((input, ()))
663}
664
665fn length_range(input: &str) -> IResult<&str, LengthRange> {
667 let (input, _) = char('*')(input)?;
668 let (input, _) = multispace0(input)?;
669
670 alt((
672 map(
674 tuple((
675 nom::character::complete::u32,
676 tag(".."),
677 nom::character::complete::u32,
678 )),
679 |(min, _, max)| LengthRange {
680 min: Some(min),
681 max: Some(max),
682 },
683 ),
684 map(preceded(tag(".."), nom::character::complete::u32), |max| {
686 LengthRange {
687 min: None,
688 max: Some(max),
689 }
690 }),
691 map(
693 tuple((nom::character::complete::u32, tag(".."))),
694 |(min, _)| LengthRange {
695 min: Some(min),
696 max: None,
697 },
698 ),
699 map(nom::character::complete::u32, |min| LengthRange {
701 min: Some(min),
702 max: Some(min),
703 }),
704 map(multispace0, |_| LengthRange {
706 min: None,
707 max: None,
708 }),
709 ))(input)
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use crate::ast::{BooleanExpression, ComparisonOperator, PropertyValue, ValueExpression};
716
717 #[test]
718 fn test_parse_simple_node_query() {
719 let query = "MATCH (n:Person) RETURN n.name";
720 let result = parse_cypher_query(query).unwrap();
721
722 assert_eq!(result.match_clauses.len(), 1);
723 assert_eq!(result.return_clause.items.len(), 1);
724 }
725
726 #[test]
727 fn test_parse_node_with_properties() {
728 let query = r#"MATCH (n:Person {name: "John", age: 30}) RETURN n"#;
729 let result = parse_cypher_query(query).unwrap();
730
731 if let GraphPattern::Node(node) = &result.match_clauses[0].patterns[0] {
732 assert_eq!(node.labels, vec!["Person"]);
733 assert_eq!(node.properties.len(), 2);
734 } else {
735 panic!("Expected node pattern");
736 }
737 }
738
739 #[test]
740 fn test_parse_simple_relationship_query() {
741 let query = "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a.name, b.name";
742 let result = parse_cypher_query(query).unwrap();
743
744 assert_eq!(result.match_clauses.len(), 1);
745 assert_eq!(result.return_clause.items.len(), 2);
746
747 if let GraphPattern::Path(path) = &result.match_clauses[0].patterns[0] {
748 assert_eq!(path.segments.len(), 1);
749 assert_eq!(path.segments[0].relationship.types, vec!["KNOWS"]);
750 } else {
751 panic!("Expected path pattern");
752 }
753 }
754
755 #[test]
756 fn test_parse_variable_length_path() {
757 let query = "MATCH (a:Person)-[:FRIEND_OF*1..2]-(b:Person) RETURN a.name, b.name";
758 let result = parse_cypher_query(query).unwrap();
759
760 assert_eq!(result.match_clauses.len(), 1);
761
762 if let GraphPattern::Path(path) = &result.match_clauses[0].patterns[0] {
763 assert_eq!(path.segments.len(), 1);
764 assert_eq!(path.segments[0].relationship.types, vec!["FRIEND_OF"]);
765
766 let length = path.segments[0].relationship.length.as_ref().unwrap();
767 assert_eq!(length.min, Some(1));
768 assert_eq!(length.max, Some(2));
769 } else {
770 panic!("Expected path pattern");
771 }
772 }
773
774 #[test]
775 fn test_parse_query_with_where_clause() {
776 let query = "MATCH (n:Person) WHERE n.age > 30 RETURN n.name";
777 let result = parse_cypher_query(query).unwrap();
778
779 assert!(result.where_clause.is_some());
780 }
781
782 #[test]
783 fn test_parse_query_with_single_quoted_literal() {
784 let query = "MATCH (n:Person) WHERE n.name = 'Alice' RETURN n.name";
785 let result = parse_cypher_query(query).unwrap();
786
787 assert!(result.where_clause.is_some());
788 }
789
790 #[test]
791 fn test_parse_query_with_and_conditions() {
792 let query = "MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) WHERE rel.relationship_type = 'WORKS_ON' AND dst.name_lower = 'presto' RETURN src.name, src.entity_id";
793 let result = parse_cypher_query(query).unwrap();
794
795 let where_clause = result.where_clause.expect("Expected WHERE clause");
796 match where_clause.expression {
797 BooleanExpression::And(left, right) => {
798 match *left {
799 BooleanExpression::Comparison {
800 left: ValueExpression::Property(ref prop),
801 operator,
802 right: ValueExpression::Literal(PropertyValue::String(ref value)),
803 } => {
804 assert_eq!(prop.variable, "rel");
805 assert_eq!(prop.property, "relationship_type");
806 assert_eq!(operator, ComparisonOperator::Equal);
807 assert_eq!(value, "WORKS_ON");
808 }
809 _ => panic!("Expected comparison for relationship_type filter"),
810 }
811
812 match *right {
813 BooleanExpression::Comparison {
814 left: ValueExpression::Property(ref prop),
815 operator,
816 right: ValueExpression::Literal(PropertyValue::String(ref value)),
817 } => {
818 assert_eq!(prop.variable, "dst");
819 assert_eq!(prop.property, "name_lower");
820 assert_eq!(operator, ComparisonOperator::Equal);
821 assert_eq!(value, "presto");
822 }
823 _ => panic!("Expected comparison for destination name filter"),
824 }
825 }
826 other => panic!("Expected AND expression, got {:?}", other),
827 }
828 }
829
830 #[test]
831 fn test_parse_query_with_in_clause() {
832 let query = "MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) WHERE rel.relationship_type IN ['WORKS_FOR', 'PART_OF'] RETURN src.name";
833 let result = parse_cypher_query(query).unwrap();
834
835 let where_clause = result.where_clause.expect("Expected WHERE clause");
836 match where_clause.expression {
837 BooleanExpression::In { expression, list } => {
838 match expression {
839 ValueExpression::Property(prop_ref) => {
840 assert_eq!(prop_ref.variable, "rel");
841 assert_eq!(prop_ref.property, "relationship_type");
842 }
843 _ => panic!("Expected property reference in IN expression"),
844 }
845 assert_eq!(list.len(), 2);
846 match &list[0] {
847 ValueExpression::Literal(PropertyValue::String(val)) => {
848 assert_eq!(val, "WORKS_FOR");
849 }
850 _ => panic!("Expected first list item to be a string literal"),
851 }
852 match &list[1] {
853 ValueExpression::Literal(PropertyValue::String(val)) => {
854 assert_eq!(val, "PART_OF");
855 }
856 _ => panic!("Expected second list item to be a string literal"),
857 }
858 }
859 other => panic!("Expected IN expression, got {:?}", other),
860 }
861 }
862
863 #[test]
864 fn test_parse_query_with_is_null() {
865 let query = "MATCH (n:Person) WHERE n.age IS NULL RETURN n.name";
866 let result = parse_cypher_query(query).unwrap();
867
868 let where_clause = result.where_clause.expect("Expected WHERE clause");
869
870 match where_clause.expression {
871 BooleanExpression::IsNull(expr) => match expr {
872 ValueExpression::Property(prop_ref) => {
873 assert_eq!(prop_ref.variable, "n");
874 assert_eq!(prop_ref.property, "age");
875 }
876 _ => panic!("Expected property reference in IS NULL expression"),
877 },
878 other => panic!("Expected IS NULL expression, got {:?}", other),
879 }
880 }
881
882 #[test]
883 fn test_parse_query_with_is_not_null() {
884 let query = "MATCH (n:Person) WHERE n.age IS NOT NULL RETURN n.name";
885 let result = parse_cypher_query(query).unwrap();
886
887 let where_clause = result.where_clause.expect("Expected WHERE clause");
888
889 match where_clause.expression {
890 BooleanExpression::IsNotNull(expr) => match expr {
891 ValueExpression::Property(prop_ref) => {
892 assert_eq!(prop_ref.variable, "n");
893 assert_eq!(prop_ref.property, "age");
894 }
895 _ => panic!("Expected property reference in IS NOT NULL expression"),
896 },
897 other => panic!("Expected IS NOT NULL expression, got {:?}", other),
898 }
899 }
900
901 #[test]
902 fn test_parse_query_with_limit() {
903 let query = "MATCH (n:Person) RETURN n.name LIMIT 10";
904 let result = parse_cypher_query(query).unwrap();
905
906 assert_eq!(result.limit, Some(10));
907 }
908
909 #[test]
910 fn test_parse_query_with_skip() {
911 let query = "MATCH (n:Person) RETURN n.name SKIP 5";
912 let result = parse_cypher_query(query).unwrap();
913
914 assert_eq!(result.skip, Some(5));
915 assert_eq!(result.limit, None);
916 }
917
918 #[test]
919 fn test_parse_query_with_skip_and_limit() {
920 let query = "MATCH (n:Person) RETURN n.name SKIP 5 LIMIT 10";
921 let result = parse_cypher_query(query).unwrap();
922
923 assert_eq!(result.skip, Some(5));
924 assert_eq!(result.limit, Some(10));
925 }
926
927 #[test]
928 fn test_parse_query_with_skip_and_order_by() {
929 let query = "MATCH (n:Person) RETURN n.name ORDER BY n.age SKIP 5";
930 let result = parse_cypher_query(query).unwrap();
931
932 assert_eq!(result.skip, Some(5));
933 assert!(result.order_by.is_some());
934 }
935
936 #[test]
937 fn test_parse_query_with_skip_order_by_and_limit() {
938 let query = "MATCH (n:Person) RETURN n.name ORDER BY n.age SKIP 5 LIMIT 10";
939 let result = parse_cypher_query(query).unwrap();
940
941 assert_eq!(result.skip, Some(5));
942 assert_eq!(result.limit, Some(10));
943 assert!(result.order_by.is_some());
944 }
945
946 #[test]
947 fn test_parse_count_star() {
948 let query = "MATCH (n:Person) RETURN count(*) AS total";
949 let result = parse_cypher_query(query).unwrap();
950
951 assert_eq!(result.return_clause.items.len(), 1);
952 let item = &result.return_clause.items[0];
953 assert_eq!(item.alias, Some("total".to_string()));
954
955 match &item.expression {
956 ValueExpression::Function { name, args } => {
957 assert_eq!(name, "count");
958 assert_eq!(args.len(), 1);
959 match &args[0] {
960 ValueExpression::Variable(v) => assert_eq!(v, "*"),
961 _ => panic!("Expected Variable(*) in count(*)"),
962 }
963 }
964 _ => panic!("Expected Function expression"),
965 }
966 }
967
968 #[test]
969 fn test_parse_count_property() {
970 let query = "MATCH (n:Person) RETURN count(n.age)";
971 let result = parse_cypher_query(query).unwrap();
972
973 assert_eq!(result.return_clause.items.len(), 1);
974 let item = &result.return_clause.items[0];
975
976 match &item.expression {
977 ValueExpression::Function { name, args } => {
978 assert_eq!(name, "count");
979 assert_eq!(args.len(), 1);
980 match &args[0] {
981 ValueExpression::Property(prop) => {
982 assert_eq!(prop.variable, "n");
983 assert_eq!(prop.property, "age");
984 }
985 _ => panic!("Expected Property in count(n.age)"),
986 }
987 }
988 _ => panic!("Expected Function expression"),
989 }
990 }
991
992 #[test]
993 fn test_parse_non_count_function_rejects_star() {
994 let query = "MATCH (n:Person) RETURN foo(*)";
996 let result = parse_cypher_query(query);
997 assert!(result.is_err(), "foo(*) should not parse successfully");
998 }
999
1000 #[test]
1001 fn test_parse_count_with_multiple_args() {
1002 let query = "MATCH (n:Person) RETURN count(n.age, n.name)";
1005 let result = parse_cypher_query(query);
1006 assert!(
1007 result.is_ok(),
1008 "Parser should accept multiple args (validation happens in semantic phase)"
1009 );
1010
1011 let ast = result.unwrap();
1013 match &ast.return_clause.items[0].expression {
1014 ValueExpression::Function { name, args } => {
1015 assert_eq!(name, "count");
1016 assert_eq!(args.len(), 2);
1017 }
1018 _ => panic!("Expected Function expression"),
1019 }
1020 }
1021}