lance_graph/
parser.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Cypher query parser
5//!
6//! This module provides parsing functionality for Cypher queries using nom parser combinators.
7//! It supports a subset of Cypher syntax focused on graph pattern matching and property access.
8
9use 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
22/// Parse a complete Cypher query
23pub 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
41// Top-level parser for a complete Cypher query
42fn 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
64// Parse a MATCH clause
65fn 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
74// Parse a graph pattern (node or path)
75fn 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
82// Parse a path pattern (only if there are segments)
83fn 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    // Only succeed if we actually have path segments
88    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
104// Parse a path segment (relationship + node)
105fn 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
118// Parse a node pattern: (variable:Label {prop: value})
119fn 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
140// Parse a relationship pattern: -[variable:TYPE {prop: value}]->
141fn relationship_pattern(input: &str) -> IResult<&str, RelationshipPattern> {
142    let (input, _) = multispace0(input)?;
143
144    // Parse direction and bracket content
145    let (input, (direction, content)) = alt((
146        // Outgoing: -[...]->
147        map(
148            tuple((
149                char('-'),
150                delimited(char('['), relationship_content, char(']')),
151                tag("->"),
152            )),
153            |(_, content, _)| (RelationshipDirection::Outgoing, content),
154        ),
155        // Incoming: <-[...]-
156        map(
157            tuple((
158                tag("<-"),
159                delimited(char('['), relationship_content, char(']')),
160                char('-'),
161            )),
162            |(_, content, _)| (RelationshipDirection::Incoming, content),
163        ),
164        // Undirected: -[...]-
165        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
189// Type alias for complex relationship content return type
190type RelationshipContentResult<'a> = (
191    Option<&'a str>,
192    Vec<&'a str>,
193    Option<HashMap<String, PropertyValue>>,
194    Option<LengthRange>,
195);
196
197// Parse relationship content inside brackets
198fn 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
211// Parse a property map: {key: value, key2: value2}
212fn 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
223// Parse a property key-value pair
224fn 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
235// Parse a property value
236fn 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
247// Parse a WHERE clause
248fn 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
257// Parse a boolean expression with OR precedence
258fn 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    // Match is null
333    if let Ok((rest, ())) = is_null_comparison(input) {
334        return Ok((rest, BooleanExpression::IsNull(left_clone)));
335    }
336    // Match is not null
337    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
355// Parse a comparison operator
356fn 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
368// Parse a value expression
369fn 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
378// Parse a function call: function_name(args)
379fn 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    // Handle COUNT(*) special case - only allow * for COUNT function
386    if let Ok((input_after_star, _)) = char::<_, nom::error::Error<&str>>('*')(input) {
387        // Validate that this is COUNT function
388        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            // Not COUNT - fail parsing to try regular argument parsing
400            // This will naturally fail since * is not a valid value_expression
401        }
402    }
403
404    // Parse regular function arguments
405    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
432// Parse a property reference: variable.property
433fn 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
447// Parse a RETURN clause
448fn 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
469// Parse a return item
470fn 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
487// Match IS NULL in WHERE clause
488fn 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
498// Match IS NOT NULL in WHERE clause
499fn 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
511// Parse an ORDER BY clause
512fn 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
523// Parse an order by item
524fn 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
541// Parse a LIMIT clause
542fn 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
551// Parse a SKIP clause
552fn 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
561// Parse pagination clauses (SKIP and LIMIT)
562fn 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
594// Helper parsers
595
596// Parse an identifier
597fn identifier(input: &str) -> IResult<&str, &str> {
598    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
599}
600
601// Parse a string literal
602fn 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
620// Parse an integer literal
621fn 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
630// Parse a float literal
631fn 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
642// Parse a boolean literal
643fn 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
650// Parse a parameter reference
651fn 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
657// Parse comma with optional whitespace
658fn 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
665// Parse variable-length path syntax: *1..2, *..3, *2.., *
666fn length_range(input: &str) -> IResult<&str, LengthRange> {
667    let (input, _) = char('*')(input)?;
668    let (input, _) = multispace0(input)?;
669
670    // Parse different length patterns
671    alt((
672        // *min..max (e.g., *1..3)
673        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        // *..max (e.g., *..3)
685        map(preceded(tag(".."), nom::character::complete::u32), |max| {
686            LengthRange {
687                min: None,
688                max: Some(max),
689            }
690        }),
691        // *min.. (e.g., *2..)
692        map(
693            tuple((nom::character::complete::u32, tag(".."))),
694            |(min, _)| LengthRange {
695                min: Some(min),
696                max: None,
697            },
698        ),
699        // *min (e.g., *2)
700        map(nom::character::complete::u32, |min| LengthRange {
701            min: Some(min),
702            max: Some(min),
703        }),
704        // * (unlimited)
705        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        // FOO(*) should fail to parse since * is only allowed for COUNT
995        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        // COUNT with multiple arguments parses successfully
1003        // but will be rejected during semantic validation
1004        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        // Verify the AST structure
1012        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}