Skip to main content

lance_csr/
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_while, take_while1},
14    character::complete::{char, digit0, digit1, multispace0, multispace1, one_of, satisfy},
15    combinator::{map, map_res, not, opt, peek, recognize},
16    multi::{many0, many1, separated_list0, separated_list1},
17    sequence::{delimited, pair, preceded, terminated, tuple},
18    IResult,
19};
20use std::collections::HashMap;
21
22/// Parse one complete Cypher query with an optional trailing semicolon.
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    let remaining = remaining.strip_prefix(';').unwrap_or(remaining);
31    if !remaining.trim().is_empty() {
32        return Err(GraphError::ParseError {
33            message: format!("Unexpected input after query: {}", remaining),
34            position: input.len() - remaining.len(),
35            location: snafu::Location::new(file!(), line!(), column!()),
36        });
37    }
38
39    let mut query = query;
40    normalize_inline_properties(&mut query);
41    Ok(query)
42}
43
44fn normalize_inline_properties(query: &mut CypherQuery) {
45    let stages = query
46        .further_with_stages
47        .iter_mut()
48        .flat_map(|s| &mut s.reading_clauses);
49    let mut anonymous = 0;
50    for clause in query
51        .reading_clauses
52        .iter_mut()
53        .chain(&mut query.post_with_reading_clauses)
54        .chain(stages)
55    {
56        match clause {
57            ReadingClause::Match(m) => move_variable_properties_to_where(m, &mut anonymous),
58            ReadingClause::Call(inner) => normalize_inline_properties(inner),
59            ReadingClause::Unwind(_) => {}
60        }
61    }
62    for branch in &mut query.union_branches {
63        normalize_inline_properties(&mut branch.query);
64    }
65}
66
67/// Inline node properties whose value is a variable or property from an earlier clause
68/// (`(t:Tag {id: tagId})`) become `t.id = tagId` in the clause's own WHERE, since scans
69/// filter only on constants. An unnamed node gets a name for the condition.
70fn move_variable_properties_to_where(clause: &mut MatchClause, anonymous: &mut usize) {
71    let mut conditions = Vec::new();
72    for pattern in &mut clause.patterns {
73        let nodes: Vec<&mut NodePattern> = match pattern {
74            GraphPattern::Node(n) => vec![n],
75            GraphPattern::Path(p) => std::iter::once(&mut p.start_node)
76                .chain(p.segments.iter_mut().map(|s| &mut s.end_node))
77                .collect(),
78        };
79        for node in nodes {
80            let keys: Vec<String> = node
81                .properties
82                .iter()
83                .filter(|(_, v)| {
84                    matches!(v, PropertyValue::Variable(_) | PropertyValue::Property(_))
85                })
86                .map(|(k, _)| k.clone())
87                .collect();
88            for key in keys {
89                let value = match node.properties.remove(&key) {
90                    Some(PropertyValue::Variable(v)) => ValueExpression::Variable(v),
91                    Some(PropertyValue::Property(p)) => ValueExpression::Property(p),
92                    _ => unreachable!("filtered above"),
93                };
94                let variable = node.variable.get_or_insert_with(|| {
95                    *anonymous += 1;
96                    format!("__inline_{anonymous}")
97                });
98                conditions.push(BooleanExpression::Comparison {
99                    left: ValueExpression::Property(PropertyRef {
100                        variable: variable.clone(),
101                        property: key,
102                    }),
103                    operator: ComparisonOperator::Equal,
104                    right: value,
105                });
106            }
107        }
108    }
109    let existing = clause.where_clause.take().map(|w| w.expression);
110    clause.where_clause = conditions
111        .into_iter()
112        .chain(existing)
113        .reduce(|a, b| BooleanExpression::And(Box::new(a), Box::new(b)))
114        .map(|expression| WhereClause { expression });
115}
116
117// Top-level parser for a complete Cypher query
118fn cypher_query(input: &str) -> IResult<&str, CypherQuery> {
119    let (input, mut query) = single_query(input)?;
120    let (input, branches) = many0(pair(union_header, single_query))(input)?;
121    query.union_branches = branches
122        .into_iter()
123        .map(|(all, query)| UnionBranch { all, query })
124        .collect();
125    Ok((input, query))
126}
127
128fn union_header(input: &str) -> IResult<&str, bool> {
129    let (input, _) = multispace0(input)?;
130    let (input, _) = tag_no_case("UNION")(input)?;
131    let (input, _) = multispace1(input)?;
132    let (input, all) = opt(terminated(tag_no_case("ALL"), multispace1))(input)?;
133    Ok((input, all.is_some()))
134}
135
136fn single_query(input: &str) -> IResult<&str, CypherQuery> {
137    let (input, _) = multispace0(input)?;
138    let (input, reading_clauses) = many0(reading_clause)(input)?;
139    let (input, pre_with_where) = opt(where_clause)(input)?;
140
141    // Optional WITH clause with optional post-WITH MATCH and WHERE
142    let (input, with_result) = opt(with_clause)(input)?;
143    // Only try to parse post-WITH clauses if we have a WITH clause
144    let (input, post_with_reading_clauses, post_with_where) = match with_result {
145        Some(_) => {
146            let (input, readings) = many0(reading_clause)(input)?;
147            let (input, where_cl) = opt(where_clause)(input)?;
148            (input, readings, where_cl)
149        }
150        None => (input, vec![], None),
151    };
152    let (input, further_with_stages) = match with_result {
153        Some(_) => many0(with_stage)(input)?,
154        None => (input, vec![]),
155    };
156
157    let (input, return_clause) = return_clause(input)?;
158    let (input, order_by) = opt(order_by_clause)(input)?;
159    let (input, (skip, limit)) = pagination_clauses(input)?;
160    let (input, _) = multispace0(input)?;
161
162    Ok((
163        input,
164        CypherQuery {
165            reading_clauses,
166            where_clause: pre_with_where,
167            with_clause: with_result,
168            post_with_reading_clauses,
169            post_with_where_clause: post_with_where,
170            further_with_stages,
171            union_branches: Vec::new(),
172            return_clause,
173            limit,
174            order_by,
175            skip,
176        },
177    ))
178}
179
180// Parse a reading clause (MATCH or UNWIND)
181fn reading_clause(input: &str) -> IResult<&str, ReadingClause> {
182    alt((
183        map(match_clause, ReadingClause::Match),
184        map(unwind_clause, ReadingClause::Unwind),
185        map(call_clause, |query| ReadingClause::Call(Box::new(query))),
186    ))(input)
187}
188
189fn call_clause(input: &str) -> IResult<&str, CypherQuery> {
190    let (input, _) = multispace0(input)?;
191    let (input, _) = tag_no_case("CALL")(input)?;
192    let (input, _) = multispace0(input)?;
193    delimited(char('{'), cypher_query, preceded(multispace0, char('}')))(input)
194}
195
196// Parse a MATCH clause
197fn match_clause(input: &str) -> IResult<&str, MatchClause> {
198    let (input, _) = multispace0(input)?;
199    let (input, optional) = opt(terminated(tag_no_case("OPTIONAL"), multispace1))(input)?;
200    let (input, _) = tag_no_case("MATCH")(input)?;
201    let (input, _) = multispace1(input)?;
202    let (input, patterns) = separated_list0(comma_ws, graph_pattern)(input)?;
203    // See `MatchClause::where_clause` for which WHERE belongs to the clause.
204    let (input, where_clause) = if optional.is_some() {
205        opt(where_clause)(input)?
206    } else {
207        opt(terminated(where_clause, peek(reading_clause)))(input)?
208    };
209
210    Ok((
211        input,
212        MatchClause {
213            patterns,
214            optional: optional.is_some(),
215            where_clause,
216        },
217    ))
218}
219
220// Parse an UNWIND clause
221fn unwind_clause(input: &str) -> IResult<&str, UnwindClause> {
222    let (input, _) = multispace0(input)?;
223    let (input, _) = tag_no_case("UNWIND")(input)?;
224    let (input, _) = multispace1(input)?;
225    let (input, expression) = value_expression(input)?;
226    let (input, _) = multispace1(input)?;
227    let (input, _) = tag_no_case("AS")(input)?;
228    let (input, _) = multispace1(input)?;
229    let (input, alias) = identifier(input)?;
230
231    Ok((
232        input,
233        UnwindClause {
234            expression,
235            alias: alias.to_string(),
236        },
237    ))
238}
239
240// Parse a graph pattern (node or path)
241fn graph_pattern(input: &str) -> IResult<&str, GraphPattern> {
242    alt((
243        map(shortest_path_pattern, GraphPattern::Path),
244        map(named_path_pattern, GraphPattern::Path),
245        map(path_pattern, GraphPattern::Path),
246        map(node_pattern, GraphPattern::Node),
247    ))(input)
248}
249
250// [p =] shortestPath((a)-[:R*min..max]-(b)) or allShortestPaths(...)
251fn shortest_path_pattern(input: &str) -> IResult<&str, PathPattern> {
252    let (input, _) = multispace0(input)?;
253    let (input, variable) = opt(terminated(
254        identifier,
255        tuple((multispace0, char('='), multispace0)),
256    ))(input)?;
257    let (input, all) = alt((
258        map(tag_no_case("allShortestPaths"), |_| true),
259        map(tag_no_case("shortestPath"), |_| false),
260    ))(input)?;
261    let (input, mut path) = delimited(
262        tuple((multispace0, char('('), multispace0)),
263        path_pattern,
264        tuple((multispace0, char(')'))),
265    )(input)?;
266    path.shortest = Some(ShortestPathSpec {
267        all,
268        variable: variable.map(str::to_string),
269    });
270    Ok((input, path))
271}
272
273// Named ordinary paths retain their textual node and relationship order.
274fn named_path_pattern(input: &str) -> IResult<&str, PathPattern> {
275    let (input, variable) = delimited(
276        multispace0,
277        identifier,
278        tuple((multispace0, char('='), multispace0)),
279    )(input)?;
280    let (input, mut path) = path_pattern(input)?;
281    path.variable = Some(variable.to_string());
282    Ok((input, path))
283}
284
285// Parse a path pattern (only if there are segments)
286fn path_pattern(input: &str) -> IResult<&str, PathPattern> {
287    let (input, start_node) = node_pattern(input)?;
288    let (input, segments) = many0(path_segment)(input)?;
289
290    // Only succeed if we actually have path segments
291    if segments.is_empty() {
292        return Err(nom::Err::Error(nom::error::Error::new(
293            input,
294            nom::error::ErrorKind::Tag,
295        )));
296    }
297
298    Ok((
299        input,
300        PathPattern {
301            start_node,
302            segments,
303            shortest: None,
304            variable: None,
305        },
306    ))
307}
308
309// Parse a path segment (relationship + node)
310fn path_segment(input: &str) -> IResult<&str, PathSegment> {
311    let (input, relationship) = relationship_pattern(input)?;
312    let (input, end_node) = node_pattern(input)?;
313
314    Ok((
315        input,
316        PathSegment {
317            relationship,
318            end_node,
319        },
320    ))
321}
322
323// Parse a node pattern: (variable:Label {prop: value})
324fn node_pattern(input: &str) -> IResult<&str, NodePattern> {
325    let (input, _) = multispace0(input)?;
326    let (input, _) = char('(')(input)?;
327    let (input, _) = multispace0(input)?;
328    let (input, variable) = opt(identifier)(input)?;
329    let (input, labels) = many0(preceded(char(':'), identifier))(input)?;
330    let (input, _) = multispace0(input)?;
331    let (input, properties) = opt(property_map)(input)?;
332    let (input, _) = multispace0(input)?;
333    let (input, _) = char(')')(input)?;
334
335    Ok((
336        input,
337        NodePattern {
338            variable: variable.map(|s| s.to_string()),
339            labels: labels.into_iter().map(|s| s.to_string()).collect(),
340            properties: properties.unwrap_or_default(),
341        },
342    ))
343}
344
345// Parse a relationship pattern: -[variable:TYPE {prop: value}]->
346fn relationship_pattern(input: &str) -> IResult<&str, RelationshipPattern> {
347    let (input, _) = multispace0(input)?;
348
349    // Parse direction and bracket content
350    let (input, (direction, content)) = alt((
351        // Outgoing: -[...]->
352        map(
353            tuple((
354                char('-'),
355                delimited(char('['), relationship_content, char(']')),
356                tag("->"),
357            )),
358            |(_, content, _)| (RelationshipDirection::Outgoing, content),
359        ),
360        // Incoming: <-[...]-
361        map(
362            tuple((
363                tag("<-"),
364                delimited(char('['), relationship_content, char(']')),
365                char('-'),
366            )),
367            |(_, content, _)| (RelationshipDirection::Incoming, content),
368        ),
369        // Undirected: -[...]-
370        map(
371            tuple((
372                char('-'),
373                delimited(char('['), relationship_content, char(']')),
374                char('-'),
375            )),
376            |(_, content, _)| (RelationshipDirection::Undirected, content),
377        ),
378    ))(input)?;
379
380    let (variable, types, properties, length) = content;
381
382    Ok((
383        input,
384        RelationshipPattern {
385            variable: variable.map(|s| s.to_string()),
386            types: types.into_iter().map(|s| s.to_string()).collect(),
387            direction,
388            properties: properties.unwrap_or_default(),
389            length,
390        },
391    ))
392}
393
394// Type alias for complex relationship content return type
395type RelationshipContentResult<'a> = (
396    Option<&'a str>,
397    Vec<&'a str>,
398    Option<HashMap<String, PropertyValue>>,
399    Option<LengthRange>,
400);
401
402// Parse relationship content inside brackets
403fn relationship_content(input: &str) -> IResult<&str, RelationshipContentResult<'_>> {
404    let (input, _) = multispace0(input)?;
405    let (input, variable) = opt(identifier)(input)?;
406    // `:A`, alternation `:A|B` or `:A|:B`, and upstream's `:A:B` (also read as A or B).
407    let (input, first) = opt(preceded(char(':'), identifier))(input)?;
408    let (input, rest) = many0(preceded(
409        alt((
410            map(
411                tuple((multispace0, char('|'), multispace0, opt(char(':')))),
412                |_| (),
413            ),
414            map(char(':'), |_| ()),
415        )),
416        identifier,
417    ))(input)?;
418    let types: Vec<&str> = first.into_iter().chain(rest).collect();
419    let (input, _) = multispace0(input)?;
420    let (input, length) = opt(length_range)(input)?;
421    let (input, _) = multispace0(input)?;
422    let (input, properties) = opt(property_map)(input)?;
423    let (input, _) = multispace0(input)?;
424
425    Ok((input, (variable, types, properties, length)))
426}
427
428// Parse a property map: {key: value, key2: value2}
429fn property_map(input: &str) -> IResult<&str, HashMap<String, PropertyValue>> {
430    let (input, _) = multispace0(input)?;
431    let (input, _) = char('{')(input)?;
432    let (input, _) = multispace0(input)?;
433    let (input, pairs) = separated_list0(comma_ws, property_pair)(input)?;
434    let (input, _) = multispace0(input)?;
435    let (input, _) = char('}')(input)?;
436
437    Ok((input, pairs.into_iter().collect()))
438}
439
440// Parse a property key-value pair
441fn property_pair(input: &str) -> IResult<&str, (String, PropertyValue)> {
442    let (input, _) = multispace0(input)?;
443    let (input, key) = identifier(input)?;
444    let (input, _) = multispace0(input)?;
445    let (input, _) = char(':')(input)?;
446    let (input, _) = multispace0(input)?;
447    let (input, value) = alt((
448        property_value,
449        map(property_reference, PropertyValue::Property),
450        map(identifier, |v| PropertyValue::Variable(v.to_string())),
451    ))(input)?;
452
453    Ok((input, (key.to_string(), value)))
454}
455
456// Parse a property value
457fn property_value(input: &str) -> IResult<&str, PropertyValue> {
458    alt((
459        map(string_literal, PropertyValue::String),
460        map(float_literal, PropertyValue::Float), // Try float BEFORE integer (more specific)
461        map(integer_literal, PropertyValue::Integer),
462        map(boolean_literal, PropertyValue::Boolean),
463        // NULL in any case, as a whole word (not the start of a name like `nullCount`).
464        map(
465            terminated(
466                tag_no_case("null"),
467                not(peek(satisfy(|c: char| c.is_alphanumeric() || c == '_'))),
468            ),
469            |_| PropertyValue::Null,
470        ),
471        map(parameter, PropertyValue::Parameter),
472    ))(input)
473}
474
475// Parse a WHERE clause
476fn where_clause(input: &str) -> IResult<&str, WhereClause> {
477    let (input, _) = multispace0(input)?;
478    let (input, _) = tag_no_case("WHERE")(input)?;
479    let (input, _) = multispace1(input)?;
480    let (input, expression) = boolean_expression(input)?;
481
482    Ok((input, WhereClause { expression }))
483}
484
485// Parse a boolean expression with OR precedence
486fn boolean_expression(input: &str) -> IResult<&str, BooleanExpression> {
487    boolean_or_expression(input)
488}
489
490fn boolean_or_expression(input: &str) -> IResult<&str, BooleanExpression> {
491    let (input, first) = boolean_and_expression(input)?;
492    let (input, rest) = many0(preceded(
493        tuple((multispace0, tag_no_case("OR"), multispace1)),
494        boolean_and_expression,
495    ))(input)?;
496    let expr = rest.into_iter().fold(first, |acc, item| {
497        BooleanExpression::Or(Box::new(acc), Box::new(item))
498    });
499    Ok((input, expr))
500}
501
502fn boolean_and_expression(input: &str) -> IResult<&str, BooleanExpression> {
503    let (input, first) = boolean_not_expression(input)?;
504    let (input, rest) = many0(preceded(
505        tuple((multispace0, tag_no_case("AND"), multispace1)),
506        boolean_not_expression,
507    ))(input)?;
508    let expr = rest.into_iter().fold(first, |acc, item| {
509        BooleanExpression::And(Box::new(acc), Box::new(item))
510    });
511    Ok((input, expr))
512}
513
514fn boolean_not_expression(input: &str) -> IResult<&str, BooleanExpression> {
515    let (input, _) = multispace0(input)?;
516    alt((
517        map(
518            preceded(
519                tuple((tag_no_case("NOT"), multispace1)),
520                boolean_not_expression,
521            ),
522            |expr| BooleanExpression::Not(Box::new(expr)),
523        ),
524        boolean_primary_expression,
525    ))(input)
526}
527
528fn boolean_primary_expression(input: &str) -> IResult<&str, BooleanExpression> {
529    let (input, _) = multispace0(input)?;
530    alt((
531        map(
532            delimited(
533                tuple((char('('), multispace0)),
534                boolean_expression,
535                tuple((multispace0, char(')'))),
536            ),
537            |expr| expr,
538        ),
539        exists_subquery,
540        // A path used as a predicate: `(a)-[:R]-(b)` (one relationship at least).
541        map(path_pattern, |path| BooleanExpression::PatternExists {
542            patterns: vec![GraphPattern::Path(path)],
543            where_clause: None,
544        }),
545        comparison_expression,
546        map(list_predicate, |left| BooleanExpression::Comparison {
547            left,
548            operator: ComparisonOperator::Equal,
549            right: ValueExpression::Literal(PropertyValue::Boolean(true)),
550        }),
551    ))(input)
552}
553
554// EXISTS { [MATCH] pattern, ... [WHERE ...] }
555fn exists_subquery(input: &str) -> IResult<&str, BooleanExpression> {
556    let (input, _) = tuple((tag_no_case("EXISTS"), multispace0, char('{'), multispace0))(input)?;
557    let (input, _) = opt(tuple((tag_no_case("MATCH"), multispace1)))(input)?;
558    let (input, patterns) = separated_list1(comma_ws, graph_pattern)(input)?;
559    let (input, where_clause) = opt(where_clause)(input)?;
560    let (input, _) = tuple((multispace0, char('}')))(input)?;
561    Ok((
562        input,
563        BooleanExpression::PatternExists {
564            patterns,
565            where_clause: where_clause.map(|w| Box::new(w.expression)),
566        },
567    ))
568}
569
570fn comparison_expression(input: &str) -> IResult<&str, BooleanExpression> {
571    let (input, _) = multispace0(input)?;
572    let (input, left) = value_expression(input)?;
573    let (input, _) = multispace0(input)?;
574    let left_clone = left.clone();
575
576    if let Ok((input_after_in, (_, _, list))) =
577        tuple((tag_no_case("IN"), multispace0, value_expression_list))(input)
578    {
579        return Ok((
580            input_after_in,
581            BooleanExpression::In {
582                expression: left,
583                list,
584            },
585        ));
586    }
587    if let Ok((input_after_in, (_, _, list))) =
588        tuple((tag_no_case("IN"), multispace1, value_expression))(input)
589    {
590        return Ok((
591            input_after_in,
592            BooleanExpression::Member {
593                expression: left,
594                list,
595            },
596        ));
597    }
598    // Match LIKE pattern
599    if let Ok((input_after_like, (_, _, pattern))) =
600        tuple((tag_no_case("LIKE"), multispace0, string_literal))(input)
601    {
602        return Ok((
603            input_after_like,
604            BooleanExpression::Like {
605                expression: left,
606                pattern,
607            },
608        ));
609    }
610    // Match ILIKE pattern (case-insensitive LIKE)
611    if let Ok((input_after_ilike, (_, _, pattern))) =
612        tuple((tag_no_case("ILIKE"), multispace0, string_literal))(input)
613    {
614        return Ok((
615            input_after_ilike,
616            BooleanExpression::ILike {
617                expression: left,
618                pattern,
619            },
620        ));
621    }
622    // Match CONTAINS substring
623    if let Ok((input_after_contains, (_, _, substring))) =
624        tuple((tag_no_case("CONTAINS"), multispace0, string_literal))(input)
625    {
626        return Ok((
627            input_after_contains,
628            BooleanExpression::Contains {
629                expression: left,
630                substring,
631            },
632        ));
633    }
634    // Match STARTS WITH prefix (note: multi-word operator)
635    if let Ok((input_after_starts, (_, _, _, _, prefix))) = tuple((
636        tag_no_case("STARTS"),
637        multispace1,
638        tag_no_case("WITH"),
639        multispace0,
640        string_literal,
641    ))(input)
642    {
643        return Ok((
644            input_after_starts,
645            BooleanExpression::StartsWith {
646                expression: left,
647                prefix,
648            },
649        ));
650    }
651    // Match ENDS WITH suffix (note: multi-word operator)
652    if let Ok((input_after_ends, (_, _, _, _, suffix))) = tuple((
653        tag_no_case("ENDS"),
654        multispace1,
655        tag_no_case("WITH"),
656        multispace0,
657        string_literal,
658    ))(input)
659    {
660        return Ok((
661            input_after_ends,
662            BooleanExpression::EndsWith {
663                expression: left,
664                suffix,
665            },
666        ));
667    }
668    // Match is null
669    if let Ok((rest, ())) = is_null_comparison(input) {
670        return Ok((rest, BooleanExpression::IsNull(left_clone)));
671    }
672    // Match is not null
673    if let Ok((rest, ())) = is_not_null_comparison(input) {
674        return Ok((rest, BooleanExpression::IsNotNull(left_clone)));
675    }
676
677    let (input, operator) = comparison_operator(input)?;
678    let (input, _) = multispace0(input)?;
679    let (mut input, mut right) = value_expression(input)?;
680    let mut expr = BooleanExpression::Comparison {
681        left: left_clone,
682        operator,
683        right: right.clone(),
684    };
685    // A chain `a < b <= c` means `a < b AND b <= c`.
686    while let Ok((rest, (_, operator, _, next))) = tuple((
687        multispace0,
688        comparison_operator,
689        multispace0,
690        value_expression,
691    ))(input)
692    {
693        expr = BooleanExpression::And(
694            Box::new(expr),
695            Box::new(BooleanExpression::Comparison {
696                left: right,
697                operator,
698                right: next.clone(),
699            }),
700        );
701        right = next;
702        input = rest;
703    }
704    Ok((input, expr))
705}
706
707// Parse a comparison operator
708fn comparison_operator(input: &str) -> IResult<&str, ComparisonOperator> {
709    alt((
710        map(tag("="), |_| ComparisonOperator::Equal),
711        map(tag("<>"), |_| ComparisonOperator::NotEqual),
712        map(tag("!="), |_| ComparisonOperator::NotEqual),
713        map(tag("<="), |_| ComparisonOperator::LessThanOrEqual),
714        map(tag(">="), |_| ComparisonOperator::GreaterThanOrEqual),
715        map(tag("<"), |_| ComparisonOperator::LessThan),
716        map(tag(">"), |_| ComparisonOperator::GreaterThan),
717    ))(input)
718}
719
720// Parse a basic value expression (without vector functions to avoid circular dependency)
721fn basic_value_expression(input: &str) -> IResult<&str, ValueExpression> {
722    alt((
723        parse_vector_literal, // Try vector literal first [0.1, 0.2]
724        pattern_comprehension,
725        list_comprehension,
726        list_literal, // Then a list of any expressions [a.x, 'y'] (or [])
727        map_literal,
728        reduce_expression,
729        list_predicate,
730        parse_parameter,                               // Try $parameter
731        function_call,                                 // Regular function calls
732        map(property_value, ValueExpression::Literal), // Try literals BEFORE property references
733        map(property_reference, ValueExpression::Property),
734        map(identifier, |id| ValueExpression::Variable(id.to_string())),
735    ))(input)
736}
737
738// A value where a predicate may also appear (projections, CASE, lists, arguments, maps):
739// `path IS NULL`, `NOT (a)-[:R]-(b)`, `x IN list` are boolean values.
740fn expression(input: &str) -> IResult<&str, ValueExpression> {
741    alt((
742        map(boolean_expression, |b| {
743            ValueExpression::Boolean(Box::new(b))
744        }),
745        value_expression,
746    ))(input)
747}
748
749// `[(a)-[:R]->(b) WHERE predicate | value]`
750fn pattern_comprehension(input: &str) -> IResult<&str, ValueExpression> {
751    let (input, _) = tuple((char('['), multispace0))(input)?;
752    let (input, pattern) = path_pattern(input)?;
753    let (input, filter) = opt(where_clause)(input)?;
754    let (input, map) = preceded(tuple((multispace0, char('|'), multispace0)), expression)(input)?;
755    let (input, _) = tuple((multispace0, char(']')))(input)?;
756    Ok((
757        input,
758        ValueExpression::PatternComprehension {
759            pattern: Box::new(pattern),
760            filter: filter.map(|w| Box::new(w.expression)),
761            map: Box::new(map),
762        },
763    ))
764}
765
766// `[x IN list WHERE predicate | value]`
767fn list_comprehension(input: &str) -> IResult<&str, ValueExpression> {
768    let (input, (_, _, variable, _, _, _, list)) = tuple((
769        char('['),
770        multispace0,
771        identifier,
772        multispace1,
773        tag_no_case("IN"),
774        multispace1,
775        value_expression,
776    ))(input)?;
777    let (input, filter) = opt(where_clause)(input)?;
778    let (input, map) = opt(preceded(
779        tuple((multispace0, char('|'), multispace0)),
780        expression,
781    ))(input)?;
782    let (input, _) = tuple((multispace0, char(']')))(input)?;
783    Ok((
784        input,
785        ValueExpression::ListComprehension {
786            variable: variable.to_string(),
787            list: Box::new(list),
788            filter: filter.map(|w| Box::new(w.expression)),
789            map: map.map(Box::new),
790        },
791    ))
792}
793
794fn reduce_expression(input: &str) -> IResult<&str, ValueExpression> {
795    let (input, _) = tuple((tag_no_case("reduce"), multispace0, char('('), multispace0))(input)?;
796    let (input, accumulator) = identifier(input)?;
797    let (input, initial) =
798        preceded(tuple((multispace0, char('='), multispace0)), expression)(input)?;
799    let (input, variable) = preceded(comma_ws, identifier)(input)?;
800    let (input, list) = preceded(
801        tuple((multispace1, tag_no_case("IN"), multispace1)),
802        value_expression,
803    )(input)?;
804    let (input, mut body) =
805        preceded(tuple((multispace0, char('|'), multispace0)), expression)(input)?;
806    let (input, _) = tuple((multispace0, char(')')))(input)?;
807    mark_locals(&mut body, &[accumulator, variable]);
808    Ok((
809        input,
810        ValueExpression::Reduce {
811            accumulator: accumulator.to_lowercase(),
812            initial: Box::new(initial),
813            variable: variable.to_lowercase(),
814            list: Box::new(list),
815            expression: Box::new(body),
816        },
817    ))
818}
819
820fn list_predicate(input: &str) -> IResult<&str, ValueExpression> {
821    let (input, kind) = alt((
822        tag_no_case("all"),
823        tag_no_case("any"),
824        tag_no_case("none"),
825        tag_no_case("single"),
826    ))(input)?;
827    let (input, variable) =
828        preceded(tuple((multispace0, char('('), multispace0)), identifier)(input)?;
829    let (input, list) = preceded(
830        tuple((multispace1, tag_no_case("IN"), multispace1)),
831        value_expression,
832    )(input)?;
833    let (input, mut predicate) = preceded(
834        tuple((multispace1, tag_no_case("WHERE"), multispace1)),
835        expression,
836    )(input)?;
837    let (input, _) = tuple((multispace0, char(')')))(input)?;
838    mark_locals(&mut predicate, &[variable]);
839    Ok((
840        input,
841        ValueExpression::ListPredicate {
842            kind: kind.to_lowercase(),
843            variable: variable.to_lowercase(),
844            list: Box::new(list),
845            predicate: Box::new(predicate),
846        },
847    ))
848}
849
850// `{key: value, ...}`
851fn map_literal(input: &str) -> IResult<&str, ValueExpression> {
852    let entry = tuple((
853        multispace0,
854        identifier,
855        multispace0,
856        char(':'),
857        multispace0,
858        expression,
859    ));
860    let (input, entries) = delimited(
861        tuple((char('{'), multispace0)),
862        separated_list0(tuple((multispace0, char(','))), entry),
863        tuple((multispace0, char('}'))),
864    )(input)?;
865    Ok((
866        input,
867        ValueExpression::Map(
868            entries
869                .into_iter()
870                .map(|(_, k, _, _, _, v)| (k.to_string(), v))
871                .collect(),
872        ),
873    ))
874}
875
876// Parse a value expression: arithmetic over primaries, `*` `/` `%` binding tighter than
877// `+` `-`, all left-associative.
878fn value_expression(input: &str) -> IResult<&str, ValueExpression> {
879    arithmetic_level(
880        input,
881        &[
882            ('+', ArithmeticOperator::Add),
883            ('-', ArithmeticOperator::Subtract),
884        ],
885    )
886}
887
888fn arithmetic_level<'a>(
889    input: &'a str,
890    operators: &[(char, ArithmeticOperator)],
891) -> IResult<&'a str, ValueExpression> {
892    let tighter = |i: &'a str| {
893        if operators[0].0 == '+' {
894            arithmetic_level(
895                i,
896                &[
897                    ('*', ArithmeticOperator::Multiply),
898                    ('/', ArithmeticOperator::Divide),
899                    ('%', ArithmeticOperator::Modulo),
900                ],
901            )
902        } else {
903            primary_value_expression(i)
904        }
905    };
906    let (mut input, mut left) = tighter(input)?;
907    loop {
908        let Ok((after_op, (_, op, _))) = tuple((
909            multispace0,
910            one_of::<_, _, nom::error::Error<&str>>("+-*/%"),
911            multispace0,
912        ))(input) else {
913            break;
914        };
915        let Some((_, operator)) = operators.iter().find(|(c, _)| *c == op) else {
916            break;
917        };
918        // A right side that doesn't parse (e.g. `-[` of a pattern) ends the expression.
919        let Ok((rest, right)) = tighter(after_op) else {
920            break;
921        };
922        left = ValueExpression::Arithmetic {
923            left: Box::new(left),
924            operator: operator.clone(),
925            right: Box::new(right),
926        };
927        input = rest;
928    }
929    Ok((input, left))
930}
931
932// `( expr )`, `CASE ... END`, or a basic value, then any `[index]` suffixes
933fn primary_value_expression(input: &str) -> IResult<&str, ValueExpression> {
934    let (mut input, mut value) = alt((
935        delimited(
936            tuple((char('('), multispace0)),
937            value_expression,
938            tuple((multispace0, char(')'))),
939        ),
940        case_expression,
941        single_value_expression,
942    ))(input)?;
943    // `list[i]`, with the `[` right after the value (so `x IN [1, 2]` is not an index), and
944    // `value.field`.
945    loop {
946        if let Ok((rest, index)) = delimited(
947            tuple((char('['), multispace0)),
948            value_expression,
949            tuple((multispace0, char(']'))),
950        )(input)
951        {
952            value = ValueExpression::ScalarFunction {
953                name: "__index".to_string(),
954                args: vec![value, index],
955            };
956            input = rest;
957        } else if let Ok((rest, field)) = preceded(char('.'), identifier)(input) {
958            value = ValueExpression::Field {
959                base: Box::new(value),
960                field: field.to_string(),
961            };
962            input = rest;
963        } else {
964            break;
965        }
966    }
967    Ok((input, value))
968}
969
970// CASE [operand] WHEN ... THEN ... [ELSE ...] END
971fn case_expression(input: &str) -> IResult<&str, ValueExpression> {
972    let keyword = |k: &'static str| tuple((multispace0, tag_no_case(k), multispace1));
973    let (input, _) = tuple((tag_no_case("CASE"), multispace1))(input)?;
974    let (input, operand) = opt(terminated(expression, peek(keyword("WHEN"))))(input)?;
975    let (input, whens) = match &operand {
976        None => many1(tuple((
977            preceded(keyword("WHEN"), boolean_expression),
978            preceded(keyword("THEN"), expression),
979        )))(input)?,
980        Some(operand) => {
981            let (input, pairs) = many1(tuple((
982                preceded(keyword("WHEN"), expression),
983                preceded(keyword("THEN"), expression),
984            )))(input)?;
985            let whens = pairs
986                .into_iter()
987                .map(|(value, then)| {
988                    let condition = BooleanExpression::Comparison {
989                        left: operand.clone(),
990                        operator: ComparisonOperator::Equal,
991                        right: value,
992                    };
993                    (condition, then)
994                })
995                .collect();
996            (input, whens)
997        }
998    };
999    let (input, otherwise) = opt(preceded(keyword("ELSE"), expression))(input)?;
1000    let (input, _) = tuple((multispace0, tag_no_case("END")))(input)?;
1001    Ok((
1002        input,
1003        ValueExpression::Case {
1004            whens,
1005            otherwise: otherwise.map(Box::new),
1006        },
1007    ))
1008}
1009
1010// Optimization: Use peek to avoid expensive backtracking for non-vector queries
1011fn single_value_expression(input: &str) -> IResult<&str, ValueExpression> {
1012    // Peek at first identifier to dispatch to correct parser
1013    // This eliminates failed parser attempts for every non-vector expression
1014    if let Ok((_, first_ident)) = peek(identifier)(input) {
1015        let ident_lower = first_ident.to_lowercase();
1016
1017        match ident_lower.as_str() {
1018            "vector_distance" => return parse_vector_distance(input),
1019            "vector_similarity" => return parse_vector_similarity(input),
1020            _ => {} // Not a vector function, continue to basic expressions
1021        }
1022    }
1023
1024    // Fast path for common expressions
1025    basic_value_expression(input)
1026}
1027
1028// Parse distance metric: cosine, l2, dot
1029fn parse_distance_metric(input: &str) -> IResult<&str, DistanceMetric> {
1030    alt((
1031        map(tag_no_case("cosine"), |_| DistanceMetric::Cosine),
1032        map(tag_no_case("l2"), |_| DistanceMetric::L2),
1033        map(tag_no_case("dot"), |_| DistanceMetric::Dot),
1034    ))(input)
1035}
1036
1037// Parse vector_distance(expr, expr, metric)
1038fn parse_vector_distance(input: &str) -> IResult<&str, ValueExpression> {
1039    let (input, _) = tag_no_case("vector_distance")(input)?;
1040    let (input, _) = multispace0(input)?;
1041    let (input, _) = char('(')(input)?;
1042    let (input, _) = multispace0(input)?;
1043
1044    // Parse left expression - use basic_value_expression to avoid circular dependency
1045    let (input, left) = basic_value_expression(input)?;
1046    let (input, _) = multispace0(input)?;
1047    let (input, _) = char(',')(input)?;
1048    let (input, _) = multispace0(input)?;
1049
1050    // Parse right expression - use basic_value_expression to avoid circular dependency
1051    let (input, right) = basic_value_expression(input)?;
1052    let (input, _) = multispace0(input)?;
1053    let (input, _) = char(',')(input)?;
1054    let (input, _) = multispace0(input)?;
1055
1056    // Parse metric
1057    let (input, metric) = parse_distance_metric(input)?;
1058    let (input, _) = multispace0(input)?;
1059    let (input, _) = char(')')(input)?;
1060
1061    Ok((
1062        input,
1063        ValueExpression::VectorDistance {
1064            left: Box::new(left),
1065            right: Box::new(right),
1066            metric,
1067        },
1068    ))
1069}
1070
1071// Parse vector_similarity(expr, expr, metric)
1072fn parse_vector_similarity(input: &str) -> IResult<&str, ValueExpression> {
1073    let (input, _) = tag_no_case("vector_similarity")(input)?;
1074    let (input, _) = multispace0(input)?;
1075    let (input, _) = char('(')(input)?;
1076    let (input, _) = multispace0(input)?;
1077
1078    // Parse left expression - use basic_value_expression to avoid circular dependency
1079    let (input, left) = basic_value_expression(input)?;
1080    let (input, _) = multispace0(input)?;
1081    let (input, _) = char(',')(input)?;
1082    let (input, _) = multispace0(input)?;
1083
1084    // Parse right expression - use basic_value_expression to avoid circular dependency
1085    let (input, right) = basic_value_expression(input)?;
1086    let (input, _) = multispace0(input)?;
1087    let (input, _) = char(',')(input)?;
1088    let (input, _) = multispace0(input)?;
1089
1090    // Parse metric
1091    let (input, metric) = parse_distance_metric(input)?;
1092    let (input, _) = multispace0(input)?;
1093    let (input, _) = char(')')(input)?;
1094
1095    Ok((
1096        input,
1097        ValueExpression::VectorSimilarity {
1098            left: Box::new(left),
1099            right: Box::new(right),
1100            metric,
1101        },
1102    ))
1103}
1104
1105// Parse parameter reference: $name
1106fn parse_parameter(input: &str) -> IResult<&str, ValueExpression> {
1107    let (input, name) = parameter(input)?;
1108    Ok((input, ValueExpression::Parameter(name)))
1109}
1110
1111// Parse a function call: function_name(args)
1112fn function_call(input: &str) -> IResult<&str, ValueExpression> {
1113    // Namespaced names such as `duration.inSeconds`.
1114    let (input, name) = recognize(pair(identifier, many0(preceded(char('.'), identifier))))(input)?;
1115    let (input, _) = multispace0(input)?;
1116    let (input, _) = char('(')(input)?;
1117    let (input, _) = multispace0(input)?;
1118
1119    // Parse optional DISTINCT keyword
1120    let (input, distinct) = opt(tag_no_case("DISTINCT"))(input)?;
1121    let distinct = distinct.is_some();
1122    let (input, _) = if distinct {
1123        multispace1(input)?
1124    } else {
1125        (input, "")
1126    };
1127
1128    // Handle COUNT(*) special case - only allow * for COUNT function
1129    if let Ok((input_after_star, _)) = char::<_, nom::error::Error<&str>>('*')(input) {
1130        // Validate that this is COUNT function
1131        if name.to_lowercase() == "count" {
1132            let (input, _) = multispace0(input_after_star)?;
1133            let (input, _) = char(')')(input)?;
1134            return Ok((
1135                input,
1136                ValueExpression::AggregateFunction {
1137                    name: name.to_string(),
1138                    args: vec![ValueExpression::Variable("*".to_string())],
1139                    distinct,
1140                },
1141            ));
1142        } else {
1143            // Not COUNT - fail parsing to try regular argument parsing
1144            // This will naturally fail since * is not a valid value_expression
1145        }
1146    }
1147
1148    // Parse regular function arguments
1149    let (input, args) =
1150        separated_list0(tuple((multispace0, char(','), multispace0)), expression)(input)?;
1151    let (input, _) = multispace0(input)?;
1152    let (input, _) = char(')')(input)?;
1153
1154    // Route based on function type
1155    use crate::ast::{classify_function, FunctionType};
1156    match classify_function(name) {
1157        FunctionType::Aggregate => Ok((
1158            input,
1159            ValueExpression::AggregateFunction {
1160                name: name.to_string(),
1161                args,
1162                distinct,
1163            },
1164        )),
1165        FunctionType::Scalar => {
1166            // Validate: reject DISTINCT on scalar functions at parse time
1167            if distinct {
1168                return Err(nom::Err::Failure(nom::error::Error::new(
1169                    input,
1170                    nom::error::ErrorKind::Verify,
1171                )));
1172            }
1173            Ok((
1174                input,
1175                ValueExpression::ScalarFunction {
1176                    name: name.to_string(),
1177                    args,
1178                },
1179            ))
1180        }
1181        FunctionType::Unknown => {
1182            // Default to ScalarFunction for unknown functions
1183            // They'll be handled as NULL in expression conversion
1184            if distinct {
1185                return Err(nom::Err::Failure(nom::error::Error::new(
1186                    input,
1187                    nom::error::ErrorKind::Verify,
1188                )));
1189            }
1190            Ok((
1191                input,
1192                ValueExpression::ScalarFunction {
1193                    name: name.to_string(),
1194                    args,
1195                },
1196            ))
1197        }
1198    }
1199}
1200
1201fn value_expression_list(input: &str) -> IResult<&str, Vec<ValueExpression>> {
1202    delimited(
1203        tuple((char('['), multispace0)),
1204        separated_list1(
1205            tuple((multispace0, char(','), multispace0)),
1206            value_expression,
1207        ),
1208        tuple((multispace0, char(']'))),
1209    )(input)
1210}
1211
1212// Parse a float32 literal for vectors
1213fn float32_literal(input: &str) -> IResult<&str, f32> {
1214    map_res(
1215        recognize(tuple((
1216            opt(char('-')),
1217            alt((
1218                // Scientific notation: 1e-3, 2.5e2
1219                recognize(tuple((
1220                    digit1,
1221                    opt(tuple((char('.'), digit0))),
1222                    one_of("eE"),
1223                    opt(one_of("+-")),
1224                    digit1,
1225                ))),
1226                // Regular float: 1.23 or integer: 123
1227                recognize(tuple((digit1, opt(tuple((char('.'), digit0)))))),
1228            )),
1229        ))),
1230        |s: &str| s.parse::<f32>(),
1231    )(input)
1232}
1233
1234// Parse vector literal: [0.1, 0.2, 0.3]
1235fn parse_vector_literal(input: &str) -> IResult<&str, ValueExpression> {
1236    let literal = input;
1237    let (input, _) = char('[')(input)?;
1238    let (input, _) = multispace0(input)?;
1239
1240    let (input, values) = separated_list1(
1241        tuple((multispace0, char(','), multispace0)),
1242        float32_literal,
1243    )(input)?;
1244
1245    let (input, _) = multispace0(input)?;
1246    let (input, _) = char(']')(input)?;
1247
1248    // Integer lists must retain Int64 precision outside vector operations.
1249    if !literal[..literal.len() - input.len()]
1250        .chars()
1251        .any(|c| matches!(c, '.' | 'e' | 'E'))
1252    {
1253        return Err(nom::Err::Error(nom::error::Error::new(
1254            literal,
1255            nom::error::ErrorKind::Verify,
1256        )));
1257    }
1258
1259    Ok((input, ValueExpression::VectorLiteral(values)))
1260}
1261
1262// `[expr, ...]` or `[]`: a list of any values (all-float lists parse as vectors first)
1263fn list_literal(input: &str) -> IResult<&str, ValueExpression> {
1264    let (input, items) = delimited(
1265        tuple((char('['), multispace0)),
1266        separated_list0(tuple((multispace0, char(','), multispace0)), expression),
1267        tuple((multispace0, char(']'))),
1268    )(input)?;
1269    Ok((
1270        input,
1271        ValueExpression::ScalarFunction {
1272            name: "__list".to_string(),
1273            args: items,
1274        },
1275    ))
1276}
1277
1278// Parse a property reference: variable.property
1279fn property_reference(input: &str) -> IResult<&str, PropertyRef> {
1280    let (input, variable) = identifier(input)?;
1281    let (input, _) = char('.')(input)?;
1282    let (input, property) = identifier(input)?;
1283
1284    Ok((
1285        input,
1286        PropertyRef {
1287            variable: variable.to_string(),
1288            property: property.to_string(),
1289        },
1290    ))
1291}
1292
1293// Parse a WITH clause (intermediate projection/aggregation)
1294fn with_clause(input: &str) -> IResult<&str, WithClause> {
1295    let (input, _) = multispace0(input)?;
1296    let (input, _) = tag_no_case("WITH")(input)?;
1297    let (input, _) = multispace1(input)?;
1298    let (input, distinct) = opt(terminated(tag_no_case("DISTINCT"), multispace1))(input)?;
1299    let (input, items) = separated_list0(comma_ws, return_item)(input)?;
1300    let (input, order_by) = opt(order_by_clause)(input)?;
1301    let (input, limit) = opt(limit_clause)(input)?;
1302    let (input, where_clause) = opt(where_clause)(input)?;
1303
1304    Ok((
1305        input,
1306        WithClause {
1307            distinct: distinct.is_some(),
1308            items,
1309            order_by,
1310            limit,
1311            where_clause,
1312        },
1313    ))
1314}
1315
1316// A WITH after the first, with its own reading clauses and WHERE
1317fn with_stage(input: &str) -> IResult<&str, WithStage> {
1318    let (input, with_clause) = with_clause(input)?;
1319    let (input, reading_clauses) = many0(reading_clause)(input)?;
1320    let (input, where_clause) = opt(where_clause)(input)?;
1321    Ok((
1322        input,
1323        WithStage {
1324            with_clause,
1325            reading_clauses,
1326            where_clause,
1327        },
1328    ))
1329}
1330
1331// Parse a RETURN clause
1332fn return_clause(input: &str) -> IResult<&str, ReturnClause> {
1333    let (input, _) = multispace0(input)?;
1334    let (input, _) = tag_no_case("RETURN")(input)?;
1335    let (input, _) = multispace1(input)?;
1336    let (input, distinct) = opt(tag_no_case("DISTINCT"))(input)?;
1337    let (input, _) = if distinct.is_some() {
1338        multispace1(input)?
1339    } else {
1340        (input, "")
1341    };
1342    let (input, items) = separated_list0(comma_ws, return_item)(input)?;
1343
1344    Ok((
1345        input,
1346        ReturnClause {
1347            distinct: distinct.is_some(),
1348            items,
1349        },
1350    ))
1351}
1352
1353// Parse a return item
1354fn return_item(input: &str) -> IResult<&str, ReturnItem> {
1355    let (input, expression) = expression(input)?;
1356    let (input, _) = multispace0(input)?;
1357    let (input, alias) = opt(preceded(
1358        tuple((tag_no_case("AS"), multispace1)),
1359        identifier,
1360    ))(input)?;
1361
1362    Ok((
1363        input,
1364        ReturnItem {
1365            expression,
1366            alias: alias.map(|s| s.to_string()),
1367        },
1368    ))
1369}
1370
1371// Match IS NULL in WHERE clause
1372fn is_null_comparison(input: &str) -> IResult<&str, ()> {
1373    let (input, _) = multispace0(input)?;
1374    let (input, _) = tag_no_case("IS")(input)?;
1375    let (input, _) = multispace1(input)?;
1376    let (input, _) = tag_no_case("NULL")(input)?;
1377    let (input, _) = multispace0(input)?;
1378
1379    Ok((input, ()))
1380}
1381
1382// Match IS NOT NULL in WHERE clause
1383fn is_not_null_comparison(input: &str) -> IResult<&str, ()> {
1384    let (input, _) = multispace0(input)?;
1385    let (input, _) = tag_no_case("IS")(input)?;
1386    let (input, _) = multispace1(input)?;
1387    let (input, _) = tag_no_case("NOT")(input)?;
1388    let (input, _) = multispace1(input)?;
1389    let (input, _) = tag_no_case("NULL")(input)?;
1390    let (input, _) = multispace0(input)?;
1391
1392    Ok((input, ()))
1393}
1394
1395// Parse an ORDER BY clause
1396fn order_by_clause(input: &str) -> IResult<&str, OrderByClause> {
1397    let (input, _) = multispace0(input)?;
1398    let (input, _) = tag_no_case("ORDER")(input)?;
1399    let (input, _) = multispace1(input)?;
1400    let (input, _) = tag_no_case("BY")(input)?;
1401    let (input, _) = multispace1(input)?;
1402    let (input, items) = separated_list0(comma_ws, order_by_item)(input)?;
1403
1404    Ok((input, OrderByClause { items }))
1405}
1406
1407// Parse an order by item
1408fn order_by_item(input: &str) -> IResult<&str, OrderByItem> {
1409    let (input, expression) = value_expression(input)?;
1410    let (input, _) = multispace0(input)?;
1411    let (input, direction) = opt(alt((
1412        map(tag_no_case("ASC"), |_| SortDirection::Ascending),
1413        map(tag_no_case("DESC"), |_| SortDirection::Descending),
1414    )))(input)?;
1415
1416    Ok((
1417        input,
1418        OrderByItem {
1419            expression,
1420            direction: direction.unwrap_or(SortDirection::Ascending),
1421        },
1422    ))
1423}
1424
1425// Parse a LIMIT clause
1426fn limit_clause(input: &str) -> IResult<&str, RowCount> {
1427    let (input, _) = multispace0(input)?;
1428    let (input, _) = tag_no_case("LIMIT")(input)?;
1429    let (input, _) = multispace1(input)?;
1430    row_count(input)
1431}
1432
1433// Parse a SKIP clause
1434fn skip_clause(input: &str) -> IResult<&str, RowCount> {
1435    let (input, _) = multispace0(input)?;
1436    let (input, _) = tag_no_case("SKIP")(input)?;
1437    let (input, _) = multispace1(input)?;
1438    row_count(input)
1439}
1440
1441fn row_count(input: &str) -> IResult<&str, RowCount> {
1442    if let Ok((remaining, name)) = parameter(input) {
1443        return Ok((remaining, RowCount::Parameter(name)));
1444    }
1445    let (remaining, value) = integer_literal(input)?;
1446    let value = u64::try_from(value).map_err(|_| {
1447        nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Verify))
1448    })?;
1449    Ok((remaining, RowCount::Literal(value)))
1450}
1451
1452// Parse pagination clauses (SKIP and LIMIT)
1453fn pagination_clauses(input: &str) -> IResult<&str, (Option<RowCount>, Option<RowCount>)> {
1454    let (mut remaining, _) = multispace0(input)?;
1455    let mut skip: Option<RowCount> = None;
1456    let mut limit: Option<RowCount> = None;
1457
1458    loop {
1459        let before = remaining;
1460
1461        if skip.is_none() {
1462            if let Ok((i, s)) = skip_clause(remaining) {
1463                skip = Some(s);
1464                remaining = i;
1465                continue;
1466            }
1467        }
1468
1469        if limit.is_none() {
1470            if let Ok((i, l)) = limit_clause(remaining) {
1471                limit = Some(l);
1472                remaining = i;
1473                continue;
1474            }
1475        }
1476
1477        if before == remaining {
1478            break;
1479        }
1480    }
1481
1482    Ok((remaining, (skip, limit)))
1483}
1484
1485// Helper parsers
1486
1487// Parse an identifier
1488fn identifier(input: &str) -> IResult<&str, &str> {
1489    take_while1(|c: char| c.is_alphanumeric() || c == '_')(input)
1490}
1491
1492// Parse a string literal
1493fn string_literal(input: &str) -> IResult<&str, String> {
1494    alt((double_quoted_string, single_quoted_string))(input)
1495}
1496
1497fn double_quoted_string(input: &str) -> IResult<&str, String> {
1498    let (input, _) = char('"')(input)?;
1499    let (input, content) = take_while(|c| c != '"')(input)?;
1500    let (input, _) = char('"')(input)?;
1501    Ok((input, content.to_string()))
1502}
1503
1504fn single_quoted_string(input: &str) -> IResult<&str, String> {
1505    let (input, _) = char('\'')(input)?;
1506    let (input, content) = take_while(|c| c != '\'')(input)?;
1507    let (input, _) = char('\'')(input)?;
1508    Ok((input, content.to_string()))
1509}
1510
1511// Parse an integer literal
1512fn integer_literal(input: &str) -> IResult<&str, i64> {
1513    let (input, digits) = recognize(pair(
1514        opt(char('-')),
1515        take_while1(|c: char| c.is_ascii_digit()),
1516    ))(input)?;
1517
1518    // Out-of-range literals fail to parse instead of panicking.
1519    let value = digits.parse().map_err(|_| {
1520        nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Digit))
1521    })?;
1522    Ok((input, value))
1523}
1524
1525// Parse a float literal
1526fn float_literal(input: &str) -> IResult<&str, f64> {
1527    let (input, number) = recognize(tuple((
1528        opt(char('-')),
1529        take_while1(|c: char| c.is_ascii_digit()),
1530        char('.'),
1531        take_while1(|c: char| c.is_ascii_digit()),
1532    )))(input)?;
1533
1534    let value: f64 = number.parse().map_err(|_| {
1535        nom::Err::Failure(nom::error::Error::new(input, nom::error::ErrorKind::Float))
1536    })?;
1537    if !value.is_finite() {
1538        return Err(nom::Err::Failure(nom::error::Error::new(
1539            input,
1540            nom::error::ErrorKind::Float,
1541        )));
1542    }
1543    Ok((input, value))
1544}
1545
1546// Parse a boolean literal
1547fn boolean_literal(input: &str) -> IResult<&str, bool> {
1548    alt((
1549        map(tag_no_case("true"), |_| true),
1550        map(tag_no_case("false"), |_| false),
1551    ))(input)
1552}
1553
1554// Parse a parameter reference
1555fn parameter(input: &str) -> IResult<&str, String> {
1556    // Only support $param syntax
1557    map(preceded(char('$'), identifier), |s| s.to_string())(input)
1558}
1559
1560// Parse comma with optional whitespace
1561fn comma_ws(input: &str) -> IResult<&str, ()> {
1562    let (input, _) = multispace0(input)?;
1563    let (input, _) = char(',')(input)?;
1564    let (input, _) = multispace0(input)?;
1565    Ok((input, ()))
1566}
1567
1568// Parse variable-length path syntax: *1..2, *..3, *2.., *
1569fn length_range(input: &str) -> IResult<&str, LengthRange> {
1570    let (input, _) = char('*')(input)?;
1571    let (input, _) = multispace0(input)?;
1572
1573    // Parse different length patterns
1574    alt((
1575        // *min..max (e.g., *1..3)
1576        map(
1577            tuple((
1578                nom::character::complete::u32,
1579                tag(".."),
1580                nom::character::complete::u32,
1581            )),
1582            |(min, _, max)| LengthRange {
1583                min: Some(min),
1584                max: Some(max),
1585            },
1586        ),
1587        // *..max (e.g., *..3)
1588        map(preceded(tag(".."), nom::character::complete::u32), |max| {
1589            LengthRange {
1590                min: None,
1591                max: Some(max),
1592            }
1593        }),
1594        // *min.. (e.g., *2..)
1595        map(
1596            tuple((nom::character::complete::u32, tag(".."))),
1597            |(min, _)| LengthRange {
1598                min: Some(min),
1599                max: None,
1600            },
1601        ),
1602        // *min (e.g., *2)
1603        map(nom::character::complete::u32, |min| LengthRange {
1604            min: Some(min),
1605            max: Some(min),
1606        }),
1607        // * (unlimited)
1608        map(multispace0, |_| LengthRange {
1609            min: None,
1610            max: None,
1611        }),
1612    ))(input)
1613}
1614
1615#[cfg(test)]
1616mod tests {
1617    use super::*;
1618    use crate::ast::{BooleanExpression, ComparisonOperator, PropertyValue, ValueExpression};
1619
1620    #[test]
1621    fn test_optional_trailing_semicolon() {
1622        for query in [
1623            "MATCH (n:Person) RETURN n.name",
1624            "MATCH (n:Person) RETURN n.id ORDER BY n.id SKIP 1 LIMIT 2",
1625            "MATCH (n:Person) WITH n.name AS name RETURN name",
1626            "MATCH (n:Person) RETURN 'a;b' AS text",
1627        ] {
1628            let expected = parse_cypher_query(query).unwrap();
1629            for suffix in [";", " ;", "; \n\t"] {
1630                assert_eq!(
1631                    parse_cypher_query(&format!("{query}{suffix}")).unwrap(),
1632                    expected
1633                );
1634            }
1635        }
1636    }
1637
1638    #[test]
1639    fn test_semicolon_does_not_allow_multiple_statements() {
1640        for query in [
1641            ";",
1642            "MATCH (n:Person) RETURN n.id;;",
1643            "MATCH (n:Person) RETURN n.id; MATCH (m:Person) RETURN m.id",
1644            "MATCH (n:Person) RETURN n.id; garbage",
1645        ] {
1646            assert!(parse_cypher_query(query).is_err(), "accepted {query}");
1647        }
1648    }
1649
1650    #[test]
1651    fn test_parse_simple_node_query() {
1652        let query = "MATCH (n:Person) RETURN n.name";
1653        let result = parse_cypher_query(query).unwrap();
1654
1655        assert_eq!(result.reading_clauses.len(), 1);
1656        assert_eq!(result.return_clause.items.len(), 1);
1657    }
1658
1659    #[test]
1660    fn test_parse_node_with_properties() {
1661        let query = r#"MATCH (n:Person {name: "John", age: 30}) RETURN n"#;
1662        let result = parse_cypher_query(query).unwrap();
1663
1664        if let ReadingClause::Match(match_clause) = &result.reading_clauses[0] {
1665            if let GraphPattern::Node(node) = &match_clause.patterns[0] {
1666                assert_eq!(node.labels, vec!["Person"]);
1667                assert_eq!(node.properties.len(), 2);
1668            } else {
1669                panic!("Expected node pattern");
1670            }
1671        } else {
1672            panic!("Expected match clause");
1673        }
1674    }
1675
1676    #[test]
1677    fn test_parse_simple_relationship_query() {
1678        let query = "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a.name, b.name";
1679        let result = parse_cypher_query(query).unwrap();
1680
1681        assert_eq!(result.reading_clauses.len(), 1);
1682        assert_eq!(result.return_clause.items.len(), 2);
1683
1684        if let ReadingClause::Match(match_clause) = &result.reading_clauses[0] {
1685            if let GraphPattern::Path(path) = &match_clause.patterns[0] {
1686                assert_eq!(path.segments.len(), 1);
1687                assert_eq!(path.segments[0].relationship.types, vec!["KNOWS"]);
1688            } else {
1689                panic!("Expected path pattern");
1690            }
1691        } else {
1692            panic!("Expected match clause");
1693        }
1694    }
1695
1696    #[test]
1697    fn test_parse_variable_length_path() {
1698        let query = "MATCH (a:Person)-[:FRIEND_OF*1..2]-(b:Person) RETURN a.name, b.name";
1699        let result = parse_cypher_query(query).unwrap();
1700
1701        assert_eq!(result.reading_clauses.len(), 1);
1702
1703        if let ReadingClause::Match(match_clause) = &result.reading_clauses[0] {
1704            if let GraphPattern::Path(path) = &match_clause.patterns[0] {
1705                assert_eq!(path.segments.len(), 1);
1706                assert_eq!(path.segments[0].relationship.types, vec!["FRIEND_OF"]);
1707
1708                let length = path.segments[0].relationship.length.as_ref().unwrap();
1709                assert_eq!(length.min, Some(1));
1710                assert_eq!(length.max, Some(2));
1711            } else {
1712                panic!("Expected path pattern");
1713            }
1714        } else {
1715            panic!("Expected match clause");
1716        }
1717    }
1718
1719    #[test]
1720    fn test_parse_query_with_where_clause() {
1721        let query = "MATCH (n:Person) WHERE n.age > 30 RETURN n.name";
1722        let result = parse_cypher_query(query).unwrap();
1723
1724        assert!(result.where_clause.is_some());
1725    }
1726
1727    #[test]
1728    fn test_parse_query_with_single_quoted_literal() {
1729        let query = "MATCH (n:Person) WHERE n.name = 'Alice' RETURN n.name";
1730        let result = parse_cypher_query(query).unwrap();
1731
1732        assert!(result.where_clause.is_some());
1733    }
1734
1735    #[test]
1736    fn test_parse_query_with_and_conditions() {
1737        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";
1738        let result = parse_cypher_query(query).unwrap();
1739
1740        let where_clause = result.where_clause.expect("Expected WHERE clause");
1741        match where_clause.expression {
1742            BooleanExpression::And(left, right) => {
1743                match *left {
1744                    BooleanExpression::Comparison {
1745                        left: ValueExpression::Property(ref prop),
1746                        operator,
1747                        right: ValueExpression::Literal(PropertyValue::String(ref value)),
1748                    } => {
1749                        assert_eq!(prop.variable, "rel");
1750                        assert_eq!(prop.property, "relationship_type");
1751                        assert_eq!(operator, ComparisonOperator::Equal);
1752                        assert_eq!(value, "WORKS_ON");
1753                    }
1754                    _ => panic!("Expected comparison for relationship_type filter"),
1755                }
1756
1757                match *right {
1758                    BooleanExpression::Comparison {
1759                        left: ValueExpression::Property(ref prop),
1760                        operator,
1761                        right: ValueExpression::Literal(PropertyValue::String(ref value)),
1762                    } => {
1763                        assert_eq!(prop.variable, "dst");
1764                        assert_eq!(prop.property, "name_lower");
1765                        assert_eq!(operator, ComparisonOperator::Equal);
1766                        assert_eq!(value, "presto");
1767                    }
1768                    _ => panic!("Expected comparison for destination name filter"),
1769                }
1770            }
1771            other => panic!("Expected AND expression, got {:?}", other),
1772        }
1773    }
1774
1775    #[test]
1776    fn test_parse_query_with_in_clause() {
1777        let query = "MATCH (src:Entity)-[rel:RELATIONSHIP]->(dst:Entity) WHERE rel.relationship_type IN ['WORKS_FOR', 'PART_OF'] RETURN src.name";
1778        let result = parse_cypher_query(query).unwrap();
1779
1780        let where_clause = result.where_clause.expect("Expected WHERE clause");
1781        match where_clause.expression {
1782            BooleanExpression::In { expression, list } => {
1783                match expression {
1784                    ValueExpression::Property(prop_ref) => {
1785                        assert_eq!(prop_ref.variable, "rel");
1786                        assert_eq!(prop_ref.property, "relationship_type");
1787                    }
1788                    _ => panic!("Expected property reference in IN expression"),
1789                }
1790                assert_eq!(list.len(), 2);
1791                match &list[0] {
1792                    ValueExpression::Literal(PropertyValue::String(val)) => {
1793                        assert_eq!(val, "WORKS_FOR");
1794                    }
1795                    _ => panic!("Expected first list item to be a string literal"),
1796                }
1797                match &list[1] {
1798                    ValueExpression::Literal(PropertyValue::String(val)) => {
1799                        assert_eq!(val, "PART_OF");
1800                    }
1801                    _ => panic!("Expected second list item to be a string literal"),
1802                }
1803            }
1804            other => panic!("Expected IN expression, got {:?}", other),
1805        }
1806    }
1807
1808    #[test]
1809    fn test_parse_query_with_is_null() {
1810        let query = "MATCH (n:Person) WHERE n.age IS NULL RETURN n.name";
1811        let result = parse_cypher_query(query).unwrap();
1812
1813        let where_clause = result.where_clause.expect("Expected WHERE clause");
1814
1815        match where_clause.expression {
1816            BooleanExpression::IsNull(expr) => match expr {
1817                ValueExpression::Property(prop_ref) => {
1818                    assert_eq!(prop_ref.variable, "n");
1819                    assert_eq!(prop_ref.property, "age");
1820                }
1821                _ => panic!("Expected property reference in IS NULL expression"),
1822            },
1823            other => panic!("Expected IS NULL expression, got {:?}", other),
1824        }
1825    }
1826
1827    #[test]
1828    fn test_parse_query_with_is_not_null() {
1829        let query = "MATCH (n:Person) WHERE n.age IS NOT NULL RETURN n.name";
1830        let result = parse_cypher_query(query).unwrap();
1831
1832        let where_clause = result.where_clause.expect("Expected WHERE clause");
1833
1834        match where_clause.expression {
1835            BooleanExpression::IsNotNull(expr) => match expr {
1836                ValueExpression::Property(prop_ref) => {
1837                    assert_eq!(prop_ref.variable, "n");
1838                    assert_eq!(prop_ref.property, "age");
1839                }
1840                _ => panic!("Expected property reference in IS NOT NULL expression"),
1841            },
1842            other => panic!("Expected IS NOT NULL expression, got {:?}", other),
1843        }
1844    }
1845
1846    #[test]
1847    fn test_parse_query_with_limit() {
1848        let query = "MATCH (n:Person) RETURN n.name LIMIT 10";
1849        let result = parse_cypher_query(query).unwrap();
1850
1851        assert_eq!(result.limit, Some(RowCount::Literal(10)));
1852    }
1853
1854    #[test]
1855    fn test_parse_query_with_skip() {
1856        let query = "MATCH (n:Person) RETURN n.name SKIP 5";
1857        let result = parse_cypher_query(query).unwrap();
1858
1859        assert_eq!(result.skip, Some(RowCount::Literal(5)));
1860        assert_eq!(result.limit, None);
1861    }
1862
1863    #[test]
1864    fn test_parse_query_with_skip_and_limit() {
1865        let query = "MATCH (n:Person) RETURN n.name SKIP 5 LIMIT 10";
1866        let result = parse_cypher_query(query).unwrap();
1867
1868        assert_eq!(result.skip, Some(RowCount::Literal(5)));
1869        assert_eq!(result.limit, Some(RowCount::Literal(10)));
1870    }
1871
1872    #[test]
1873    fn test_parse_query_with_skip_and_order_by() {
1874        let query = "MATCH (n:Person) RETURN n.name ORDER BY n.age SKIP 5";
1875        let result = parse_cypher_query(query).unwrap();
1876
1877        assert_eq!(result.skip, Some(RowCount::Literal(5)));
1878        assert!(result.order_by.is_some());
1879    }
1880
1881    #[test]
1882    fn test_parse_query_with_skip_order_by_and_limit() {
1883        let query = "MATCH (n:Person) RETURN n.name ORDER BY n.age SKIP 5 LIMIT 10";
1884        let result = parse_cypher_query(query).unwrap();
1885
1886        assert_eq!(result.skip, Some(RowCount::Literal(5)));
1887        assert_eq!(result.limit, Some(RowCount::Literal(10)));
1888        assert!(result.order_by.is_some());
1889    }
1890
1891    #[test]
1892    fn test_parse_count_star() {
1893        let query = "MATCH (n:Person) RETURN count(*) AS total";
1894        let result = parse_cypher_query(query).unwrap();
1895
1896        assert_eq!(result.return_clause.items.len(), 1);
1897        let item = &result.return_clause.items[0];
1898        assert_eq!(item.alias, Some("total".to_string()));
1899
1900        match &item.expression {
1901            ValueExpression::AggregateFunction { name, args, .. } => {
1902                assert_eq!(name, "count");
1903                assert_eq!(args.len(), 1);
1904                match &args[0] {
1905                    ValueExpression::Variable(v) => assert_eq!(v, "*"),
1906                    _ => panic!("Expected Variable(*) in count(*)"),
1907                }
1908            }
1909            _ => panic!("Expected AggregateFunction expression"),
1910        }
1911    }
1912
1913    #[test]
1914    fn test_parse_count_property() {
1915        let query = "MATCH (n:Person) RETURN count(n.age)";
1916        let result = parse_cypher_query(query).unwrap();
1917
1918        assert_eq!(result.return_clause.items.len(), 1);
1919        let item = &result.return_clause.items[0];
1920
1921        match &item.expression {
1922            ValueExpression::AggregateFunction { name, args, .. } => {
1923                assert_eq!(name, "count");
1924                assert_eq!(args.len(), 1);
1925                match &args[0] {
1926                    ValueExpression::Property(prop) => {
1927                        assert_eq!(prop.variable, "n");
1928                        assert_eq!(prop.property, "age");
1929                    }
1930                    _ => panic!("Expected Property in count(n.age)"),
1931                }
1932            }
1933            _ => panic!("Expected AggregateFunction expression"),
1934        }
1935    }
1936
1937    #[test]
1938    fn test_parse_non_count_function_rejects_star() {
1939        // FOO(*) should fail to parse since * is only allowed for COUNT
1940        let query = "MATCH (n:Person) RETURN foo(*)";
1941        let result = parse_cypher_query(query);
1942        assert!(result.is_err(), "foo(*) should not parse successfully");
1943    }
1944
1945    #[test]
1946    fn test_parse_count_with_multiple_args() {
1947        // COUNT with multiple arguments parses successfully
1948        // but will be rejected during semantic validation
1949        let query = "MATCH (n:Person) RETURN count(n.age, n.name)";
1950        let result = parse_cypher_query(query);
1951        assert!(
1952            result.is_ok(),
1953            "Parser should accept multiple args (validation happens in semantic phase)"
1954        );
1955
1956        // Verify the AST structure
1957        let ast = result.unwrap();
1958        match &ast.return_clause.items[0].expression {
1959            ValueExpression::AggregateFunction { name, args, .. } => {
1960                assert_eq!(name, "count");
1961                assert_eq!(args.len(), 2);
1962            }
1963            _ => panic!("Expected AggregateFunction expression"),
1964        }
1965    }
1966
1967    #[test]
1968    fn test_parser_rejects_distinct_on_scalar() {
1969        // Parser should reject DISTINCT on scalar functions at parse time
1970        let query = "RETURN toLower(DISTINCT p.name)";
1971        let result = parse_cypher_query(query);
1972        assert!(
1973            result.is_err(),
1974            "Parser should reject DISTINCT on scalar functions"
1975        );
1976
1977        let query2 = "RETURN upper(DISTINCT p.name)";
1978        let result2 = parse_cypher_query(query2);
1979        assert!(
1980            result2.is_err(),
1981            "Parser should reject DISTINCT on scalar functions"
1982        );
1983    }
1984
1985    #[test]
1986    fn test_parse_like_pattern() {
1987        let query = "MATCH (n:Person) WHERE n.name LIKE 'A%' RETURN n.name";
1988        let result = parse_cypher_query(query);
1989        assert!(result.is_ok(), "LIKE pattern should parse successfully");
1990
1991        let ast = result.unwrap();
1992        let where_clause = ast.where_clause.expect("Expected WHERE clause");
1993
1994        match where_clause.expression {
1995            BooleanExpression::Like {
1996                expression,
1997                pattern,
1998            } => {
1999                match expression {
2000                    ValueExpression::Property(prop) => {
2001                        assert_eq!(prop.variable, "n");
2002                        assert_eq!(prop.property, "name");
2003                    }
2004                    _ => panic!("Expected property expression"),
2005                }
2006                assert_eq!(pattern, "A%");
2007            }
2008            _ => panic!("Expected LIKE expression"),
2009        }
2010    }
2011
2012    #[test]
2013    fn test_parse_like_with_double_quotes() {
2014        let query = r#"MATCH (n:Person) WHERE n.email LIKE "%@example.com" RETURN n.email"#;
2015        let result = parse_cypher_query(query);
2016        assert!(result.is_ok(), "LIKE with double quotes should parse");
2017
2018        let ast = result.unwrap();
2019        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2020
2021        match where_clause.expression {
2022            BooleanExpression::Like { pattern, .. } => {
2023                assert_eq!(pattern, "%@example.com");
2024            }
2025            _ => panic!("Expected LIKE expression"),
2026        }
2027    }
2028
2029    #[test]
2030    fn test_parse_like_in_complex_where() {
2031        let query = "MATCH (n:Person) WHERE n.age > 20 AND n.name LIKE 'J%' RETURN n.name";
2032        let result = parse_cypher_query(query);
2033        assert!(result.is_ok(), "LIKE in complex WHERE should parse");
2034
2035        let ast = result.unwrap();
2036        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2037
2038        match where_clause.expression {
2039            BooleanExpression::And(left, right) => {
2040                // Left should be age > 20
2041                match *left {
2042                    BooleanExpression::Comparison { .. } => {}
2043                    _ => panic!("Expected comparison on left"),
2044                }
2045                // Right should be LIKE
2046                match *right {
2047                    BooleanExpression::Like { pattern, .. } => {
2048                        assert_eq!(pattern, "J%");
2049                    }
2050                    _ => panic!("Expected LIKE expression on right"),
2051                }
2052            }
2053            _ => panic!("Expected AND expression"),
2054        }
2055    }
2056
2057    #[test]
2058    fn test_parse_contains() {
2059        let query = "MATCH (n:Person) WHERE n.name CONTAINS 'Jo' RETURN n.name";
2060        let result = parse_cypher_query(query);
2061        assert!(result.is_ok());
2062
2063        let query = result.unwrap();
2064        assert!(query.where_clause.is_some());
2065
2066        match &query.where_clause.unwrap().expression {
2067            BooleanExpression::Contains {
2068                expression,
2069                substring,
2070            } => {
2071                assert_eq!(substring, "Jo");
2072                match expression {
2073                    ValueExpression::Property(prop) => {
2074                        assert_eq!(prop.variable, "n");
2075                        assert_eq!(prop.property, "name");
2076                    }
2077                    _ => panic!("Expected property reference"),
2078                }
2079            }
2080            _ => panic!("Expected CONTAINS expression"),
2081        }
2082    }
2083
2084    #[test]
2085    fn test_parse_starts_with() {
2086        let query = "MATCH (n:Person) WHERE n.name STARTS WITH 'Alice' RETURN n.name";
2087        let result = parse_cypher_query(query);
2088        assert!(result.is_ok());
2089
2090        let query = result.unwrap();
2091        assert!(query.where_clause.is_some());
2092
2093        match &query.where_clause.unwrap().expression {
2094            BooleanExpression::StartsWith { expression, prefix } => {
2095                assert_eq!(prefix, "Alice");
2096                match expression {
2097                    ValueExpression::Property(prop) => {
2098                        assert_eq!(prop.variable, "n");
2099                        assert_eq!(prop.property, "name");
2100                    }
2101                    _ => panic!("Expected property reference"),
2102                }
2103            }
2104            _ => panic!("Expected STARTS WITH expression"),
2105        }
2106    }
2107
2108    #[test]
2109    fn test_parse_ends_with() {
2110        let query = "MATCH (n:Person) WHERE n.email ENDS WITH '@example.com' RETURN n.email";
2111        let result = parse_cypher_query(query);
2112        assert!(result.is_ok());
2113
2114        let query = result.unwrap();
2115        assert!(query.where_clause.is_some());
2116
2117        match &query.where_clause.unwrap().expression {
2118            BooleanExpression::EndsWith { expression, suffix } => {
2119                assert_eq!(suffix, "@example.com");
2120                match expression {
2121                    ValueExpression::Property(prop) => {
2122                        assert_eq!(prop.variable, "n");
2123                        assert_eq!(prop.property, "email");
2124                    }
2125                    _ => panic!("Expected property reference"),
2126                }
2127            }
2128            _ => panic!("Expected ENDS WITH expression"),
2129        }
2130    }
2131
2132    #[test]
2133    fn test_parse_contains_case_insensitive_keyword() {
2134        let query = "MATCH (n:Person) WHERE n.name contains 'test' RETURN n.name";
2135        let result = parse_cypher_query(query);
2136        assert!(result.is_ok());
2137
2138        match &result.unwrap().where_clause.unwrap().expression {
2139            BooleanExpression::Contains { substring, .. } => {
2140                assert_eq!(substring, "test");
2141            }
2142            _ => panic!("Expected CONTAINS expression"),
2143        }
2144    }
2145
2146    #[test]
2147    fn test_parse_string_operators_in_complex_where() {
2148        let query =
2149            "MATCH (n:Person) WHERE n.name CONTAINS 'Jo' AND n.email ENDS WITH '.com' RETURN n";
2150        let result = parse_cypher_query(query);
2151        assert!(result.is_ok());
2152
2153        match &result.unwrap().where_clause.unwrap().expression {
2154            BooleanExpression::And(left, right) => {
2155                // Left should be CONTAINS
2156                match **left {
2157                    BooleanExpression::Contains { ref substring, .. } => {
2158                        assert_eq!(substring, "Jo");
2159                    }
2160                    _ => panic!("Expected CONTAINS expression on left"),
2161                }
2162                // Right should be ENDS WITH
2163                match **right {
2164                    BooleanExpression::EndsWith { ref suffix, .. } => {
2165                        assert_eq!(suffix, ".com");
2166                    }
2167                    _ => panic!("Expected ENDS WITH expression on right"),
2168                }
2169            }
2170            _ => panic!("Expected AND expression"),
2171        }
2172    }
2173
2174    #[test]
2175    fn test_parse_ilike_pattern() {
2176        let query = "MATCH (n:Person) WHERE n.name ILIKE 'alice%' RETURN n.name";
2177        let result = parse_cypher_query(query);
2178        assert!(result.is_ok(), "ILIKE pattern should parse successfully");
2179
2180        let ast = result.unwrap();
2181        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2182
2183        match where_clause.expression {
2184            BooleanExpression::ILike {
2185                expression,
2186                pattern,
2187            } => {
2188                match expression {
2189                    ValueExpression::Property(prop) => {
2190                        assert_eq!(prop.variable, "n");
2191                        assert_eq!(prop.property, "name");
2192                    }
2193                    _ => panic!("Expected property expression"),
2194                }
2195                assert_eq!(pattern, "alice%");
2196            }
2197            _ => panic!("Expected ILIKE expression"),
2198        }
2199    }
2200
2201    #[test]
2202    fn test_parse_like_and_ilike_together() {
2203        let query =
2204            "MATCH (n:Person) WHERE n.name LIKE 'Alice%' OR n.name ILIKE 'bob%' RETURN n.name";
2205        let result = parse_cypher_query(query);
2206        assert!(result.is_ok(), "LIKE and ILIKE together should parse");
2207
2208        let ast = result.unwrap();
2209        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2210
2211        match where_clause.expression {
2212            BooleanExpression::Or(left, right) => {
2213                // Left should be LIKE (case-sensitive)
2214                match *left {
2215                    BooleanExpression::Like { pattern, .. } => {
2216                        assert_eq!(pattern, "Alice%");
2217                    }
2218                    _ => panic!("Expected LIKE expression on left"),
2219                }
2220                // Right should be ILIKE (case-insensitive)
2221                match *right {
2222                    BooleanExpression::ILike { pattern, .. } => {
2223                        assert_eq!(pattern, "bob%");
2224                    }
2225                    _ => panic!("Expected ILIKE expression on right"),
2226                }
2227            }
2228            _ => panic!("Expected OR expression"),
2229        }
2230    }
2231
2232    #[test]
2233    fn test_parse_vector_distance() {
2234        let query = "MATCH (p:Person) WHERE vector_distance(p.embedding, $query_vec, cosine) < 0.5 RETURN p.name";
2235        let result = parse_cypher_query(query);
2236        assert!(result.is_ok(), "vector_distance should parse successfully");
2237
2238        let ast = result.unwrap();
2239        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2240
2241        // Verify it's a comparison with vector_distance
2242        match where_clause.expression {
2243            BooleanExpression::Comparison { left, operator, .. } => {
2244                match left {
2245                    ValueExpression::VectorDistance {
2246                        left,
2247                        right,
2248                        metric,
2249                    } => {
2250                        assert_eq!(metric, DistanceMetric::Cosine);
2251                        // Verify left is property reference
2252                        assert!(matches!(*left, ValueExpression::Property(_)));
2253                        // Verify right is parameter
2254                        assert!(matches!(*right, ValueExpression::Parameter(_)));
2255                    }
2256                    _ => panic!("Expected VectorDistance"),
2257                }
2258                assert_eq!(operator, ComparisonOperator::LessThan);
2259            }
2260            _ => panic!("Expected comparison"),
2261        }
2262    }
2263
2264    #[test]
2265    fn test_parse_vector_similarity() {
2266        let query =
2267            "MATCH (p:Person) WHERE vector_similarity(p.embedding, $vec, l2) > 0.8 RETURN p";
2268        let result = parse_cypher_query(query);
2269        assert!(
2270            result.is_ok(),
2271            "vector_similarity should parse successfully"
2272        );
2273
2274        let ast = result.unwrap();
2275        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2276
2277        match where_clause.expression {
2278            BooleanExpression::Comparison { left, operator, .. } => {
2279                match left {
2280                    ValueExpression::VectorSimilarity { metric, .. } => {
2281                        assert_eq!(metric, DistanceMetric::L2);
2282                    }
2283                    _ => panic!("Expected VectorSimilarity"),
2284                }
2285                assert_eq!(operator, ComparisonOperator::GreaterThan);
2286            }
2287            _ => panic!("Expected comparison"),
2288        }
2289    }
2290
2291    #[test]
2292    fn test_parse_parameter() {
2293        let query = "MATCH (p:Person) WHERE p.age = $min_age RETURN p";
2294        let result = parse_cypher_query(query);
2295        assert!(result.is_ok(), "Parameter should parse successfully");
2296
2297        let ast = result.unwrap();
2298        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2299
2300        match where_clause.expression {
2301            BooleanExpression::Comparison { right, .. } => match right {
2302                ValueExpression::Parameter(name) => {
2303                    assert_eq!(name, "min_age");
2304                }
2305                _ => panic!("Expected Parameter"),
2306            },
2307            _ => panic!("Expected comparison"),
2308        }
2309    }
2310
2311    #[test]
2312    fn test_parse_multiple_parameters() {
2313        let query = "MATCH (p:Person) WHERE p.age > $min_age AND p.age < $max_age RETURN p";
2314        let result = parse_cypher_query(query);
2315        assert!(
2316            result.is_ok(),
2317            "Multiple parameters should parse successfully"
2318        );
2319
2320        let ast = result.unwrap();
2321        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2322
2323        match where_clause.expression {
2324            BooleanExpression::And(left, right) => {
2325                // Check left: p.age > $min_age
2326                match *left {
2327                    BooleanExpression::Comparison {
2328                        right: val_right, ..
2329                    } => match val_right {
2330                        ValueExpression::Parameter(name) => {
2331                            assert_eq!(name, "min_age");
2332                        }
2333                        _ => panic!("Expected Parameter min_age"),
2334                    },
2335                    _ => panic!("Expected comparison on left"),
2336                }
2337
2338                // Check right: p.age < $max_age
2339                match *right {
2340                    BooleanExpression::Comparison {
2341                        right: val_right, ..
2342                    } => match val_right {
2343                        ValueExpression::Parameter(name) => {
2344                            assert_eq!(name, "max_age");
2345                        }
2346                        _ => panic!("Expected Parameter max_age"),
2347                    },
2348                    _ => panic!("Expected comparison on right"),
2349                }
2350            }
2351            _ => panic!("Expected AND expression"),
2352        }
2353    }
2354
2355    #[test]
2356    fn test_parse_parameter_formats() {
2357        // Test $param (should succeed)
2358        let query = "MATCH (p:Person) WHERE p.age > $min_age RETURN p";
2359        let result = parse_cypher_query(query);
2360        assert!(result.is_ok(), "$param should parse successfully");
2361    }
2362
2363    #[test]
2364    fn test_vector_distance_metrics() {
2365        for metric in &["cosine", "l2", "dot"] {
2366            let query = format!(
2367                "MATCH (p:Person) RETURN vector_distance(p.emb, $v, {}) AS dist",
2368                metric
2369            );
2370            let result = parse_cypher_query(&query);
2371            assert!(result.is_ok(), "Failed to parse metric: {}", metric);
2372
2373            let ast = result.unwrap();
2374            let return_item = &ast.return_clause.items[0];
2375
2376            match &return_item.expression {
2377                ValueExpression::VectorDistance {
2378                    metric: parsed_metric,
2379                    ..
2380                } => {
2381                    let expected = match *metric {
2382                        "cosine" => DistanceMetric::Cosine,
2383                        "l2" => DistanceMetric::L2,
2384                        "dot" => DistanceMetric::Dot,
2385                        _ => panic!("Unexpected metric"),
2386                    };
2387                    assert_eq!(*parsed_metric, expected);
2388                }
2389                _ => panic!("Expected VectorDistance"),
2390            }
2391        }
2392    }
2393
2394    #[test]
2395    fn test_vector_search_in_order_by() {
2396        let query = "MATCH (p:Person) RETURN p.name ORDER BY vector_distance(p.embedding, $query_vec, cosine) ASC LIMIT 10";
2397        let result = parse_cypher_query(query);
2398        assert!(result.is_ok(), "vector_distance in ORDER BY should parse");
2399
2400        let ast = result.unwrap();
2401        let order_by = ast.order_by.expect("Expected ORDER BY clause");
2402
2403        assert_eq!(order_by.items.len(), 1);
2404        match &order_by.items[0].expression {
2405            ValueExpression::VectorDistance { .. } => {
2406                // Success
2407            }
2408            _ => panic!("Expected VectorDistance in ORDER BY"),
2409        }
2410    }
2411
2412    #[test]
2413    fn test_hybrid_query_with_vector_and_property_filters() {
2414        let query = "MATCH (p:Person) WHERE p.age > 25 AND vector_similarity(p.embedding, $query_vec, cosine) > 0.7 RETURN p.name";
2415        let result = parse_cypher_query(query);
2416        assert!(result.is_ok(), "Hybrid query should parse");
2417
2418        let ast = result.unwrap();
2419        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2420
2421        // Should be an AND expression
2422        match where_clause.expression {
2423            BooleanExpression::And(left, right) => {
2424                // Left should be age > 25
2425                match *left {
2426                    BooleanExpression::Comparison { .. } => {}
2427                    _ => panic!("Expected comparison on left"),
2428                }
2429                // Right should be vector_similarity > 0.7
2430                match *right {
2431                    BooleanExpression::Comparison { left, .. } => match left {
2432                        ValueExpression::VectorSimilarity { .. } => {}
2433                        _ => panic!("Expected VectorSimilarity"),
2434                    },
2435                    _ => panic!("Expected comparison on right"),
2436                }
2437            }
2438            _ => panic!("Expected AND expression"),
2439        }
2440    }
2441
2442    #[test]
2443    fn unions_preserve_modes_and_quoted_keywords() {
2444        let query = parse_cypher_query(
2445            "RETURN 'UNION' AS n UNION ALL RETURN 'ALL' AS n UNION RETURN 'UNION ALL' AS n",
2446        )
2447        .unwrap();
2448        assert_eq!(query.union_branches.len(), 2);
2449        assert!(query.union_branches[0].all);
2450        assert!(!query.union_branches[1].all);
2451        assert!(parse_cypher_query("RETURN 1 AS n UNION ALL").is_err());
2452    }
2453
2454    #[test]
2455    fn pagination_accepts_parameters_without_losing_names() {
2456        let query = parse_cypher_query("RETURN 1 AS n SKIP $offset LIMIT $count").unwrap();
2457        assert_eq!(query.skip, Some(RowCount::Parameter("offset".to_string())));
2458        assert_eq!(query.limit, Some(RowCount::Parameter("count".to_string())));
2459        assert!(parse_cypher_query("RETURN 1 LIMIT -1").is_err());
2460        assert!(parse_cypher_query("RETURN 1 LIMIT 1.5").is_err());
2461    }
2462
2463    #[test]
2464    fn test_integer_lists_preserve_precision() {
2465        for literal in ["[1, 2, 3]", "[-19791209300775, 16777217]", "[]"] {
2466            assert!(parse_vector_literal(literal).is_err());
2467            let (remaining, expression) = value_expression(literal).unwrap();
2468            assert!(remaining.is_empty());
2469            let ValueExpression::ScalarFunction { name, args } = expression else {
2470                panic!("expected an ordinary list");
2471            };
2472            assert_eq!(name, "__list");
2473            assert!(args
2474                .iter()
2475                .all(|v| matches!(v, ValueExpression::Literal(PropertyValue::Integer(_)))));
2476        }
2477        let (_, expression) = value_expression("[19791209300775]").unwrap();
2478        assert_eq!(
2479            expression,
2480            ValueExpression::ScalarFunction {
2481                name: "__list".to_string(),
2482                args: vec![ValueExpression::Literal(PropertyValue::Integer(
2483                    19791209300775
2484                ))],
2485            }
2486        );
2487        for literal in ["[1, 2.0]", "[1e-3, 2]", "[-1.0, 2]"] {
2488            assert!(matches!(
2489                value_expression(literal).unwrap().1,
2490                ValueExpression::VectorLiteral(_)
2491            ));
2492        }
2493    }
2494
2495    #[test]
2496    fn test_parse_vector_literal() {
2497        let result = parse_vector_literal("[0.1, 0.2, 0.3]");
2498        assert!(result.is_ok());
2499        let (_, expr) = result.unwrap();
2500        match expr {
2501            ValueExpression::VectorLiteral(vec) => {
2502                assert_eq!(vec.len(), 3);
2503                assert_eq!(vec[0], 0.1);
2504                assert_eq!(vec[1], 0.2);
2505                assert_eq!(vec[2], 0.3);
2506            }
2507            _ => panic!("Expected VectorLiteral"),
2508        }
2509    }
2510
2511    #[test]
2512    fn test_parse_vector_literal_with_negative_values() {
2513        let result = parse_vector_literal("[-0.1, 0.2, -0.3]");
2514        assert!(result.is_ok());
2515        let (_, expr) = result.unwrap();
2516        match expr {
2517            ValueExpression::VectorLiteral(vec) => {
2518                assert_eq!(vec.len(), 3);
2519                assert_eq!(vec[0], -0.1);
2520                assert_eq!(vec[2], -0.3);
2521            }
2522            _ => panic!("Expected VectorLiteral"),
2523        }
2524    }
2525
2526    #[test]
2527    fn test_parse_vector_literal_scientific_notation() {
2528        let result = parse_vector_literal("[1e-3, 2.5e2, -3e-1]");
2529        assert!(result.is_ok());
2530        let (_, expr) = result.unwrap();
2531        match expr {
2532            ValueExpression::VectorLiteral(vec) => {
2533                assert_eq!(vec.len(), 3);
2534                assert!((vec[0] - 0.001).abs() < 1e-6);
2535                assert!((vec[1] - 250.0).abs() < 1e-6);
2536                assert!((vec[2] - (-0.3)).abs() < 1e-6);
2537            }
2538            _ => panic!("Expected VectorLiteral"),
2539        }
2540    }
2541
2542    #[test]
2543    fn test_vector_distance_with_literal() {
2544        let query =
2545            "MATCH (p:Person) WHERE vector_distance(p.embedding, [0.1, 0.2], l2) < 0.5 RETURN p";
2546        let result = parse_cypher_query(query);
2547        assert!(result.is_ok());
2548
2549        let ast = result.unwrap();
2550        let where_clause = ast.where_clause.expect("Expected WHERE clause");
2551
2552        match where_clause.expression {
2553            BooleanExpression::Comparison { left, operator, .. } => {
2554                match left {
2555                    ValueExpression::VectorDistance {
2556                        left,
2557                        right,
2558                        metric,
2559                    } => {
2560                        // Left should be property reference
2561                        assert!(matches!(*left, ValueExpression::Property(_)));
2562                        // Right should be vector literal
2563                        match *right {
2564                            ValueExpression::VectorLiteral(vec) => {
2565                                assert_eq!(vec.len(), 2);
2566                                assert_eq!(vec[0], 0.1);
2567                                assert_eq!(vec[1], 0.2);
2568                            }
2569                            _ => panic!("Expected VectorLiteral"),
2570                        }
2571                        assert_eq!(metric, DistanceMetric::L2);
2572                    }
2573                    _ => panic!("Expected VectorDistance"),
2574                }
2575                assert_eq!(operator, ComparisonOperator::LessThan);
2576            }
2577            _ => panic!("Expected comparison"),
2578        }
2579    }
2580
2581    // UNWIND parser tests
2582    #[test]
2583    fn test_parse_unwind_simple() {
2584        let query = "UNWIND [1, 2, 3] AS num RETURN num";
2585        let ast = parse_cypher_query(query);
2586        assert!(ast.is_ok(), "Failed to parse simple UNWIND query");
2587    }
2588
2589    #[test]
2590    fn test_parse_unwind_after_match() {
2591        let query = "MATCH (n) UNWIND n.list AS item RETURN item";
2592        let ast = parse_cypher_query(query);
2593        assert!(ast.is_ok(), "Failed to parse UNWIND after MATCH");
2594    }
2595
2596    #[test]
2597    fn out_of_range_numbers_are_parse_errors_not_panics() {
2598        for q in [
2599            "MATCH (n:Person) RETURN n LIMIT 99999999999999999999",
2600            "MATCH (n:Person) RETURN n LIMIT -1",
2601            "MATCH (n:Person) RETURN n SKIP -1",
2602            "MATCH (n:Person) WHERE n.age > 99999999999999999999 RETURN n",
2603        ] {
2604            assert!(parse_cypher_query(q).is_err(), "{q}");
2605        }
2606        let huge = format!(
2607            "MATCH (n:Person) WHERE n.x > {}.0 RETURN n",
2608            "9".repeat(400)
2609        );
2610        assert!(parse_cypher_query(&huge).is_err());
2611    }
2612
2613    #[test]
2614    fn relationship_type_alternation() {
2615        for q in [
2616            "MATCH (a)-[:A|B]->(b) RETURN b",
2617            "MATCH (a)-[:A|:B]->(b) RETURN b",
2618            "MATCH (a)-[r:A | B *1..2]->(b) RETURN b",
2619            "MATCH (a)-[:A:B]->(b) RETURN b",
2620        ] {
2621            let query = parse_cypher_query(q).unwrap_or_else(|e| panic!("{q}: {e}"));
2622            let ReadingClause::Match(m) = &query.reading_clauses[0] else {
2623                panic!("{q}: not a MATCH");
2624            };
2625            let GraphPattern::Path(path) = &m.patterns[0] else {
2626                panic!("{q}: not a path");
2627            };
2628            assert_eq!(path.segments[0].relationship.types, vec!["A", "B"], "{q}");
2629        }
2630    }
2631
2632    #[test]
2633    fn chained_with_stages_and_with_distinct() {
2634        let q = "MATCH (a)-[:R]->(b) WITH DISTINCT b WHERE b.x > 1 \
2635                 MATCH (b)-[:R]->(c) WITH c, count(*) AS n WITH c WHERE n > 2 RETURN c";
2636        let query = parse_cypher_query(q).unwrap();
2637        let first = query.with_clause.as_ref().unwrap();
2638        assert!(first.distinct && first.where_clause.is_some());
2639        assert_eq!(query.post_with_reading_clauses.len(), 1);
2640        assert_eq!(query.further_with_stages.len(), 2);
2641        let [second, third] = &query.further_with_stages[..] else {
2642            unreachable!()
2643        };
2644        assert!(!second.with_clause.distinct && second.reading_clauses.is_empty());
2645        assert!(third.with_clause.where_clause.is_some());
2646    }
2647
2648    #[test]
2649    fn optional_match_owns_its_where_and_inner_where_attaches_to_match() {
2650        let q = "MATCH (a) WHERE a.x > 1 OPTIONAL MATCH (a)-[:R]->(b) WHERE b.y = 2 \
2651                 MATCH (a)-[:S]->(c) WHERE c.z = 3 RETURN a";
2652        let query = parse_cypher_query(q).unwrap();
2653        let clauses: Vec<&MatchClause> = query
2654            .reading_clauses
2655            .iter()
2656            .map(|c| match c {
2657                ReadingClause::Match(m) => m,
2658                ReadingClause::Unwind(_) | ReadingClause::Call(_) => unreachable!(),
2659            })
2660            .collect();
2661        assert_eq!(clauses.len(), 3);
2662        assert!(!clauses[0].optional && clauses[0].where_clause.is_some());
2663        assert!(clauses[1].optional && clauses[1].where_clause.is_some());
2664        // The last plain MATCH's WHERE stays in the query's slot (re-rooting reads it).
2665        assert!(clauses[2].where_clause.is_none() && query.where_clause.is_some());
2666    }
2667
2668    #[test]
2669    fn null_literal_any_case_but_not_a_name_prefix() {
2670        for q in [
2671            "MATCH (n) RETURN NULL AS x",
2672            "MATCH (n) RETURN null AS x",
2673            "MATCH (n) RETURN Null AS x",
2674        ] {
2675            let query = parse_cypher_query(q).unwrap();
2676            assert_eq!(
2677                query.return_clause.items[0].expression,
2678                ValueExpression::Literal(PropertyValue::Null),
2679                "{q}"
2680            );
2681        }
2682        let query = parse_cypher_query("MATCH (n) WITH 1 AS nullCount RETURN nullCount").unwrap();
2683        assert_eq!(
2684            query.return_clause.items[0].expression,
2685            ValueExpression::Variable("nullCount".into())
2686        );
2687    }
2688    #[test]
2689    fn reduce_keeps_inputs_in_outer_scope_and_marks_local_properties() {
2690        let q =
2691            parse_cypher_query("WITH {n:1} AS x RETURN reduce(s=x.n, x IN [x] | s+x.n)").unwrap();
2692        let ValueExpression::Reduce {
2693            initial,
2694            list,
2695            expression,
2696            ..
2697        } = &q.return_clause.items[0].expression
2698        else {
2699            panic!("expected reduce");
2700        };
2701        assert!(matches!(initial.as_ref(), ValueExpression::Property(p) if p.variable == "x"));
2702        let mut input_locals = 0;
2703        transform_value(&mut list.as_ref().clone(), &mut |v| {
2704            input_locals += usize::from(matches!(v, ValueExpression::LocalVariable(_)))
2705        });
2706        assert_eq!(input_locals, 0);
2707        let mut body_locals = Vec::new();
2708        transform_value(&mut expression.as_ref().clone(), &mut |v| {
2709            if let ValueExpression::LocalVariable(n) = v {
2710                body_locals.push(n.clone());
2711            }
2712        });
2713        assert_eq!(body_locals, ["s", "x"]);
2714    }
2715}