Skip to main content

marsdb_query/
antlr_visitor.rs

1// `AstBuilder`/`AstNode` internals stay unused-by-external-code from
2// rustc's perspective even though `parse_antlr` (this file's real public
3// entry point, re-exported as `lib.rs`'s `parse`/`parse_many`) exercises
4// them at runtime -- the visitor trait's `visit_X` overrides are only
5// ever called through dynamic dispatch (`accept()`), which rustc's
6// dead-code analysis can't see through.
7#![allow(dead_code)]
8
9//! ANTLR-based AST builder -- replaced the old pest-tree-walk
10//! (`parser.rs`/`cypher.pest`, deleted at cutover) as this
11//! crate's real Cypher parser. `parse_antlr`/`parse_antlr_many` are
12//! re-exported by `lib.rs` as `parse`/`parse_many`.
13//!
14//! Implements the generated `CypherParserVisitorCompat` trait rather than
15//! manually walking context accessors: ANTLR's own `accept()`/`visit()`
16//! double-dispatch already routes to the right `visit_X` method for
17//! whichever grammar alternative is actually present, so alternation
18//! (`literal : boolLit | numLit | NULL_W | stringLit | charLit | listLit
19//! | mapLit`) doesn't need a hand-written `if let Some(x) = ctx.boolLit()
20//! ... else if ...` chain -- only `visit_literal` needs a one-line manual
21//! check, for the bare `NULL_W` terminal alternative specifically (a
22//! terminal has no grammar-rule `visit_X` hook of its own to override).
23//!
24//! `Return` (via [`AstNode`]) is one shared enum across the whole
25//! visitor -- required by `ParseTreeVisitorCompat`, which supports
26//! exactly one `Return` type for the entire tree walk, not a per-rule
27//! type. Grows a variant per AST node kind as later increments need it.
28
29use crate::ast::{
30    is_aggregate_name, ArithOp, CallClause, CallYield, CompareOp, Expr, Literal, MergeClause,
31    NodePattern, Pattern, PropAccess, QuantifierKind, QueryClause, QueryPart, RelDirection,
32    RelPattern, RemoveItem, ReturnExpr, ReturnItem, ReturnTail, SetItem, SortDir, Statement, Tail,
33    UnwindClause, UnwindSource, WithClause, WithExpr,
34};
35use crate::error::QueryError;
36use crate::generated::cypherparser::{
37    AddSubExpressionContext, AndExpressionContext, AndExpressionContextAttrs, AtomContext,
38    AtomContextAttrs, AtomicExpressionContext, AtomicExpressionContextAll,
39    AtomicExpressionContextAttrs, BoolLitContext, BoolLitContextAttrs, CaseExpressionContext,
40    CharLitContext, CharLitContextAttrs, ComparisonExpressionContext,
41    ComparisonExpressionContextAttrs, ComparisonSignsContextAll, ComparisonSignsContextAttrs,
42    CountAllContext, CreateIndexStContext, CreateIndexStContextAttrs, CreateStContext,
43    CreateStContextAttrs, DeleteStContext, DeleteStContextAttrs, ExplainStContext,
44    ExplainStContextAttrs, ExpressionChainContextAttrs, ExpressionContext, ExpressionContextAttrs,
45    FilterExpressionContext, FilterExpressionContextAttrs, FilterWithContext,
46    FilterWithContextAttrs, FunctionInvocationContext, FunctionInvocationContextAttrs,
47    InExpressionContextAttrs, InvocationNameContextAll, InvocationNameContextAttrs,
48    LhsContextAttrs, LimitStContextAttrs, ListComprehensionContext, ListComprehensionContextAttrs,
49    ListExpressionContextAll, ListExpressionContextAttrs, ListLitContext, ListLitContextAttrs,
50    LiteralContext, LiteralContextAttrs, MapLitContext, MapLitContextAttrs, MapPairContextAttrs,
51    MatchStContext, MatchStContextAttrs, MergeActionContextAll, MergeActionContextAttrs,
52    MergeStContext, MergeStContextAttrs, MultDivExpressionContext, MultiPartQContext,
53    MultiPartQContextAttrs, NameContextAll, NameContextAttrs, NodeLabelsContextAttrs,
54    NodePatternContext, NodePatternContextAttrs, NotExpressionContext, NotExpressionContextAttrs,
55    NullExpressionContextAttrs, NumLitContext, NumLitContextAll, NumLitContextAttrs,
56    OrderItemContextAttrs, OrderStContext, OrderStContextAttrs, ParameterContext,
57    ParameterContextAttrs, ParenExpressionChainContextAll, ParenExpressionChainContextAttrs,
58    ParenthesizedExpressionContext, ParenthesizedExpressionContextAttrs,
59    PatternComprehensionContext, PatternComprehensionContextAttrs, PatternContextAttrs,
60    PatternElemChainContextAttrs, PatternElemContext, PatternElemContextAttrs,
61    PatternPartContextAttrs, PatternWhereContextAttrs, PowerExpressionContext,
62    PowerExpressionContextAttrs, ProjectionBodyContext, ProjectionBodyContextAttrs,
63    ProjectionItemContextAttrs, ProjectionItemsContextAttrs, PropertiesContextAll,
64    PropertiesContextAttrs, PropertyExpressionContext, PropertyExpressionContextAttrs,
65    PropertyOrLabelExpressionContext, PropertyOrLabelExpressionContextAttrs, QueryCallStContextAll,
66    QueryCallStContextAttrs, ReadingStatementContextAll, ReadingStatementContextAttrs,
67    RegularQueryContext, RegularQueryContextAttrs, RelationDetailContext,
68    RelationDetailContextAttrs, RelationshipPatternContext, RelationshipPatternContextAttrs,
69    RelationshipTypesContextAttrs, RelationshipsChainPatternContext,
70    RelationshipsChainPatternContextAttrs, RemoveItemContextAll, RemoveItemContextAttrs,
71    RemoveStContext, RemoveStContextAttrs, ReturnStContext, ReturnStContextAttrs,
72    SetItemContextAll, SetItemContextAttrs, SetStContext, SetStContextAttrs,
73    ShortestPathWrapperContextAttrs, SinglePartQContext, SinglePartQContextAttrs,
74    SkipStContextAttrs, StandaloneCallContext, StandaloneCallContextAttrs,
75    StringExpPrefixContextAll, StringExpPrefixContextAttrs, StringExpressionContextAll,
76    StringExpressionContextAttrs, StringListNullExpressionContext,
77    StringListNullExpressionContextAttrs, StringLitContext, StringLitContextAttrs,
78    SubqueryExistContext, SubqueryExistContextAttrs, SymbolContextAll, SymbolContextAttrs,
79    UnaryAddSubExpressionContext, UnaryAddSubExpressionContextAttrs, UnionStContextAttrs,
80    UnwindStContext, UnwindStContextAttrs, UpdatingStatementContextAll,
81    UpdatingStatementContextAttrs, WhereContextAttrs, WithStContext, WithStContextAttrs,
82    XorExpressionContext, XorExpressionContextAttrs, YieldItemContextAttrs, YieldItemsContextAll,
83    YieldItemsContextAttrs,
84};
85use crate::generated::cypherparservisitor::CypherParserVisitorCompat;
86use crate::parse_helpers::{
87    group_into_linear_patterns, parse_int_literal, parse_rel_range, unescape_string,
88    validate_named_path_pattern, validate_shortest_path_pattern,
89};
90use antlr4rust::parser_rule_context::ParserRuleContext;
91use antlr4rust::token::Token;
92use antlr4rust::tree::{ParseTree, ParseTreeVisitorCompat, Tree};
93use std::rc::Rc;
94
95#[derive(Debug, Default)]
96pub(crate) enum AstNode {
97    #[default]
98    None,
99    Literal(Literal),
100    NodePattern(NodePattern),
101    RelPattern(RelPattern),
102    Pattern(Pattern),
103    QueryParts(Vec<QueryPart>),
104    ReturnExpr(ReturnExpr),
105    ReturnClause(ParsedReturnClause),
106    WithClause(WithClause),
107    UnwindClause(UnwindClause),
108    SetItems(Vec<SetItem>),
109    DeleteItems(ParsedDelete),
110    RemoveItems(Vec<RemoveItem>),
111    CreatePatterns(Vec<Pattern>),
112    MergeClause(MergeClause),
113    Statement(Statement),
114    Err(QueryError),
115}
116
117#[derive(Debug)]
118pub(crate) struct ParsedDelete {
119    pub items: Vec<ReturnExpr>,
120    pub detach: bool,
121}
122
123/// `returnSt`'s/`withSt`'s shared `projectionBody` bundles the item list,
124/// `DISTINCT`, `ORDER BY`, `SKIP`, and `LIMIT` together, but `Tail`
125/// (items + distinct) and `order_by`/`skip`/`limit` live at different
126/// levels of `ast::Statement::Match` (the latter three are statement-wide,
127/// not per-`Tail`) -- this carries all four out of the visitor together
128/// so the caller building `Statement::Match` can split them apart.
129#[derive(Debug)]
130pub(crate) struct ParsedReturnClause {
131    pub tail: Tail,
132    pub order_by: Option<Vec<(ReturnExpr, SortDir)>>,
133    pub skip: Option<ReturnExpr>,
134    pub limit: Option<ReturnExpr>,
135}
136
137macro_rules! ast_node_into {
138    ($name:ident, $variant:ident, $ty:ty) => {
139        fn $name(self) -> Result<$ty, QueryError> {
140            match self {
141                AstNode::$variant(v) => Ok(v),
142                AstNode::Err(e) => Err(e),
143                other => unreachable!("expected AstNode::{}, got {other:?}", stringify!($variant)),
144            }
145        }
146    };
147}
148
149impl AstNode {
150    ast_node_into!(into_literal, Literal, Literal);
151    ast_node_into!(into_node_pattern, NodePattern, NodePattern);
152    ast_node_into!(into_rel_pattern, RelPattern, RelPattern);
153    ast_node_into!(into_pattern, Pattern, Pattern);
154    ast_node_into!(into_query_parts, QueryParts, Vec<QueryPart>);
155    ast_node_into!(into_return_expr, ReturnExpr, ReturnExpr);
156
157    /// `ast::Literal` has no List/Map variant -- `listLit`/`mapLit` build
158    /// `ReturnExpr::ListLit`/`MapLit` directly instead (see
159    /// `visit_listLit`/`visit_mapLit`), so a `literal` context (reached
160    /// via `atom`, which can't tell in advance which of its 7 alternatives
161    /// it'll get) may resolve to either an `AstNode::Literal` (bool/num/
162    /// string/char/null) or an `AstNode::ReturnExpr` (list/map). This
163    /// accepts either, wrapping a bare `Literal` in `ReturnExpr::Lit`.
164    fn into_return_expr_lenient(self) -> Result<ReturnExpr, QueryError> {
165        match self {
166            AstNode::Literal(l) => Ok(ReturnExpr::Lit(l)),
167            AstNode::ReturnExpr(e) => Ok(e),
168            AstNode::Err(e) => Err(e),
169            other => {
170                unreachable!("expected AstNode::Literal or AstNode::ReturnExpr, got {other:?}")
171            }
172        }
173    }
174    ast_node_into!(into_return_clause, ReturnClause, ParsedReturnClause);
175    ast_node_into!(into_with_clause, WithClause, WithClause);
176    ast_node_into!(into_unwind_clause, UnwindClause, UnwindClause);
177    ast_node_into!(into_set_items, SetItems, Vec<SetItem>);
178    ast_node_into!(into_delete_items, DeleteItems, ParsedDelete);
179    ast_node_into!(into_remove_items, RemoveItems, Vec<RemoveItem>);
180    ast_node_into!(into_create_patterns, CreatePatterns, Vec<Pattern>);
181    ast_node_into!(into_merge_clause, MergeClause, MergeClause);
182    ast_node_into!(into_statement, Statement, Statement);
183}
184
185pub(crate) struct AstBuilder {
186    result: AstNode,
187}
188
189impl AstBuilder {
190    pub(crate) fn new() -> Self {
191        AstBuilder {
192            result: AstNode::default(),
193        }
194    }
195}
196
197impl<'input> ParseTreeVisitorCompat<'input> for AstBuilder {
198    type Node = crate::generated::cypherparser::CypherParserContextType;
199    type Return = AstNode;
200
201    fn temp_result(&mut self) -> &mut Self::Return {
202        &mut self.result
203    }
204}
205
206impl<'input> CypherParserVisitorCompat<'input> for AstBuilder {
207    fn visit_literal(&mut self, ctx: &LiteralContext<'input>) -> Self::Return {
208        // The only alternative that's a bare terminal, not a sub-rule --
209        // no `visit_X` grammar-rule hook exists to override for it, so it
210        // needs the one manual check `visit_children`'s default dispatch
211        // can't cover on its own.
212        if ctx.NULL_W().is_some() {
213            return AstNode::Literal(Literal::Null);
214        }
215        self.visit_children(ctx)
216    }
217
218    fn visit_boolLit(&mut self, ctx: &BoolLitContext<'input>) -> Self::Return {
219        AstNode::Literal(Literal::Bool(ctx.TRUE().is_some()))
220    }
221
222    /// The lexer's `DIGIT` token covers hex/octal/decimal integers *and*
223    /// floats in one token (`DIGIT : HexDigits | OctalDigits | Digits |
224    /// FLOAT;`), unlike pest's grammar which split `int_literal`/
225    /// `float_literal` into separate rules -- so int-vs-float is decided
226    /// from the raw text here instead of from which sub-rule matched.
227    /// Hex/octal integers can't contain `.`/exponent/`f`/`d` at all (the
228    /// lexer wouldn't have matched `DIGIT` as those alternatives if they
229    /// did), so checking for those unconditionally is safe and doesn't
230    /// misfire on e.g. `0xE` (a hex digit `E`, not a float exponent).
231    fn visit_numLit(&mut self, ctx: &NumLitContext<'input>) -> Self::Return {
232        let text = ctx
233            .DIGIT()
234            .expect("numLit context always has a DIGIT token")
235            .get_text();
236        match parse_num_lit_text(&text) {
237            Ok(lit) => AstNode::Literal(lit),
238            Err(e) => AstNode::Err(e),
239        }
240    }
241
242    fn visit_stringLit(&mut self, ctx: &StringLitContext<'input>) -> Self::Return {
243        let text = ctx
244            .STRING_LITERAL()
245            .expect("stringLit context always has a STRING_LITERAL token")
246            .get_text();
247        match unescape_string(&text[1..text.len() - 1]) {
248            Ok(s) => AstNode::Literal(Literal::String(s)),
249            Err(e) => AstNode::Err(e),
250        }
251    }
252
253    fn visit_charLit(&mut self, ctx: &CharLitContext<'input>) -> Self::Return {
254        let text = ctx
255            .CHAR_LITERAL()
256            .expect("charLit context always has a CHAR_LITERAL token")
257            .get_text();
258        match unescape_string(&text[1..text.len() - 1]) {
259            Ok(s) => AstNode::Literal(Literal::String(s)),
260            Err(e) => AstNode::Err(e),
261        }
262    }
263
264    fn visit_nodePattern(&mut self, ctx: &NodePatternContext<'input>) -> Self::Return {
265        match self.build_node_pattern(ctx) {
266            Ok(n) => AstNode::NodePattern(n),
267            Err(e) => AstNode::Err(e),
268        }
269    }
270
271    fn visit_relationDetail(&mut self, ctx: &RelationDetailContext<'input>) -> Self::Return {
272        match self.build_rel_detail(ctx) {
273            Ok(r) => AstNode::RelPattern(r),
274            Err(e) => AstNode::Err(e),
275        }
276    }
277
278    fn visit_relationshipPattern(
279        &mut self,
280        ctx: &RelationshipPatternContext<'input>,
281    ) -> Self::Return {
282        match self.build_relationship_pattern(ctx) {
283            Ok(r) => AstNode::RelPattern(r),
284            Err(e) => AstNode::Err(e),
285        }
286    }
287
288    fn visit_patternElem(&mut self, ctx: &PatternElemContext<'input>) -> Self::Return {
289        match self.build_pattern_elem(ctx) {
290            Ok(p) => AstNode::Pattern(p),
291            Err(e) => AstNode::Err(e),
292        }
293    }
294
295    fn visit_matchSt(&mut self, ctx: &MatchStContext<'input>) -> Self::Return {
296        match self.build_match_st(ctx) {
297            Ok(parts) => AstNode::QueryParts(parts),
298            Err(e) => AstNode::Err(e),
299        }
300    }
301
302    fn visit_expression(&mut self, ctx: &ExpressionContext<'input>) -> Self::Return {
303        let mut operands = ctx.xorExpression_all().into_iter();
304        let mut lhs = match self
305            .visit(
306                &*operands
307                    .next()
308                    .expect("expression has at least one xorExpression"),
309            )
310            .into_return_expr()
311        {
312            Ok(e) => e,
313            Err(e) => return AstNode::Err(e),
314        };
315        for rhs_ctx in operands {
316            let rhs = match self.visit(&*rhs_ctx).into_return_expr() {
317                Ok(e) => e,
318                Err(e) => return AstNode::Err(e),
319            };
320            lhs = ReturnExpr::Or(Box::new(lhs), Box::new(rhs));
321        }
322        AstNode::ReturnExpr(lhs)
323    }
324
325    fn visit_xorExpression(&mut self, ctx: &XorExpressionContext<'input>) -> Self::Return {
326        let mut operands = ctx.andExpression_all().into_iter();
327        let mut lhs = match self
328            .visit(
329                &*operands
330                    .next()
331                    .expect("xorExpression has at least one andExpression"),
332            )
333            .into_return_expr()
334        {
335            Ok(e) => e,
336            Err(e) => return AstNode::Err(e),
337        };
338        for rhs_ctx in operands {
339            let rhs = match self.visit(&*rhs_ctx).into_return_expr() {
340                Ok(e) => e,
341                Err(e) => return AstNode::Err(e),
342            };
343            lhs = ReturnExpr::Xor(Box::new(lhs), Box::new(rhs));
344        }
345        AstNode::ReturnExpr(lhs)
346    }
347
348    fn visit_andExpression(&mut self, ctx: &AndExpressionContext<'input>) -> Self::Return {
349        let mut operands = ctx.notExpression_all().into_iter();
350        let mut lhs = match self
351            .visit(
352                &*operands
353                    .next()
354                    .expect("andExpression has at least one notExpression"),
355            )
356            .into_return_expr()
357        {
358            Ok(e) => e,
359            Err(e) => return AstNode::Err(e),
360        };
361        for rhs_ctx in operands {
362            let rhs = match self.visit(&*rhs_ctx).into_return_expr() {
363                Ok(e) => e,
364                Err(e) => return AstNode::Err(e),
365            };
366            lhs = ReturnExpr::And(Box::new(lhs), Box::new(rhs));
367        }
368        AstNode::ReturnExpr(lhs)
369    }
370
371    fn visit_notExpression(&mut self, ctx: &NotExpressionContext<'input>) -> Self::Return {
372        let inner = ctx
373            .comparisonExpression()
374            .expect("notExpression always has a comparisonExpression");
375        match self.visit(&*inner).into_return_expr() {
376            Ok(mut expr) => {
377                for _ in ctx.NOT_all() {
378                    expr = ReturnExpr::Not(Box::new(expr));
379                }
380                AstNode::ReturnExpr(expr)
381            }
382            Err(e) => AstNode::Err(e),
383        }
384    }
385
386    fn visit_comparisonExpression(
387        &mut self,
388        ctx: &ComparisonExpressionContext<'input>,
389    ) -> Self::Return {
390        match self.build_comparison_expression(ctx) {
391            Ok(expr) => AstNode::ReturnExpr(expr),
392            Err(e) => AstNode::Err(e),
393        }
394    }
395
396    fn visit_stringListNullExpression(
397        &mut self,
398        ctx: &StringListNullExpressionContext<'input>,
399    ) -> Self::Return {
400        match self.build_string_list_null_expression(ctx) {
401            Ok(expr) => AstNode::ReturnExpr(expr),
402            Err(e) => AstNode::Err(e),
403        }
404    }
405
406    fn visit_addSubExpression(&mut self, ctx: &AddSubExpressionContext<'input>) -> Self::Return {
407        match self.build_add_sub_expression(ctx) {
408            Ok(expr) => AstNode::ReturnExpr(expr),
409            Err(e) => AstNode::Err(e),
410        }
411    }
412
413    fn visit_multDivExpression(&mut self, ctx: &MultDivExpressionContext<'input>) -> Self::Return {
414        match self.build_mult_div_expression(ctx) {
415            Ok(expr) => AstNode::ReturnExpr(expr),
416            Err(e) => AstNode::Err(e),
417        }
418    }
419
420    /// Left-associative (`4 ^ 3 ^ 2` is `(4 ^ 3) ^ 2`), same as every
421    /// other binary chain here -- matches `parser.rs`'s `parse_pow_expr`,
422    /// confirmed against the real TCK fixture (see that function's docs).
423    fn visit_powerExpression(&mut self, ctx: &PowerExpressionContext<'input>) -> Self::Return {
424        let mut operands = ctx.unaryAddSubExpression_all().into_iter();
425        let mut lhs = match self
426            .visit(
427                &*operands
428                    .next()
429                    .expect("powerExpression has at least one unaryAddSubExpression"),
430            )
431            .into_return_expr()
432        {
433            Ok(e) => e,
434            Err(e) => return AstNode::Err(e),
435        };
436        for rhs_ctx in operands {
437            let rhs = match self.visit(&*rhs_ctx).into_return_expr() {
438                Ok(e) => e,
439                Err(e) => return AstNode::Err(e),
440            };
441            lhs = ReturnExpr::Arith(Box::new(lhs), ArithOp::Pow, Box::new(rhs));
442        }
443        AstNode::ReturnExpr(lhs)
444    }
445
446    fn visit_unaryAddSubExpression(
447        &mut self,
448        ctx: &UnaryAddSubExpressionContext<'input>,
449    ) -> Self::Return {
450        match self.build_unary_add_sub_expression(ctx) {
451            Ok(expr) => AstNode::ReturnExpr(expr),
452            Err(e) => AstNode::Err(e),
453        }
454    }
455
456    fn visit_atomicExpression(&mut self, ctx: &AtomicExpressionContext<'input>) -> Self::Return {
457        match self.build_atomic_expression(ctx) {
458            Ok(expr) => AstNode::ReturnExpr(expr),
459            Err(e) => AstNode::Err(e),
460        }
461    }
462
463    fn visit_propertyOrLabelExpression(
464        &mut self,
465        ctx: &PropertyOrLabelExpressionContext<'input>,
466    ) -> Self::Return {
467        match self.build_property_or_label_expression(ctx) {
468            Ok(expr) => AstNode::ReturnExpr(expr),
469            Err(e) => AstNode::Err(e),
470        }
471    }
472
473    fn visit_propertyExpression(
474        &mut self,
475        ctx: &PropertyExpressionContext<'input>,
476    ) -> Self::Return {
477        match self.build_property_expression(ctx) {
478            Ok(expr) => AstNode::ReturnExpr(expr),
479            Err(e) => AstNode::Err(e),
480        }
481    }
482
483    fn visit_atom(&mut self, ctx: &AtomContext<'input>) -> Self::Return {
484        match self.build_atom(ctx) {
485            Ok(expr) => AstNode::ReturnExpr(expr),
486            Err(e) => AstNode::Err(e),
487        }
488    }
489
490    fn visit_parenthesizedExpression(
491        &mut self,
492        ctx: &ParenthesizedExpressionContext<'input>,
493    ) -> Self::Return {
494        let inner = ctx
495            .expression()
496            .expect("parenthesizedExpression always has an expression");
497        self.visit(&*inner)
498    }
499
500    fn visit_functionInvocation(
501        &mut self,
502        ctx: &FunctionInvocationContext<'input>,
503    ) -> Self::Return {
504        match self.build_function_invocation(ctx) {
505            Ok(expr) => AstNode::ReturnExpr(expr),
506            Err(e) => AstNode::Err(e),
507        }
508    }
509
510    fn visit_parameter(&mut self, ctx: &ParameterContext<'input>) -> Self::Return {
511        match self.build_parameter(ctx) {
512            Ok(expr) => AstNode::ReturnExpr(expr),
513            Err(e) => AstNode::Err(e),
514        }
515    }
516
517    fn visit_countAll(&mut self, _ctx: &CountAllContext<'input>) -> Self::Return {
518        AstNode::ReturnExpr(ReturnExpr::CountStar)
519    }
520
521    fn visit_returnSt(&mut self, ctx: &ReturnStContext<'input>) -> Self::Return {
522        let body_ctx = ctx
523            .projectionBody()
524            .expect("returnSt always has a projectionBody");
525        match self.build_projection_body(&body_ctx) {
526            Ok(c) => AstNode::ReturnClause(c),
527            Err(e) => AstNode::Err(e),
528        }
529    }
530
531    fn visit_withSt(&mut self, ctx: &WithStContext<'input>) -> Self::Return {
532        match self.build_with_clause(ctx) {
533            Ok(c) => AstNode::WithClause(c),
534            Err(e) => AstNode::Err(e),
535        }
536    }
537
538    fn visit_unwindSt(&mut self, ctx: &UnwindStContext<'input>) -> Self::Return {
539        match self.build_unwind_st(ctx) {
540            Ok(c) => AstNode::UnwindClause(c),
541            Err(e) => AstNode::Err(e),
542        }
543    }
544
545    fn visit_setSt(&mut self, ctx: &SetStContext<'input>) -> Self::Return {
546        match self.build_set_st(ctx) {
547            Ok(items) => AstNode::SetItems(items),
548            Err(e) => AstNode::Err(e),
549        }
550    }
551
552    fn visit_deleteSt(&mut self, ctx: &DeleteStContext<'input>) -> Self::Return {
553        match self.build_delete_st(ctx) {
554            Ok(d) => AstNode::DeleteItems(d),
555            Err(e) => AstNode::Err(e),
556        }
557    }
558
559    fn visit_removeSt(&mut self, ctx: &RemoveStContext<'input>) -> Self::Return {
560        match self.build_remove_st(ctx) {
561            Ok(items) => AstNode::RemoveItems(items),
562            Err(e) => AstNode::Err(e),
563        }
564    }
565
566    fn visit_createSt(&mut self, ctx: &CreateStContext<'input>) -> Self::Return {
567        match self.build_create_st(ctx) {
568            Ok(patterns) => AstNode::CreatePatterns(patterns),
569            Err(e) => AstNode::Err(e),
570        }
571    }
572
573    fn visit_mergeSt(&mut self, ctx: &MergeStContext<'input>) -> Self::Return {
574        match self.build_merge_st(ctx) {
575            Ok(c) => AstNode::MergeClause(c),
576            Err(e) => AstNode::Err(e),
577        }
578    }
579
580    fn visit_singlePartQ(&mut self, ctx: &SinglePartQContext<'input>) -> Self::Return {
581        match self.build_single_part_q(ctx) {
582            Ok(s) => AstNode::Statement(s),
583            Err(e) => AstNode::Err(e),
584        }
585    }
586
587    fn visit_multiPartQ(&mut self, ctx: &MultiPartQContext<'input>) -> Self::Return {
588        match self.build_multi_part_q(ctx) {
589            Ok(s) => AstNode::Statement(s),
590            Err(e) => AstNode::Err(e),
591        }
592    }
593
594    fn visit_regularQuery(&mut self, ctx: &RegularQueryContext<'input>) -> Self::Return {
595        match self.build_regular_query(ctx) {
596            Ok(s) => AstNode::Statement(s),
597            Err(e) => AstNode::Err(e),
598        }
599    }
600
601    // `query : explainSt | regularQuery | standaloneCall | createIndexSt`
602    // -- `regularQuery`/`explainSt`/`createIndexSt` need no override of
603    // their own *here* (default `visit_children` dispatch already routes
604    // to each rule's own override below); `standaloneCall` (a bare
605    // `CALL proc(...) YIELD ...` with no MATCH at all) builds its own
606    // `Statement::StandaloneCall` directly, same reasoning `visit_
607    // regularQuery` already has for delegating to a `build_*` helper.
608    fn visit_standaloneCall(&mut self, ctx: &StandaloneCallContext<'input>) -> Self::Return {
609        match self.build_standalone_call(ctx) {
610            Ok(s) => AstNode::Statement(s),
611            Err(e) => AstNode::Err(e),
612        }
613    }
614
615    fn visit_explainSt(&mut self, ctx: &ExplainStContext<'input>) -> Self::Return {
616        match self.build_explain_st(ctx) {
617            Ok(s) => AstNode::Statement(s),
618            Err(e) => AstNode::Err(e),
619        }
620    }
621
622    fn visit_createIndexSt(&mut self, ctx: &CreateIndexStContext<'input>) -> Self::Return {
623        match self.build_create_index_st(ctx) {
624            Ok(s) => AstNode::Statement(s),
625            Err(e) => AstNode::Err(e),
626        }
627    }
628
629    fn visit_listLit(&mut self, ctx: &ListLitContext<'input>) -> Self::Return {
630        let mut items = Vec::new();
631        if let Some(chain_ctx) = ctx.expressionChain() {
632            for expr_ctx in chain_ctx.expression_all() {
633                match self.visit(&*expr_ctx).into_return_expr() {
634                    Ok(e) => items.push(e),
635                    Err(e) => return AstNode::Err(e),
636                }
637            }
638        }
639        AstNode::ReturnExpr(ReturnExpr::ListLit(items))
640    }
641
642    fn visit_mapLit(&mut self, ctx: &MapLitContext<'input>) -> Self::Return {
643        let mut items = Vec::new();
644        for pair_ctx in ctx.mapPair_all() {
645            let name_ctx = pair_ctx.name().expect("mapPair always has a name");
646            let expr_ctx = pair_ctx
647                .expression()
648                .expect("mapPair always has an expression");
649            let value = match self.visit(&*expr_ctx).into_return_expr() {
650                Ok(v) => v,
651                Err(e) => return AstNode::Err(e),
652            };
653            items.push((name_text(&name_ctx), value));
654        }
655        AstNode::ReturnExpr(ReturnExpr::MapLit(items))
656    }
657}
658
659fn symbol_text(ctx: &SymbolContextAll) -> String {
660    match ctx.ESC_LITERAL() {
661        // `` `a weird name` `` -- strip the surrounding backticks.
662        Some(t) => {
663            let text = t.get_text();
664            text[1..text.len() - 1].to_string()
665        }
666        None => ctx.get_text(),
667    }
668}
669
670/// `name : symbol | reservedWord`, used for label/property/map-key names
671/// (unlike `symbol`, usable for bound-variable names too). Delegates to
672/// `symbol_text` (backtick-stripping) when the alternative taken is
673/// `symbol` -- a bare `.get_text()` here would keep the backticks
674/// themselves as part of the name (e.g. `` map.`name` `` would look up
675/// the map key `` `name` `` instead of `name`, always missing -- a real
676/// bug found via the TCK, not just deferred coverage). `reservedWord` has
677/// no escaping to strip either way.
678fn name_text(ctx: &NameContextAll) -> String {
679    match ctx.symbol() {
680        Some(s) => symbol_text(&s),
681        None => ctx.get_text(),
682    }
683}
684
685/// Shared by `visit_numLit` (unsigned) and `build_unary_add_sub_expr`'s
686/// sign-folding special case (`text` prefixed with `-`) -- see that
687/// function's docs for why a leading sign has to be handled there instead
688/// of in `DIGIT` itself.
689fn parse_num_lit_text(text: &str) -> Result<Literal, QueryError> {
690    // A hex/octal literal's own digits can end in `f`/`F`/`d`/`D` (real hex
691    // digits, e.g. `0x7FFFFFFFFFFFFFFF`) or contain `e`/`E` (also a real
692    // hex digit) -- neither is real openCypher's `<approximate number
693    // suffix>` (`F`/`D`/`f`), which per spec only ever follows a decimal
694    // literal already in scientific or common (has a `.`) notation. Found
695    // via a Phase 3 dry-run behavioral test failure
696    // (`int_literal_accepts_hex_and_octal_forms`): a hex literal ending in
697    // a suffix-shaped digit was misdetected as float and failed to parse.
698    let unsigned = text.strip_prefix('-').unwrap_or(text);
699    let is_hex_or_octal = unsigned
700        .as_bytes()
701        .get(1)
702        .is_some_and(|b| matches!(b, b'x' | b'X' | b'o' | b'O'))
703        && unsigned.starts_with('0');
704    let is_float = !is_hex_or_octal
705        && (text.contains('.')
706            || text.ends_with(['f', 'F', 'd', 'D'])
707            || text
708                .rfind(['e', 'E'])
709                .is_some_and(|i| text[..i].chars().all(|c| c.is_ascii_digit() || c == '-')));
710    if is_float {
711        let f: f64 = text
712            .parse()
713            .map_err(|_| QueryError::Syntax(format!("invalid float literal '{text}'")))?;
714        // `str::parse::<f64>()` silently returns `f64::INFINITY` for a
715        // magnitude beyond f64's representable range instead of erroring
716        // (`"1e999".parse::<f64>()` is `Ok(inf)`) -- real Cypher requires
717        // this to be a compile-time error (TCK Literals5 [27],
718        // `FloatingPointOverflow`).
719        if f.is_infinite() {
720            Err(QueryError::Syntax(format!(
721                "float literal '{text}' is too large to represent"
722            )))
723        } else {
724            Ok(Literal::Float(f))
725        }
726    } else {
727        parse_int_literal(text).map(Literal::Int)
728    }
729}
730
731fn compare_sign(ctx: &ComparisonSignsContextAll) -> CompareOp {
732    if ctx.LE().is_some() {
733        CompareOp::Le
734    } else if ctx.GE().is_some() {
735        CompareOp::Ge
736    } else if ctx.GT().is_some() {
737        CompareOp::Gt
738    } else if ctx.LT().is_some() {
739        CompareOp::Lt
740    } else if ctx.NOT_EQUAL().is_some() {
741        CompareOp::Ne
742    } else {
743        // Only ASSIGN ('=') left -- comparisonSigns' six alternatives are
744        // exhaustive.
745        CompareOp::Eq
746    }
747}
748
749fn string_exp_op(ctx: &StringExpPrefixContextAll) -> CompareOp {
750    if ctx.STARTS().is_some() {
751        CompareOp::StartsWith
752    } else if ctx.ENDS().is_some() {
753        CompareOp::EndsWith
754    } else {
755        CompareOp::Contains
756    }
757}
758
759fn invocation_name_text(ctx: &InvocationNameContextAll) -> String {
760    ctx.symbol_all()
761        .iter()
762        .map(|s| symbol_text(s))
763        .collect::<Vec<_>>()
764        .join(".")
765}
766
767/// Whether `atomicExpression` reduces to exactly a bare numeric literal --
768/// no property/label/postfix suffixes at any level between it and the
769/// `numLit` itself. Used by `build_unary_add_sub_expression` to fold a
770/// leading `-` directly into the literal (see that function's docs).
771/// `stringExpression`/`nullExpression`/`inExpression` no longer live at
772/// this level at all (moved up to `stringListNullExpression`, see its own
773/// docs) -- only `listExpression` (postfix index/slice) can still appear
774/// here, so that's the only check left.
775fn bare_num_lit<'i>(
776    ctx: &AtomicExpressionContextAll<'i>,
777) -> Option<std::rc::Rc<NumLitContextAll<'i>>> {
778    if !ctx.listExpression_all().is_empty() {
779        return None;
780    }
781    let prop_or_label = ctx.propertyOrLabelExpression()?;
782    if prop_or_label.nodeLabels().is_some() {
783        return None;
784    }
785    let prop_expr = prop_or_label.propertyExpression()?;
786    if !prop_expr.name_all().is_empty() {
787        return None;
788    }
789    prop_expr.atom()?.literal()?.numLit()
790}
791
792/// For a single-bound `list[..N]`/`list[N..]` slice, `expression_all()`
793/// alone can't say which side of `RANGE` the one present bound is on --
794/// walk the raw children between `LBRACK` and `RBRACK` to find out, same
795/// "read raw children in source order" approach `build_add_sub_expression`
796/// uses. `[`/`]`/`..` are the only fixed-text children possible here; the
797/// one remaining child is the expression itself.
798fn list_expr_bound_is_before_range(ctx: &ListExpressionContextAll) -> bool {
799    let mut seen_range = false;
800    for child in ctx.get_children() {
801        match child.get_text().as_str() {
802            "[" | "]" => continue,
803            ".." => seen_range = true,
804            _ => return !seen_range,
805        }
806    }
807    unreachable!("listExpression slice form always has exactly one expression child")
808}
809
810impl AstBuilder {
811    /// `properties : mapLit | parameter`. Only the `mapLit` alternative
812    /// has a real `NodePattern`/`RelPattern::props` representation --
813    /// `Vec<(String, ReturnExpr)>` has no "the whole map comes from one
814    /// parameter" shape, and pest doesn't support that on a pattern's
815    /// inline properties either (only `map_expr`), so rejecting it here
816    /// isn't a regression, just parity.
817    fn build_properties(
818        &mut self,
819        ctx: Option<Rc<PropertiesContextAll>>,
820    ) -> Result<Vec<(String, ReturnExpr)>, QueryError> {
821        let Some(ctx) = ctx else {
822            return Ok(Vec::new());
823        };
824        let Some(map_ctx) = ctx.mapLit() else {
825            return Err(QueryError::Syntax(
826                "a parameter can't be used as a pattern's whole properties map".into(),
827            ));
828        };
829        let expr = self.visit(&*map_ctx).into_return_expr()?;
830        let ReturnExpr::MapLit(items) = expr else {
831            unreachable!("mapLit always builds a ReturnExpr::MapLit");
832        };
833        Ok(items)
834    }
835
836    fn build_node_pattern(&mut self, ctx: &NodePatternContext) -> Result<NodePattern, QueryError> {
837        let var = ctx.symbol().map(|s| symbol_text(&s));
838        let labels = ctx
839            .nodeLabels()
840            .map(|nl| nl.name_all().iter().map(|n| name_text(n)).collect())
841            .unwrap_or_default();
842        let has_explicit_props = ctx.properties().is_some();
843        let props = self.build_properties(ctx.properties())?;
844        Ok(NodePattern {
845            var,
846            labels,
847            props,
848            has_explicit_props,
849        })
850    }
851
852    fn build_rel_detail(&mut self, ctx: &RelationDetailContext) -> Result<RelPattern, QueryError> {
853        let var = ctx.symbol().map(|s| symbol_text(&s));
854        let rel_types = ctx
855            .relationshipTypes()
856            .map(|rt| rt.name_all().iter().map(|n| name_text(n)).collect())
857            .unwrap_or_default();
858        let props = self.build_properties(ctx.properties())?;
859        let hop_range = ctx
860            .rangeLit()
861            .map(|r| parse_rel_range(&r.get_text()))
862            .transpose()?;
863        Ok(RelPattern {
864            var,
865            rel_types,
866            props,
867            // Overwritten by `build_relationship_pattern`, the only
868            // caller -- `relationDetail` itself (`[...]`) carries no
869            // directionality, that's `<`/`>` on the surrounding
870            // `relationshipPattern`.
871            direction: RelDirection::Either,
872            hop_range,
873            capture_path_segment: false,
874            rel_list_var: None,
875        })
876    }
877
878    fn build_relationship_pattern(
879        &mut self,
880        ctx: &RelationshipPatternContext,
881    ) -> Result<RelPattern, QueryError> {
882        let mut rel = match ctx.relationDetail() {
883            Some(rd) => self.visit(&*rd).into_rel_pattern()?,
884            None => RelPattern {
885                var: None,
886                rel_types: Vec::new(),
887                props: Vec::new(),
888                direction: RelDirection::Either,
889                hop_range: None,
890                capture_path_segment: false,
891                rel_list_var: None,
892            },
893        };
894        // Both LT and GT present (`<-[...]->`) is *not* "left wins" --
895        // it's the same undirected/either shape as neither being present
896        // (`-[...]-`), and CREATE/MERGE already reject `Either` outright
897        // (`RequiresDirectedRelationship`, executor.rs). Found via the
898        // TCK: the old `if LT ... else if GT ...` order silently treated
899        // `<-[:FOO]->` as plain `Left`, both letting CREATE wrongly
900        // succeed (Create2 [20]) and giving MATCH's own undirected
901        // multi-hop patterns the wrong direction entirely
902        // (Match5 [27]/Match6 [12]'s wrong row counts).
903        rel.direction = match (ctx.LT().is_some(), ctx.GT().is_some()) {
904            (true, false) => RelDirection::Left,
905            (false, true) => RelDirection::Right,
906            (true, true) | (false, false) => RelDirection::Either,
907        };
908        Ok(rel)
909    }
910
911    fn build_pattern_elem(&mut self, ctx: &PatternElemContext) -> Result<Pattern, QueryError> {
912        if ctx.LPAREN().is_some() || !ctx.qppElemChain_all().is_empty() {
913            return Err(QueryError::Syntax(
914                "quantified path patterns aren't supported yet".into(),
915            ));
916        }
917        let node_ctx = ctx
918            .nodePattern()
919            .expect("patternElem always starts with a nodePattern in the non-QPP alternative");
920        let start = self.visit(&*node_ctx).into_node_pattern()?;
921        let mut hops = Vec::new();
922        for chain in ctx.patternElemChain_all() {
923            let rel_ctx = chain
924                .relationshipPattern()
925                .expect("patternElemChain always has a relationshipPattern");
926            let node_ctx = chain
927                .nodePattern()
928                .expect("patternElemChain always has a nodePattern");
929            let rel = self.visit(&*rel_ctx).into_rel_pattern()?;
930            let node = self.visit(&*node_ctx).into_node_pattern()?;
931            hops.push((rel, node));
932        }
933        Ok(Pattern { start, hops })
934    }
935
936    /// `relationshipsChainPattern : nodePattern patternElemChain+` -- an
937    /// `atom` alternative (`(n)-->()` used directly as a boolean
938    /// expression, TCK's Pattern1/2 "Pattern predicate"), same node+chain
939    /// shape `build_pattern_elem` already builds for real match patterns,
940    /// just requiring at least one hop (no bare-node pattern predicate,
941    /// matching the grammar's own `+` here vs `patternElem`'s `*`).
942    fn build_relationships_chain_pattern(
943        &mut self,
944        ctx: &RelationshipsChainPatternContext,
945    ) -> Result<Pattern, QueryError> {
946        let node_ctx = ctx
947            .nodePattern()
948            .expect("relationshipsChainPattern always has a nodePattern");
949        let start = self.visit(&*node_ctx).into_node_pattern()?;
950        let mut hops = Vec::new();
951        for chain in ctx.patternElemChain_all() {
952            let rel_ctx = chain
953                .relationshipPattern()
954                .expect("patternElemChain always has a relationshipPattern");
955            let node_ctx = chain
956                .nodePattern()
957                .expect("patternElemChain always has a nodePattern");
958            let rel = self.visit(&*rel_ctx).into_rel_pattern()?;
959            let node = self.visit(&*node_ctx).into_node_pattern()?;
960            hops.push((rel, node));
961        }
962        Ok(Pattern { start, hops })
963    }
964
965    /// Mirrors `parser.rs`'s `parse_match_part` -- comma-separated pattern
966    /// parts splice into linear chains (shared-node merging) or split into
967    /// separate `QueryPart`s (disjoint cross join) via
968    /// `group_into_linear_patterns`, reused as-is.
969    fn build_match_st(&mut self, ctx: &MatchStContext) -> Result<Vec<QueryPart>, QueryError> {
970        let optional = ctx.OPTIONAL().is_some();
971        let pw = ctx
972            .patternWhere()
973            .expect("matchSt always has a patternWhere");
974        let where_clause = match pw.where_() {
975            Some(where_ctx) => {
976                let expr_ctx = where_ctx
977                    .expression()
978                    .expect("where always has an expression");
979                let expr = self.visit(&*expr_ctx).into_return_expr()?;
980                Some(return_expr_to_expr(expr)?)
981            }
982            None => None,
983        };
984        let pattern_ctx = pw.pattern().expect("patternWhere always has a pattern");
985
986        let mut path_var = None;
987        let mut shortest_path = false;
988        let mut patterns = Vec::new();
989        for (i, part) in pattern_ctx.patternPart_all().into_iter().enumerate() {
990            // `shortestPathWrapper` is grammar-permissive (any
991            // comma-separated position) -- restricted here to the first
992            // position only, same as `parser.rs`'s `parse_path_pattern`
993            // (real Cypher: naming/shortestPath only make sense on a
994            // single linear pattern, never a cross join).
995            let pattern = match part.shortestPathWrapper() {
996                Some(sp_ctx) => {
997                    if i != 0 {
998                        return Err(QueryError::Syntax(
999                            "shortestPath() must be the first (and only) comma-separated pattern"
1000                                .into(),
1001                        ));
1002                    }
1003                    shortest_path = true;
1004                    let elem_ctx = sp_ctx
1005                        .patternElem()
1006                        .expect("shortestPathWrapper always has a patternElem");
1007                    self.visit(&*elem_ctx).into_pattern()?
1008                }
1009                None => {
1010                    let elem_ctx = part.patternElem().expect(
1011                        "patternPart always has a patternElem when shortestPathWrapper is absent",
1012                    );
1013                    self.visit(&*elem_ctx).into_pattern()?
1014                }
1015            };
1016            patterns.push(pattern);
1017            if part.ASSIGN().is_some() {
1018                if path_var.is_some() {
1019                    return Err(QueryError::Syntax(
1020                        "at most one comma-separated pattern part can have a named-path variable"
1021                            .into(),
1022                    ));
1023                }
1024                let symbol_ctx = part
1025                    .symbol()
1026                    .expect("patternPart with ASSIGN always has a symbol");
1027                path_var = Some(symbol_text(&symbol_ctx));
1028            }
1029        }
1030
1031        let groups = group_into_linear_patterns(patterns)?;
1032        if groups.len() > 1 && (shortest_path || path_var.is_some()) {
1033            return Err(QueryError::Syntax(
1034                "a named path/shortestPath() can't span a comma-separated cross join".into(),
1035            ));
1036        }
1037        if shortest_path {
1038            validate_shortest_path_pattern(&groups[0])?;
1039        } else if path_var.is_some() {
1040            validate_named_path_pattern(&groups[0])?;
1041        }
1042
1043        // `where_clause` attaches to the *last* group only, same as
1044        // `parser.rs`'s `parse_match_part` -- a comma-separated cross join
1045        // sees every group's bindings by the time WHERE runs. `with` stays
1046        // unconditionally `None` here: this grammar's `matchSt` has no
1047        // trailing WITH of its own (that's a separate clause in the
1048        // statement's clause list, attached by whichever caller builds
1049        // that list, not here).
1050        let last = groups.len() - 1;
1051        Ok(groups
1052            .into_iter()
1053            .enumerate()
1054            .map(|(i, pattern)| QueryPart {
1055                optional,
1056                path_var: if i == 0 { path_var.clone() } else { None },
1057                shortest_path: i == 0 && shortest_path,
1058                pattern,
1059                where_clause: if i == last {
1060                    where_clause.clone()
1061                } else {
1062                    None
1063                },
1064                with: None,
1065                // Parts 1.. share the clause's relationship-uniqueness
1066                // scope with part 0 — see `QueryPart::continues_clause`.
1067                continues_clause: i > 0,
1068            })
1069            .collect())
1070    }
1071
1072    /// Mirrors `parser.rs`'s `parse_compare_expr` -- a chain folds into
1073    /// nested `And`s of each *adjacent* pair (`a op0 b op1 c` -> `(a op0
1074    /// b) AND (b op1 c)`, real Cypher's own chained-comparison semantics),
1075    /// not a separate AST shape. Operand type is `stringListNullExpression`
1076    /// (not `addSubExpression` directly) since the precedence fix moved
1077    /// `IN`/`STARTS WITH`/etc up to sit between this level and arithmetic
1078    /// -- see `build_string_list_null_expression`'s docs.
1079    fn build_comparison_expression(
1080        &mut self,
1081        ctx: &ComparisonExpressionContext,
1082    ) -> Result<ReturnExpr, QueryError> {
1083        let mut operands = Vec::new();
1084        for operand_ctx in ctx.stringListNullExpression_all() {
1085            operands.push(self.visit(&*operand_ctx).into_return_expr()?);
1086        }
1087        let mut ops = Vec::new();
1088        for sign_ctx in ctx.comparisonSigns_all() {
1089            ops.push(compare_sign(&sign_ctx));
1090        }
1091        if ops.is_empty() {
1092            return Ok(operands
1093                .into_iter()
1094                .next()
1095                .expect("comparisonExpression has at least one stringListNullExpression"));
1096        }
1097        let mut pairs = operands.windows(2).zip(&ops).map(|(pair, op)| {
1098            ReturnExpr::Compare(Box::new(pair[0].clone()), *op, Box::new(pair[1].clone()))
1099        });
1100        let mut acc = pairs
1101            .next()
1102            .expect("a comparison chain has at least one pair");
1103        for next in pairs {
1104            acc = ReturnExpr::And(Box::new(acc), Box::new(next));
1105        }
1106        Ok(acc)
1107    }
1108
1109    /// `SUB_all()`/`PLUS_all()` each only return same-type tokens, losing
1110    /// which operator occupies which position among possibly-mixed `+`/`-`
1111    /// -- walking the raw children directly instead recovers real source
1112    /// order for free, and lets ANTLR's own dispatch (`self.visit` on a
1113    /// generic child) route each operand to `visit_multDivExpression`
1114    /// rather than needing the typed `multDivExpression_all()` accessor at
1115    /// all. The grammar shape (`multDivExpression ((PLUS | SUB)
1116    /// multDivExpression)*`) guarantees strict operand/operator
1117    /// alternation, so no type check is needed to tell them apart.
1118    fn build_add_sub_expression(
1119        &mut self,
1120        ctx: &AddSubExpressionContext,
1121    ) -> Result<ReturnExpr, QueryError> {
1122        let mut children = ctx.get_children();
1123        let mut lhs = self
1124            .visit(
1125                &*children
1126                    .next()
1127                    .expect("addSubExpression has at least one multDivExpression"),
1128            )
1129            .into_return_expr()?;
1130        while let Some(op_node) = children.next() {
1131            let op = match op_node.get_text().as_str() {
1132                "+" => ArithOp::Add,
1133                "-" => ArithOp::Sub,
1134                other => unreachable!("unexpected addSubExpression operator {other:?}"),
1135            };
1136            let rhs_node = children
1137                .next()
1138                .expect("addSubExpression operator has a following multDivExpression");
1139            let rhs = self.visit(&*rhs_node).into_return_expr()?;
1140            lhs = ReturnExpr::Arith(Box::new(lhs), op, Box::new(rhs));
1141        }
1142        Ok(lhs)
1143    }
1144
1145    fn build_mult_div_expression(
1146        &mut self,
1147        ctx: &MultDivExpressionContext,
1148    ) -> Result<ReturnExpr, QueryError> {
1149        let mut children = ctx.get_children();
1150        let mut lhs = self
1151            .visit(
1152                &*children
1153                    .next()
1154                    .expect("multDivExpression has at least one powerExpression"),
1155            )
1156            .into_return_expr()?;
1157        while let Some(op_node) = children.next() {
1158            let op = match op_node.get_text().as_str() {
1159                "*" => ArithOp::Mul,
1160                "/" => ArithOp::Div,
1161                "%" => ArithOp::Mod,
1162                other => unreachable!("unexpected multDivExpression operator {other:?}"),
1163            };
1164            let rhs_node = children
1165                .next()
1166                .expect("multDivExpression operator has a following powerExpression");
1167            let rhs = self.visit(&*rhs_node).into_return_expr()?;
1168            lhs = ReturnExpr::Arith(Box::new(lhs), op, Box::new(rhs));
1169        }
1170        Ok(lhs)
1171    }
1172
1173    /// The parser already has correct unary-minus handling at this
1174    /// precedence level (`(PLUS | SUB)? atomicExpression`), but for
1175    /// `i64::MIN` (`-9223372036854775808`) to round-trip, the sign has to
1176    /// fold directly into the literal's own parse rather than building
1177    /// `Neg(Lit(Int(9223372036854775808)))` -- `9223372036854775808`
1178    /// itself doesn't fit in a positive `i64` at all (only `i64::MIN`'s
1179    /// magnitude does, via `parse_int_literal`'s two's-complement special
1180    /// case, which needs the sign in its input string up front). Pest's
1181    /// grammar sidestepped this by including an optional leading `-` in
1182    /// `int_literal`/`float_literal` themselves; this grammar's `DIGIT`
1183    /// deliberately doesn't (see the binary-minus fix), so the fold has to
1184    /// happen here instead, for the one case where the operand is exactly
1185    /// a bare numeric literal with no other operators/suffixes.
1186    fn build_unary_add_sub_expression(
1187        &mut self,
1188        ctx: &UnaryAddSubExpressionContext,
1189    ) -> Result<ReturnExpr, QueryError> {
1190        let atomic_ctx = ctx
1191            .atomicExpression()
1192            .expect("unaryAddSubExpression always has an atomicExpression");
1193        if ctx.SUB().is_some() {
1194            if let Some(numlit_ctx) = bare_num_lit(&atomic_ctx) {
1195                let text = numlit_ctx
1196                    .DIGIT()
1197                    .expect("numLit context always has a DIGIT token")
1198                    .get_text();
1199                return parse_num_lit_text(&format!("-{text}")).map(ReturnExpr::Lit);
1200            }
1201            let operand = self.visit(&*atomic_ctx).into_return_expr()?;
1202            return Ok(ReturnExpr::Neg(Box::new(operand)));
1203        }
1204        // A leading `+` is always a no-op in Cypher (`+x` is just `x`).
1205        self.visit(&*atomic_ctx).into_return_expr()
1206    }
1207
1208    /// `atomicExpression : propertyOrLabelExpression (listExpression)*`
1209    /// -- only postfix index/slice suffixes live here now (`IN`/
1210    /// `stringExpression`/`nullExpression` moved up to
1211    /// `stringListNullExpression`, see its own docs); genuinely
1212    /// left-to-right chainable (`list[0][1]`, real postfix repetition per
1213    /// openCypher.bnf's `<postfix expression> ::= ... | <postfix
1214    /// expression> <postfix operator>`), so no "at most one" restriction
1215    /// is needed here at all anymore.
1216    fn build_atomic_expression(
1217        &mut self,
1218        ctx: &AtomicExpressionContext,
1219    ) -> Result<ReturnExpr, QueryError> {
1220        let base_ctx = ctx
1221            .propertyOrLabelExpression()
1222            .expect("atomicExpression always has a propertyOrLabelExpression");
1223        let mut base = self.visit(&*base_ctx).into_return_expr()?;
1224        for l in ctx.listExpression_all() {
1225            base = self.build_list_expression(&l, base)?;
1226        }
1227        Ok(base)
1228    }
1229
1230    /// `stringListNullExpression : addSubExpression (stringExpression |
1231    /// inExpression | nullExpression)?` -- fixes a real precedence bug in
1232    /// the vendored grammar (found via a Phase 3 behavioral dry-run, not
1233    /// the TCK): `IN`/`STARTS WITH`/`ENDS WITH`/`CONTAINS`/`IS NULL` used
1234    /// to attach at `atomicExpression`'s level (tighter than `+`/`-`/`*`/
1235    /// `/`/`^`), so `n.val + 0 IS NULL` parsed as `n.val + (0 IS NULL)`.
1236    /// Per openCypher.bnf's `<comparison predicate>` chain, these operate
1237    /// on a full `<arithmetic value expression>` (this file's
1238    /// `addSubExpression`), sitting above arithmetic and below `=`/`<>`/
1239    /// `<`/`>`/`<=`/`>=` (`comparisonExpression`, one level up) --
1240    /// see `grammar/README.md` for the upstream PR this was also sent to.
1241    fn build_string_list_null_expression(
1242        &mut self,
1243        ctx: &StringListNullExpressionContext,
1244    ) -> Result<ReturnExpr, QueryError> {
1245        let base_ctx = ctx
1246            .addSubExpression()
1247            .expect("stringListNullExpression always has an addSubExpression");
1248        let base = self.visit(&*base_ctx).into_return_expr()?;
1249        if let Some(s) = ctx.stringExpression() {
1250            return self.build_string_expression(&s, base);
1251        }
1252        if let Some(i) = ctx.inExpression() {
1253            let rhs_ctx = i
1254                .addSubExpression()
1255                .expect("inExpression always has an addSubExpression");
1256            let rhs = self.visit(&*rhs_ctx).into_return_expr()?;
1257            return Ok(ReturnExpr::In(Box::new(base), Box::new(rhs)));
1258        }
1259        let Some(n) = ctx.nullExpression() else {
1260            return Ok(base);
1261        };
1262        Ok(if n.NOT().is_some() {
1263            ReturnExpr::Not(Box::new(ReturnExpr::IsNull(Box::new(base))))
1264        } else {
1265            ReturnExpr::IsNull(Box::new(base))
1266        })
1267    }
1268
1269    /// Operand widened from `propertyOrLabelExpression` to
1270    /// `addSubExpression` (moved up alongside `stringListNullExpression`,
1271    /// see its own docs) -- `x STARTS WITH y + z` is now real, matching
1272    /// spec's `<advanced comparison predicand> ::= <arithmetic value
1273    /// expression>`.
1274    fn build_string_expression(
1275        &mut self,
1276        ctx: &StringExpressionContextAll,
1277        base: ReturnExpr,
1278    ) -> Result<ReturnExpr, QueryError> {
1279        let prefix_ctx = ctx
1280            .stringExpPrefix()
1281            .expect("stringExpression always has a stringExpPrefix");
1282        let op = string_exp_op(&prefix_ctx);
1283        let rhs_ctx = ctx
1284            .addSubExpression()
1285            .expect("stringExpression always has an addSubExpression");
1286        let rhs = self.visit(&*rhs_ctx).into_return_expr()?;
1287        Ok(ReturnExpr::Compare(Box::new(base), op, Box::new(rhs)))
1288    }
1289
1290    /// `listExpression` no longer has an `IN` alternative at all (moved to
1291    /// the new `inExpression` rule, built directly in
1292    /// `build_string_list_null_expression`) -- only the postfix index/
1293    /// slice forms remain.
1294    fn build_list_expression(
1295        &mut self,
1296        ctx: &ListExpressionContextAll,
1297        base: ReturnExpr,
1298    ) -> Result<ReturnExpr, QueryError> {
1299        let exprs = ctx.expression_all();
1300        if ctx.RANGE().is_some() {
1301            // `list[start..end]` -- either bound can be omitted.
1302            // `expression_all()` in source order: 0, 1, or 2 present.
1303            let (start, end) = match exprs.len() {
1304                0 => (None, None),
1305                1 => {
1306                    // One bound present -- is it before or after `RANGE`?
1307                    // Same alternating-children approach as
1308                    // `build_add_sub_expression`: walk raw children past
1309                    // `LBRACK` and see whether the expression comes before
1310                    // or after the `..` token.
1311                    let before_range = list_expr_bound_is_before_range(ctx);
1312                    let e = self.visit(&*exprs[0].clone()).into_return_expr()?;
1313                    if before_range {
1314                        (Some(Box::new(e)), None)
1315                    } else {
1316                        (None, Some(Box::new(e)))
1317                    }
1318                }
1319                2 => {
1320                    let start = self.visit(&*exprs[0].clone()).into_return_expr()?;
1321                    let end = self.visit(&*exprs[1].clone()).into_return_expr()?;
1322                    (Some(Box::new(start)), Some(Box::new(end)))
1323                }
1324                n => unreachable!("listExpression slice form has {n} expressions, expected 0-2"),
1325            };
1326            return Ok(ReturnExpr::Slice(Box::new(base), start, end));
1327        }
1328        let index_ctx = exprs
1329            .into_iter()
1330            .next()
1331            .expect("non-slice listExpression always has exactly one expression");
1332        let index = self.visit(&*index_ctx).into_return_expr()?;
1333        Ok(ReturnExpr::Index(Box::new(base), Box::new(index)))
1334    }
1335
1336    fn build_property_or_label_expression(
1337        &mut self,
1338        ctx: &PropertyOrLabelExpressionContext,
1339    ) -> Result<ReturnExpr, QueryError> {
1340        let prop_ctx = ctx
1341            .propertyExpression()
1342            .expect("propertyOrLabelExpression always has a propertyExpression");
1343        let base = self.visit(&*prop_ctx).into_return_expr()?;
1344        let Some(labels_ctx) = ctx.nodeLabels() else {
1345            return Ok(base);
1346        };
1347        let ReturnExpr::Var(var) = base else {
1348            return Err(QueryError::Syntax(
1349                "a label check (`x:Label`) only applies to a bare variable".into(),
1350            ));
1351        };
1352        let labels = labels_ctx.name_all().iter().map(|n| name_text(n)).collect();
1353        Ok(ReturnExpr::HasLabel(var, labels))
1354    }
1355
1356    /// `propertyExpression : atom (DOT name)*`. A bare atom (no `.name`
1357    /// suffix) passes through unchanged; the first suffix on a bare
1358    /// variable becomes the flat `Prop` shape (`{var, prop}`); every other
1359    /// suffix -- the first one when `atom` isn't a bare variable, and
1360    /// every suffix after the first regardless -- becomes `PropOf`
1361    /// instead, folded left-to-right (`a.b.c` -> `PropOf(Prop{a,b}, c)`),
1362    /// each evaluated by evaluating its own base first, then looking the
1363    /// property up on whatever `Value` that produced (TCK's Graph6 [4]/
1364    /// [8], Map1 [3], Merge5 [11], With2 [2]).
1365    fn build_property_expression(
1366        &mut self,
1367        ctx: &PropertyExpressionContext,
1368    ) -> Result<ReturnExpr, QueryError> {
1369        let atom_ctx = ctx.atom().expect("propertyExpression always has an atom");
1370        let base = self.visit(&*atom_ctx).into_return_expr()?;
1371        let mut names = ctx.name_all().into_iter();
1372        let Some(first) = names.next() else {
1373            return Ok(base);
1374        };
1375        // First suffix on a bare variable becomes the flat `Prop` shape
1376        // (`{var, prop}`); every suffix after that -- including this one
1377        // when `base` isn't a bare variable -- becomes `PropOf`, folded
1378        // left-to-right (`a.b.c` -> `PropOf(Prop{a,b}, c)`, TCK's With2
1379        // `[2]`, `nestedMap.name.name2`).
1380        let mut expr = match base {
1381            ReturnExpr::Var(var) => ReturnExpr::Prop(PropAccess {
1382                var,
1383                prop: name_text(&first),
1384            }),
1385            other => ReturnExpr::PropOf(Box::new(other), name_text(&first)),
1386        };
1387        for name in names {
1388            expr = ReturnExpr::PropOf(Box::new(expr), name_text(&name));
1389        }
1390        Ok(expr)
1391    }
1392
1393    fn build_atom(&mut self, ctx: &AtomContext) -> Result<ReturnExpr, QueryError> {
1394        if let Some(lit_ctx) = ctx.literal() {
1395            return self.visit(&*lit_ctx).into_return_expr_lenient();
1396        }
1397        if let Some(param_ctx) = ctx.parameter() {
1398            return self.build_parameter(&param_ctx);
1399        }
1400        if let Some(paren_ctx) = ctx.parenthesizedExpression() {
1401            return self.visit(&*paren_ctx).into_return_expr();
1402        }
1403        if let Some(func_ctx) = ctx.functionInvocation() {
1404            return self.build_function_invocation(&func_ctx);
1405        }
1406        if let Some(count_ctx) = ctx.countAll() {
1407            let _ = self.visit(&*count_ctx);
1408            return Ok(ReturnExpr::CountStar);
1409        }
1410        if let Some(sym_ctx) = ctx.symbol() {
1411            return Ok(ReturnExpr::Var(symbol_text(&sym_ctx)));
1412        }
1413        if let Some(filter_ctx) = ctx.filterWith() {
1414            return self.build_filter_with(&filter_ctx);
1415        }
1416        if let Some(lc_ctx) = ctx.listComprehension() {
1417            return self.build_list_comprehension(&lc_ctx);
1418        }
1419        if let Some(case_ctx) = ctx.caseExpression() {
1420            return self.build_case_expression(&case_ctx);
1421        }
1422        if let Some(pc_ctx) = ctx.patternComprehension() {
1423            return self.build_pattern_comprehension(&pc_ctx);
1424        }
1425        if let Some(rcp_ctx) = ctx.relationshipsChainPattern() {
1426            return Ok(ReturnExpr::PatternPredicate(
1427                self.build_relationships_chain_pattern(&rcp_ctx)?,
1428            ));
1429        }
1430        if let Some(se_ctx) = ctx.subqueryExist() {
1431            return self.build_subquery_exist(&se_ctx);
1432        }
1433        Err(QueryError::Syntax(
1434            "this expression form (path-as-expression) isn't supported by the ANTLR parser yet"
1435                .into(),
1436        ))
1437    }
1438
1439    /// `patternComprehension : LBRACK lhs? relationshipsChainPattern where?
1440    /// STICK expression RBRACK` -- `lhs` (`symbol ASSIGN`) is the optional
1441    /// named-path capture (`p = (n)-->()`), reusing
1442    /// `build_relationships_chain_pattern` for the pattern itself (same
1443    /// node+chain shape a pattern predicate already builds, just here it's
1444    /// enumerated rather than existence-checked) and the same `where?`
1445    /// production `build_match_st` uses for an ordinary `MATCH`'s own
1446    /// pattern-level `WHERE` (not `ListComp`'s post-projection
1447    /// `ReturnExpr`-shaped filter -- `patternComprehension` shares its
1448    /// grammar rule with `MATCH`, not with `listComprehension`).
1449    fn build_pattern_comprehension(
1450        &mut self,
1451        ctx: &PatternComprehensionContext,
1452    ) -> Result<ReturnExpr, QueryError> {
1453        let path_var = ctx
1454            .lhs()
1455            .and_then(|lhs| lhs.symbol())
1456            .map(|s| symbol_text(&s));
1457        let rcp_ctx = ctx
1458            .relationshipsChainPattern()
1459            .expect("patternComprehension always has a relationshipsChainPattern");
1460        let pattern = self.build_relationships_chain_pattern(&rcp_ctx)?;
1461        let where_clause = match ctx.where_() {
1462            Some(where_ctx) => {
1463                let expr_ctx = where_ctx
1464                    .expression()
1465                    .expect("where always has an expression");
1466                let expr = self.visit(&*expr_ctx).into_return_expr()?;
1467                Some(Box::new(return_expr_to_expr(expr)?))
1468            }
1469            None => None,
1470        };
1471        let proj_ctx = ctx
1472            .expression()
1473            .expect("patternComprehension always has a projection expression");
1474        let projection = self.visit(&*proj_ctx).into_return_expr()?;
1475        Ok(ReturnExpr::PatternComprehension {
1476            path_var,
1477            pattern: Box::new(pattern),
1478            where_clause,
1479            projection: Box::new(projection),
1480        })
1481    }
1482
1483    /// `subqueryExist : EXISTS LBRACE (regularQuery | patternWhere)
1484    /// RBRACE` -- `patternWhere` (TCK's ExistentialSubquery1, the "simple"
1485    /// form: a pattern with an optional inline `WHERE`, same grammar rule
1486    /// `MATCH` itself uses) builds a `ReturnExpr::ExistsPattern`;
1487    /// `regularQuery` (TCK's ExistentialSubquery2/3, a full nested `MATCH
1488    /// ... RETURN ...` subquery, arbitrarily many clauses, possibly itself
1489    /// containing a nested `exists {}`) reuses `build_regular_query`
1490    /// verbatim -- the exact same builder a top-level statement goes
1491    /// through -- and wraps the result in `ReturnExpr::ExistsSubquery`.
1492    /// Real Cypher restricts `exists {}`'s body to read-only clauses;
1493    /// `semantic::validate_statement`/`validate_match_clauses` reject a
1494    /// mutating clause or non-`Statement::Match` shape at compile time
1495    /// (TCK's ExistentialSubquery2 `[3]`), not here -- this stays a
1496    /// structural build step, same division of labor as every other
1497    /// pattern this visitor builds.
1498    fn build_subquery_exist(
1499        &mut self,
1500        ctx: &SubqueryExistContext,
1501    ) -> Result<ReturnExpr, QueryError> {
1502        if let Some(rq_ctx) = ctx.regularQuery() {
1503            let stmt = self.build_regular_query(&rq_ctx)?;
1504            return Ok(ReturnExpr::ExistsSubquery(Box::new(stmt)));
1505        }
1506        let pw_ctx = ctx
1507            .patternWhere()
1508            .expect("subqueryExist always has a regularQuery or patternWhere");
1509        let pattern_ctx = pw_ctx.pattern().expect("patternWhere always has a pattern");
1510        let mut parts = pattern_ctx.patternPart_all().into_iter();
1511        let part = parts
1512            .next()
1513            .expect("pattern always has at least one patternPart");
1514        if parts.next().is_some() {
1515            return Err(QueryError::Syntax(
1516                "exists {} with more than one comma-separated pattern isn't supported yet".into(),
1517            ));
1518        }
1519        if part.ASSIGN().is_some() || part.shortestPathWrapper().is_some() {
1520            return Err(QueryError::Syntax(
1521                "exists {} doesn't support a named path or shortestPath()".into(),
1522            ));
1523        }
1524        let elem_ctx = part
1525            .patternElem()
1526            .expect("a patternPart without ASSIGN/shortestPathWrapper always has a patternElem");
1527        let pattern = self.visit(&*elem_ctx).into_pattern()?;
1528        let where_clause = match pw_ctx.where_() {
1529            Some(where_ctx) => {
1530                let expr_ctx = where_ctx
1531                    .expression()
1532                    .expect("where always has an expression");
1533                let expr = self.visit(&*expr_ctx).into_return_expr()?;
1534                Some(Box::new(return_expr_to_expr(expr)?))
1535            }
1536            None => None,
1537        };
1538        Ok(ReturnExpr::ExistsPattern {
1539            pattern: Box::new(pattern),
1540            where_clause,
1541        })
1542    }
1543
1544    /// `caseExpression : CASE expression? (WHEN expression THEN
1545    /// expression)+ (ELSE expression)? END`. No typed per-`WHEN`/`THEN`
1546    /// accessor exists (`expression_all()` flattens every branch's exprs
1547    /// together, `WHEN()`/`THEN()`/`ELSE()` only ever return the *first*
1548    /// occurrence) -- walked via raw children instead, same "read raw
1549    /// children in source order" approach `build_add_sub_expression` uses,
1550    /// tracking position via each keyword *terminal*'s own text. Matched
1551    /// case-insensitively (the lexer's `caseInsensitive = true` means
1552    /// `get_text()` returns the source's own casing, e.g. `case`/`CASE`
1553    /// both valid) -- safe against a same-named real expression, since
1554    /// CASE/WHEN/THEN/ELSE/END are all in `reservedWord`, so none can
1555    /// appear as a bare variable at this position.
1556    fn build_case_expression(
1557        &mut self,
1558        ctx: &CaseExpressionContext,
1559    ) -> Result<ReturnExpr, QueryError> {
1560        #[derive(PartialEq)]
1561        enum Pos {
1562            BeforeFirstWhen,
1563            AfterWhen,
1564            AfterThen,
1565            AfterElse,
1566        }
1567        let mut pos = Pos::BeforeFirstWhen;
1568        let mut test = None;
1569        let mut whens: Vec<(ReturnExpr, ReturnExpr)> = Vec::new();
1570        let mut pending_when: Option<ReturnExpr> = None;
1571        let mut else_ = None;
1572        for child in ctx.get_children() {
1573            match child.get_text().to_ascii_uppercase().as_str() {
1574                "CASE" | "END" => continue,
1575                "WHEN" => pos = Pos::AfterWhen,
1576                "THEN" => pos = Pos::AfterThen,
1577                "ELSE" => pos = Pos::AfterElse,
1578                _ => {
1579                    let expr = self.visit(&*child).into_return_expr()?;
1580                    match pos {
1581                        Pos::BeforeFirstWhen => test = Some(Box::new(expr)),
1582                        Pos::AfterWhen => pending_when = Some(expr),
1583                        Pos::AfterThen => {
1584                            let w = pending_when
1585                                .take()
1586                                .expect("a THEN expression always follows a WHEN expression");
1587                            whens.push((w, expr));
1588                        }
1589                        Pos::AfterElse => else_ = Some(Box::new(expr)),
1590                    }
1591                }
1592            }
1593        }
1594        Ok(ReturnExpr::Case { test, whens, else_ })
1595    }
1596
1597    /// `filterExpression : symbol IN expression where?` -- shared by
1598    /// `filterWith` (ALL/ANY/NONE/SINGLE quantifiers) and
1599    /// `listComprehension`, both of which bind one variable over a source
1600    /// list, optionally filtered.
1601    fn build_filter_expression(
1602        &mut self,
1603        ctx: &FilterExpressionContext,
1604    ) -> Result<(String, ReturnExpr, Option<Box<ReturnExpr>>), QueryError> {
1605        let var_ctx = ctx.symbol().expect("filterExpression always has a symbol");
1606        let var = symbol_text(&var_ctx);
1607        let source_ctx = ctx
1608            .expression()
1609            .expect("filterExpression always has an expression");
1610        let source = self.visit(&*source_ctx).into_return_expr()?;
1611        let where_clause = match ctx.where_() {
1612            Some(where_ctx) => {
1613                let expr_ctx = where_ctx
1614                    .expression()
1615                    .expect("where always has an expression");
1616                Some(Box::new(self.visit(&*expr_ctx).into_return_expr()?))
1617            }
1618            None => None,
1619        };
1620        Ok((var, source, where_clause))
1621    }
1622
1623    /// `filterWith : (ALL | ANY | NONE | SINGLE) LPAREN filterExpression
1624    /// RPAREN` -- `ReturnExpr::Quantifier`, always evaluates to a bool
1625    /// (`where_clause` absent means "every element's own truthiness", same
1626    /// convention `Quantifier::where_clause`'s own docs describe).
1627    fn build_filter_with(&mut self, ctx: &FilterWithContext) -> Result<ReturnExpr, QueryError> {
1628        let kind = if ctx.ALL().is_some() {
1629            QuantifierKind::All
1630        } else if ctx.ANY().is_some() {
1631            QuantifierKind::Any
1632        } else if ctx.NONE().is_some() {
1633            QuantifierKind::None
1634        } else {
1635            ctx.SINGLE()
1636                .expect("filterWith always has one of ALL/ANY/NONE/SINGLE");
1637            QuantifierKind::Single
1638        };
1639        let fe_ctx = ctx
1640            .filterExpression()
1641            .expect("filterWith always has a filterExpression");
1642        let (var, source, where_clause) = self.build_filter_expression(&fe_ctx)?;
1643        Ok(ReturnExpr::Quantifier {
1644            kind,
1645            var,
1646            source: Box::new(source),
1647            where_clause,
1648        })
1649    }
1650
1651    /// `listComprehension : LBRACK filterExpression (STICK expression)?
1652    /// RBRACK` -- `ctx.expression()` here is `listComprehension`'s own
1653    /// direct child (the `STICK`-following projection), not
1654    /// `filterExpression`'s nested one (a different context type, no
1655    /// ambiguity).
1656    fn build_list_comprehension(
1657        &mut self,
1658        ctx: &ListComprehensionContext,
1659    ) -> Result<ReturnExpr, QueryError> {
1660        let fe_ctx = ctx
1661            .filterExpression()
1662            .expect("listComprehension always has a filterExpression");
1663        let (var, source, where_clause) = self.build_filter_expression(&fe_ctx)?;
1664        let project = match ctx.expression() {
1665            Some(expr_ctx) => Some(Box::new(self.visit(&*expr_ctx).into_return_expr()?)),
1666            None => None,
1667        };
1668        Ok(ReturnExpr::ListComp {
1669            var,
1670            source: Box::new(source),
1671            where_clause,
1672            project,
1673        })
1674    }
1675
1676    fn build_function_invocation(
1677        &mut self,
1678        ctx: &FunctionInvocationContext,
1679    ) -> Result<ReturnExpr, QueryError> {
1680        let name_ctx = ctx
1681            .invocationName()
1682            .expect("functionInvocation always has an invocationName");
1683        let name = invocation_name_text(&name_ctx);
1684        let distinct = ctx.DISTINCT().is_some();
1685        let mut args = Vec::new();
1686        if let Some(chain_ctx) = ctx.expressionChain() {
1687            for arg_ctx in chain_ctx.expression_all() {
1688                args.push(self.visit(&*arg_ctx).into_return_expr()?);
1689            }
1690        }
1691        if distinct && !is_aggregate_name(&name) {
1692            return Err(QueryError::Syntax(format!(
1693                "'{name}(DISTINCT ...)' isn't valid — DISTINCT is only meaningful inside an aggregate function"
1694            )));
1695        }
1696        Ok(ReturnExpr::Call {
1697            name,
1698            args,
1699            distinct,
1700        })
1701    }
1702
1703    /// `standaloneCall : CALL invocationName parenExpressionChain? (YIELD
1704    /// (MULT | yieldItems))?` -- the top-level, no-MATCH form (TCK's
1705    /// Call1/Call2). `parenExpressionChain?`'s absence is the implicit-
1706    /// argument shape (`CALL proc`, no parens at all -- `CallClause::args:
1707    /// None`, see its own docs); `MULT` (`YIELD *`) is only reachable
1708    /// here, never from `queryCallSt`'s own grammar production below.
1709    fn build_standalone_call(
1710        &mut self,
1711        ctx: &StandaloneCallContext,
1712    ) -> Result<Statement, QueryError> {
1713        let name_ctx = ctx
1714            .invocationName()
1715            .expect("standaloneCall always has an invocationName");
1716        let name = invocation_name_text(&name_ctx);
1717        let args = match ctx.parenExpressionChain() {
1718            Some(paren_ctx) => Some(self.build_call_args(&paren_ctx)?),
1719            None => None,
1720        };
1721        let yield_items = if ctx.MULT().is_some() {
1722            Some(CallYield::Star)
1723        } else if let Some(yi_ctx) = ctx.yieldItems() {
1724            Some(self.build_yield_items(&yi_ctx)?)
1725        } else {
1726            None
1727        };
1728        Ok(Statement::StandaloneCall(Box::new(CallClause {
1729            name,
1730            args,
1731            with: None,
1732            yield_items,
1733        })))
1734    }
1735
1736    /// `queryCallSt : CALL invocationName parenExpressionChain (YIELD
1737    /// yieldItems)?` -- the in-query reading-clause form (TCK's
1738    /// Call1 `[3]`/`[4]`/etc). Parens are mandatory here (no implicit-
1739    /// argument shape mid-query, TCK's Call2 `[4]`, `@skipGrammarCheck`
1740    /// but structurally impossible to reach via this grammar rule either
1741    /// way) and there's no `YIELD *` alternative (only `standaloneCall`
1742    /// has one).
1743    fn build_query_call_st(
1744        &mut self,
1745        ctx: &QueryCallStContextAll,
1746    ) -> Result<CallClause, QueryError> {
1747        let name_ctx = ctx
1748            .invocationName()
1749            .expect("queryCallSt always has an invocationName");
1750        let name = invocation_name_text(&name_ctx);
1751        let paren_ctx = ctx
1752            .parenExpressionChain()
1753            .expect("queryCallSt always has a parenExpressionChain");
1754        let args = Some(self.build_call_args(&paren_ctx)?);
1755        let yield_items = match ctx.yieldItems() {
1756            Some(yi_ctx) => Some(self.build_yield_items(&yi_ctx)?),
1757            None => None,
1758        };
1759        Ok(CallClause {
1760            name,
1761            args,
1762            with: None,
1763            yield_items,
1764        })
1765    }
1766
1767    fn build_call_args(
1768        &mut self,
1769        ctx: &ParenExpressionChainContextAll,
1770    ) -> Result<Vec<ReturnExpr>, QueryError> {
1771        let mut args = Vec::new();
1772        if let Some(chain_ctx) = ctx.expressionChain() {
1773            for arg_ctx in chain_ctx.expression_all() {
1774                args.push(self.visit(&*arg_ctx).into_return_expr()?);
1775            }
1776        }
1777        Ok(args)
1778    }
1779
1780    /// `yieldItems : yieldItem (COMMA yieldItem)* where?`, `yieldItem :
1781    /// (symbol AS)? symbol` -- one or two `symbol`s per item: two means
1782    /// `a AS c` (the procedure's own declared output name `a`, renamed to
1783    /// `c`), one means the declared name doubles as the binding name too
1784    /// (no rename).
1785    fn build_yield_items(&mut self, ctx: &YieldItemsContextAll) -> Result<CallYield, QueryError> {
1786        let mut items = Vec::new();
1787        for item_ctx in ctx.yieldItem_all() {
1788            let symbols = item_ctx.symbol_all();
1789            let (name, alias) = match symbols.len() {
1790                1 => (symbol_text(&symbols[0]), None),
1791                2 => (symbol_text(&symbols[0]), Some(symbol_text(&symbols[1]))),
1792                other => unreachable!("yieldItem always has 1 or 2 symbols, got {other}"),
1793            };
1794            items.push((name, alias));
1795        }
1796        let where_clause = match ctx.where_() {
1797            Some(where_ctx) => {
1798                let expr_ctx = where_ctx
1799                    .expression()
1800                    .expect("where always has an expression");
1801                let expr = self.visit(&*expr_ctx).into_return_expr()?;
1802                Some(Box::new(return_expr_to_expr(expr)?))
1803            }
1804            None => None,
1805        };
1806        Ok(CallYield::Items(items, where_clause))
1807    }
1808
1809    fn build_parameter(&mut self, ctx: &ParameterContext) -> Result<ReturnExpr, QueryError> {
1810        let name = if let Some(sym_ctx) = ctx.symbol() {
1811            symbol_text(&sym_ctx)
1812        } else if let Some(num_ctx) = ctx.numLit() {
1813            num_ctx
1814                .DIGIT()
1815                .expect("numLit context always has a DIGIT token")
1816                .get_text()
1817        } else {
1818            unreachable!("parameter always has a symbol or numLit")
1819        };
1820        Ok(ReturnExpr::Lit(Literal::Param(name)))
1821    }
1822
1823    fn build_projection_body(
1824        &mut self,
1825        ctx: &ProjectionBodyContext,
1826    ) -> Result<ParsedReturnClause, QueryError> {
1827        let distinct = ctx.DISTINCT().is_some();
1828        let items_ctx = ctx
1829            .projectionItems()
1830            .expect("projectionBody always has projectionItems");
1831        let tail = if items_ctx.MULT().is_some() {
1832            // `projectionItems : (MULT | projectionItem) (COMMA
1833            // projectionItem)*` syntactically allows `RETURN *, x AS y`
1834            // (MULT first, then a COMMA'd projectionItem) -- but
1835            // `Tail::ReturnStar` has no field for extra items alongside
1836            // the star (unlike `WithClause`, which has both `star` and
1837            // `items`), so silently taking the star-only path here would
1838            // drop `x AS y` on the floor. Error instead.
1839            if !items_ctx.projectionItem_all().is_empty() {
1840                return Err(QueryError::Syntax(
1841                    "RETURN * can't be combined with additional items".into(),
1842                ));
1843            }
1844            Tail::ReturnStar(distinct)
1845        } else {
1846            let mut items = Vec::new();
1847            for item_ctx in items_ctx.projectionItem_all() {
1848                let expr_ctx = item_ctx
1849                    .expression()
1850                    .expect("projectionItem always has an expression");
1851                let expr = self.visit(&*expr_ctx).into_return_expr()?;
1852                let alias = item_ctx.symbol().map(|s| symbol_text(&s));
1853                items.push(ReturnItem { expr, alias });
1854            }
1855            Tail::Return(items, distinct)
1856        };
1857
1858        let (order_by, skip, limit) = self.build_order_skip_limit(ctx)?;
1859
1860        Ok(ParsedReturnClause {
1861            tail,
1862            order_by,
1863            skip,
1864            limit,
1865        })
1866    }
1867
1868    /// Shared by `build_projection_body` (RETURN) and `build_with_clause`
1869    /// (WITH) -- both grammar rules bundle `orderSt`/`skipSt`/`limitSt`
1870    /// into the same `projectionBody`.
1871    #[allow(clippy::type_complexity)]
1872    fn build_order_skip_limit(
1873        &mut self,
1874        ctx: &ProjectionBodyContext,
1875    ) -> Result<
1876        (
1877            Option<Vec<(ReturnExpr, SortDir)>>,
1878            Option<ReturnExpr>,
1879            Option<ReturnExpr>,
1880        ),
1881        QueryError,
1882    > {
1883        let order_by = match ctx.orderSt() {
1884            Some(order_ctx) => Some(self.build_order_by(&order_ctx)?),
1885            None => None,
1886        };
1887        let skip = match ctx.skipSt() {
1888            Some(skip_ctx) => {
1889                let expr_ctx = skip_ctx
1890                    .expression()
1891                    .expect("skipSt always has an expression");
1892                Some(self.visit(&*expr_ctx).into_return_expr()?)
1893            }
1894            None => None,
1895        };
1896        let limit = match ctx.limitSt() {
1897            Some(limit_ctx) => {
1898                let expr_ctx = limit_ctx
1899                    .expression()
1900                    .expect("limitSt always has an expression");
1901                Some(self.visit(&*expr_ctx).into_return_expr()?)
1902            }
1903            None => None,
1904        };
1905        Ok((order_by, skip, limit))
1906    }
1907
1908    fn build_order_by(
1909        &mut self,
1910        ctx: &OrderStContext,
1911    ) -> Result<Vec<(ReturnExpr, SortDir)>, QueryError> {
1912        let mut items = Vec::new();
1913        for item_ctx in ctx.orderItem_all() {
1914            let expr_ctx = item_ctx
1915                .expression()
1916                .expect("orderItem always has an expression");
1917            let expr = self.visit(&*expr_ctx).into_return_expr()?;
1918            let dir = if item_ctx.DESC().is_some() || item_ctx.DESCENDING().is_some() {
1919                SortDir::Desc
1920            } else {
1921                SortDir::Asc
1922            };
1923            items.push((expr, dir));
1924        }
1925        Ok(items)
1926    }
1927
1928    fn build_with_clause(&mut self, ctx: &WithStContext) -> Result<WithClause, QueryError> {
1929        let body_ctx = ctx
1930            .projectionBody()
1931            .expect("withSt always has a projectionBody");
1932        let distinct = body_ctx.DISTINCT().is_some();
1933        let items_ctx = body_ctx
1934            .projectionItems()
1935            .expect("projectionBody always has projectionItems");
1936        let star = items_ctx.MULT().is_some();
1937        let mut items = Vec::new();
1938        for item_ctx in items_ctx.projectionItem_all() {
1939            let expr_ctx = item_ctx
1940                .expression()
1941                .expect("projectionItem always has an expression");
1942            let expr = self.visit(&*expr_ctx).into_return_expr()?;
1943            let alias = item_ctx.symbol().map(|s| symbol_text(&s));
1944            items.push(ReturnItem { expr, alias });
1945        }
1946        let (order_by, skip, limit) = self.build_order_skip_limit(&body_ctx)?;
1947        let where_clause = match ctx.where_() {
1948            Some(where_ctx) => {
1949                let expr_ctx = where_ctx
1950                    .expression()
1951                    .expect("where always has an expression");
1952                let expr = self.visit(&*expr_ctx).into_return_expr()?;
1953                Some(return_expr_to_with_expr(expr))
1954            }
1955            None => None,
1956        };
1957        Ok(WithClause {
1958            items,
1959            star,
1960            distinct,
1961            where_clause,
1962            order_by,
1963            skip,
1964            limit,
1965        })
1966    }
1967
1968    /// `UnwindClause::where_clause`/`::with` are populated wherever mars's
1969    /// own AST assembly attaches a following `WHERE`/`WITH` -- neither is
1970    /// part of `unwindSt`'s own grammar (`UNWIND expression AS symbol`,
1971    /// no trailing clauses at all), unlike pest's grammar, which does let
1972    /// UNWIND carry an inline WHERE directly (a mars-specific extension
1973    /// beyond real openCypher syntax, per `UnwindClause::where_clause`'s
1974    /// own docs). Always `None` here; a real capability gap versus pest
1975    /// for this specific extension, not a deferred-for-now stub.
1976    fn build_unwind_st(&mut self, ctx: &UnwindStContext) -> Result<UnwindClause, QueryError> {
1977        let expr_ctx = ctx.expression().expect("unwindSt always has an expression");
1978        let source = UnwindSource(self.visit(&*expr_ctx).into_return_expr()?);
1979        let var_ctx = ctx.symbol().expect("unwindSt always has a symbol");
1980        Ok(UnwindClause {
1981            source,
1982            var: symbol_text(&var_ctx),
1983            where_clause: None,
1984            with: None,
1985        })
1986    }
1987
1988    fn build_set_st(&mut self, ctx: &SetStContext) -> Result<Vec<SetItem>, QueryError> {
1989        ctx.setItem_all()
1990            .into_iter()
1991            .map(|item_ctx| self.build_set_item(&item_ctx))
1992            .collect()
1993    }
1994
1995    fn build_set_item(&mut self, ctx: &SetItemContextAll) -> Result<SetItem, QueryError> {
1996        // `setItem`'s first alternative is `propertyExpression ASSIGN
1997        // expression`, and `propertyExpression`'s own zero-`.name`-suffix
1998        // form degenerates to a bare variable -- so `n = {...}` (no dots
1999        // at all) parses through *this* alternative too, not the
2000        // `symbol ASSIGN expression` one below (which ANTLR only reaches
2001        // for `+=`, since alternative one has no ADD_ASSIGN option at
2002        // all). `build_property_expression`'s result tells them apart:
2003        // `Prop` is real `x.prop` access; `Var` is the degenerate case,
2004        // meaning `SetItem::MapAssign` (never `merge: true` here --
2005        // that's only reachable via `+=`, which can't take this branch).
2006        if let Some(prop_ctx) = ctx.propertyExpression() {
2007            let expr_ctx = ctx
2008                .expression()
2009                .expect("setItem's propertyExpression form always has an expression");
2010            return match self.build_property_expression(&prop_ctx)? {
2011                ReturnExpr::Prop(prop) => {
2012                    let value = self.visit(&*expr_ctx).into_return_expr()?;
2013                    Ok(SetItem::Prop(prop, value))
2014                }
2015                ReturnExpr::Var(var) => {
2016                    let value = self.visit(&*expr_ctx).into_return_expr()?;
2017                    Ok(SetItem::MapAssign {
2018                        var,
2019                        value,
2020                        merge: false,
2021                    })
2022                }
2023                _ => Err(QueryError::Syntax(
2024                    "expected a property access (x.prop) or variable on the left of SET's `=`"
2025                        .into(),
2026                )),
2027            };
2028        }
2029        let sym_ctx = ctx
2030            .symbol()
2031            .expect("setItem always has a propertyExpression or symbol");
2032        let var = symbol_text(&sym_ctx);
2033        if let Some(labels_ctx) = ctx.nodeLabels() {
2034            let labels = labels_ctx.name_all().iter().map(|n| name_text(n)).collect();
2035            return Ok(SetItem::Labels(var, labels));
2036        }
2037        let expr_ctx = ctx
2038            .expression()
2039            .expect("setItem's symbol-assign form always has an expression");
2040        let value = self.visit(&*expr_ctx).into_return_expr()?;
2041        Ok(SetItem::MapAssign {
2042            var,
2043            value,
2044            merge: ctx.ADD_ASSIGN().is_some(),
2045        })
2046    }
2047
2048    fn build_delete_st(&mut self, ctx: &DeleteStContext) -> Result<ParsedDelete, QueryError> {
2049        let chain_ctx = ctx
2050            .expressionChain()
2051            .expect("deleteSt always has an expressionChain");
2052        let mut items = Vec::new();
2053        for expr_ctx in chain_ctx.expression_all() {
2054            items.push(self.visit(&*expr_ctx).into_return_expr()?);
2055        }
2056        Ok(ParsedDelete {
2057            items,
2058            detach: ctx.DETACH().is_some(),
2059        })
2060    }
2061
2062    fn build_remove_st(&mut self, ctx: &RemoveStContext) -> Result<Vec<RemoveItem>, QueryError> {
2063        ctx.removeItem_all()
2064            .into_iter()
2065            .map(|item_ctx| self.build_remove_item(&item_ctx))
2066            .collect()
2067    }
2068
2069    fn build_remove_item(&mut self, ctx: &RemoveItemContextAll) -> Result<RemoveItem, QueryError> {
2070        if let Some(prop_ctx) = ctx.propertyExpression() {
2071            return Ok(RemoveItem::Prop(self.build_prop_access(&prop_ctx)?));
2072        }
2073        let sym_ctx = ctx
2074            .symbol()
2075            .expect("removeItem always has a symbol+nodeLabels or a propertyExpression");
2076        let labels_ctx = ctx
2077            .nodeLabels()
2078            .expect("removeItem's symbol form always has nodeLabels");
2079        let labels = labels_ctx.name_all().iter().map(|n| name_text(n)).collect();
2080        Ok(RemoveItem::Labels(symbol_text(&sym_ctx), labels))
2081    }
2082
2083    /// `propertyExpression`'s own grammar rule is reused by `setItem`/
2084    /// `removeItem` for their `x.prop` alternative -- `build_property_
2085    /// expression` already builds exactly `ReturnExpr::Prop` for that
2086    /// shape (or errors for anything wider, chained access etc), so this
2087    /// just unwraps the one variant these two callers can ever legally
2088    /// see here (the grammar alternative they're on doesn't admit a bare
2089    /// `symbol` or anything else propertyExpression could otherwise
2090    /// produce).
2091    fn build_prop_access(
2092        &mut self,
2093        ctx: &PropertyExpressionContext,
2094    ) -> Result<PropAccess, QueryError> {
2095        match self.build_property_expression(ctx)? {
2096            ReturnExpr::Prop(p) => Ok(p),
2097            _ => Err(QueryError::Syntax(
2098                "expected a property access (x.prop)".into(),
2099            )),
2100        }
2101    }
2102
2103    /// `Statement::Create`'s `Vec<Pattern>` has no named-path-capture slot
2104    /// at all (unlike `QueryPart::path_var`), and unlike `MATCH`, CREATE's
2105    /// comma-separated patterns are never spliced into linear chains --
2106    /// each becomes its own independent `Pattern` directly (matches
2107    /// `parser.rs`'s `parse_create_patterns`, which does the same, no
2108    /// `group_into_linear_patterns` call).
2109    fn build_create_st(&mut self, ctx: &CreateStContext) -> Result<Vec<Pattern>, QueryError> {
2110        let pattern_ctx = ctx.pattern().expect("createSt always has a pattern");
2111        pattern_ctx
2112            .patternPart_all()
2113            .into_iter()
2114            .map(|part_ctx| {
2115                if part_ctx.ASSIGN().is_some() {
2116                    return Err(QueryError::Syntax(
2117                        "named-path capture (`p = ...`) isn't supported on CREATE".into(),
2118                    ));
2119                }
2120                if part_ctx.shortestPathWrapper().is_some() {
2121                    return Err(QueryError::Syntax(
2122                        "shortestPath() isn't valid in CREATE".into(),
2123                    ));
2124                }
2125                let elem_ctx = part_ctx.patternElem().expect(
2126                    "patternPart always has a patternElem when shortestPathWrapper is absent",
2127                );
2128                self.visit(&*elem_ctx).into_pattern()
2129            })
2130            .collect()
2131    }
2132
2133    /// Mirrors `parser.rs`'s `parse_merge_clause`: `MergeClause::pattern`
2134    /// caps at one relationship hop (checked here, not the grammar, which
2135    /// permissively allows any hop count via the same `patternElem` every
2136    /// other pattern context uses), and real Cypher rejects more than one
2137    /// `ON CREATE`/`ON MATCH` on the same MERGE (also grammar-permissive,
2138    /// `mergeAction*` allows any order/count) -- same "grammar permissive,
2139    /// builder enforces the exact constraint" split used there.
2140    /// `p = ...` named-path capture (unlike `build_create_st`, which still
2141    /// rejects it) is supported here -- MERGE's own pattern is simple
2142    /// enough (at most one hop, no `shortestPath()`, no variable-length
2143    /// hop) that `executor::merge_one_row` can just reuse ordinary MATCH's
2144    /// own `name_pattern_for_path`/`assemble_path` machinery directly, no
2145    /// bespoke path-assembly logic needed.
2146    fn build_merge_st(&mut self, ctx: &MergeStContext) -> Result<MergeClause, QueryError> {
2147        let part_ctx = ctx.patternPart().expect("mergeSt always has a patternPart");
2148        let path_var = if part_ctx.ASSIGN().is_some() {
2149            let symbol_ctx = part_ctx
2150                .symbol()
2151                .expect("patternPart with ASSIGN always has a symbol");
2152            Some(symbol_text(&symbol_ctx))
2153        } else {
2154            None
2155        };
2156        if part_ctx.shortestPathWrapper().is_some() {
2157            return Err(QueryError::Syntax(
2158                "shortestPath() isn't valid in MERGE".into(),
2159            ));
2160        }
2161        let elem_ctx = part_ctx
2162            .patternElem()
2163            .expect("patternPart always has a patternElem when shortestPathWrapper is absent");
2164        let pattern = self.visit(&*elem_ctx).into_pattern()?;
2165        if pattern.hops.len() > 1 {
2166            return Err(QueryError::Syntax(
2167                "MERGE with more than one relationship hop isn't supported yet — split it into a MATCH \
2168                 for the already-known part and a MERGE for one new hop"
2169                    .into(),
2170            ));
2171        }
2172
2173        let mut on_create = Vec::new();
2174        let mut on_match = Vec::new();
2175        for action_ctx in ctx.mergeAction_all() {
2176            let set_items = self.build_merge_action(&action_ctx)?;
2177            if action_ctx.MATCH().is_some() {
2178                if !on_match.is_empty() {
2179                    return Err(QueryError::Syntax(
2180                        "MERGE can have at most one ON MATCH SET clause".into(),
2181                    ));
2182                }
2183                on_match = set_items;
2184            } else {
2185                if !on_create.is_empty() {
2186                    return Err(QueryError::Syntax(
2187                        "MERGE can have at most one ON CREATE SET clause".into(),
2188                    ));
2189                }
2190                on_create = set_items;
2191            }
2192        }
2193
2194        Ok(MergeClause {
2195            pattern,
2196            path_var,
2197            on_create,
2198            on_match,
2199            with: None,
2200        })
2201    }
2202
2203    fn build_merge_action(
2204        &mut self,
2205        ctx: &MergeActionContextAll,
2206    ) -> Result<Vec<SetItem>, QueryError> {
2207        let set_ctx = ctx.setSt().expect("mergeAction always has a setSt");
2208        self.build_set_st(&set_ctx)
2209    }
2210
2211    /// `readingStatement : matchSt | unwindSt | queryCallSt`. `matchSt` can
2212    /// expand to more than one `QueryClause::Match` (comma-separated
2213    /// disjoint patterns splice into separate `QueryPart`s -- see
2214    /// `build_match_st`'s docs), so this appends rather than returning a
2215    /// single clause. `queryCallSt` (`CALL proc(...) YIELD ...` used as a
2216    /// reading clause) builds a `QueryClause::Call` -- unlike
2217    /// `standaloneCall`'s own grammar rule, this one's `parenExpressionChain`
2218    /// is mandatory (no implicit-argument form in-query) and its `YIELD`
2219    /// has no `*` alternative (only `yieldItems`), see `CallClause`'s own
2220    /// docs.
2221    fn append_reading_statement(
2222        &mut self,
2223        ctx: &ReadingStatementContextAll,
2224        clauses: &mut Vec<QueryClause>,
2225    ) -> Result<(), QueryError> {
2226        if let Some(match_ctx) = ctx.matchSt() {
2227            let parts = self.visit(&*match_ctx).into_query_parts()?;
2228            clauses.extend(parts.into_iter().map(QueryClause::Match));
2229            return Ok(());
2230        }
2231        if let Some(unwind_ctx) = ctx.unwindSt() {
2232            let clause = self.visit(&*unwind_ctx).into_unwind_clause()?;
2233            clauses.push(QueryClause::Unwind(clause));
2234            return Ok(());
2235        }
2236        let call_ctx = ctx
2237            .queryCallSt()
2238            .expect("readingStatement is matchSt | unwindSt | queryCallSt");
2239        let call = self.build_query_call_st(&call_ctx)?;
2240        clauses.push(QueryClause::Call(call));
2241        Ok(())
2242    }
2243
2244    /// `updatingStatement : createSt | mergeSt | deleteSt | setSt |
2245    /// removeSt`, used where it's just another clause in the sequence (not
2246    /// the statement's final tail -- see `build_mutating_tail` for that
2247    /// position instead).
2248    fn build_updating_statement_as_clause(
2249        &mut self,
2250        ctx: &UpdatingStatementContextAll,
2251    ) -> Result<QueryClause, QueryError> {
2252        if let Some(create_ctx) = ctx.createSt() {
2253            return Ok(QueryClause::Create(
2254                self.visit(&*create_ctx).into_create_patterns()?,
2255            ));
2256        }
2257        if let Some(merge_ctx) = ctx.mergeSt() {
2258            return Ok(QueryClause::Merge(
2259                self.visit(&*merge_ctx).into_merge_clause()?,
2260            ));
2261        }
2262        if let Some(delete_ctx) = ctx.deleteSt() {
2263            let d = self.visit(&*delete_ctx).into_delete_items()?;
2264            return Ok(QueryClause::Delete {
2265                items: d.items,
2266                detach: d.detach,
2267            });
2268        }
2269        if let Some(set_ctx) = ctx.setSt() {
2270            return Ok(QueryClause::Set(self.visit(&*set_ctx).into_set_items()?));
2271        }
2272        let remove_ctx = ctx
2273            .removeSt()
2274            .expect("updatingStatement always has one of its 5 alternatives");
2275        Ok(QueryClause::Remove(
2276            self.visit(&*remove_ctx).into_remove_items()?,
2277        ))
2278    }
2279
2280    /// The statement's final mutating clause (`createSt`/`deleteSt`/
2281    /// `setSt`/`removeSt` -- never `mergeSt`, which has no `Tail` variant
2282    /// at all and always becomes a `QueryClause::Merge` entry even when
2283    /// it's last, per `Statement::Match`'s own "missing tail is only valid
2284    /// with a MERGE clause" rule) folds into a `Tail::X(_, Option
2285    /// <ReturnTail>)`, consuming an optional trailing `returnSt` as a
2286    /// narrower `ReturnTail` (items + distinct only, matching pest's
2287    /// `ReturnTail`, which has no other fields either). `RETURN *` isn't
2288    /// supported in this position (`ReturnTail` has no star-resolution
2289    /// site -- mirrors `parser.rs`'s `parse_mutating_tail`, same
2290    /// real restriction there too, confirmed via the TCK). ORDER BY/SKIP/
2291    /// LIMIT, though, are NOT restricted here (an earlier version of this
2292    /// function wrongly rejected them, found via a full TCK parse-parity
2293    /// run -- `MATCH (n) DELETE n RETURN 42 LIMIT 0` is real, TCK-tested
2294    /// Cypher) -- returned to the caller instead, which places them on the
2295    /// *statement's* own `order_by`/`skip`/`limit` fields, same as pest:
2296    /// its `mutating_tail` rule has no order/skip/limit slot of its own at
2297    /// all, they're siblings of `tail_clause` at `match_stmt`'s own level
2298    /// (`clause* ~ tail_clause? ~ order_by_clause? ~ skip_clause? ~
2299    /// limit_clause?`), applying regardless of which `Tail` variant is
2300    /// active. This grammar just nests them inside `returnSt`'s own
2301    /// `projectionBody` structurally instead of keeping them as separate
2302    /// statement-level siblings -- same semantics, different grammar shape.
2303    #[allow(clippy::type_complexity)]
2304    fn build_mutating_tail(
2305        &mut self,
2306        ctx: &UpdatingStatementContextAll,
2307        return_ctx: Option<&ReturnStContext>,
2308    ) -> Result<
2309        (
2310            Tail,
2311            Option<Vec<(ReturnExpr, SortDir)>>,
2312            Option<ReturnExpr>,
2313            Option<ReturnExpr>,
2314        ),
2315        QueryError,
2316    > {
2317        let mut order_by = None;
2318        let mut skip = None;
2319        let mut limit = None;
2320        let ret = match return_ctx {
2321            Some(return_ctx) => {
2322                let c = self.visit(return_ctx).into_return_clause()?;
2323                order_by = c.order_by;
2324                skip = c.skip;
2325                limit = c.limit;
2326                let Tail::Return(items, distinct) = c.tail else {
2327                    return Err(QueryError::Syntax(
2328                        "RETURN * isn't supported as a mutating clause's own trailing RETURN"
2329                            .into(),
2330                    ));
2331                };
2332                Some(ReturnTail { items, distinct })
2333            }
2334            None => None,
2335        };
2336        let tail = if let Some(create_ctx) = ctx.createSt() {
2337            Tail::Create(self.visit(&*create_ctx).into_create_patterns()?, ret)
2338        } else if let Some(delete_ctx) = ctx.deleteSt() {
2339            let d = self.visit(&*delete_ctx).into_delete_items()?;
2340            if d.detach {
2341                Tail::DetachDelete(d.items, ret)
2342            } else {
2343                Tail::Delete(d.items, ret)
2344            }
2345        } else if let Some(set_ctx) = ctx.setSt() {
2346            Tail::Set(self.visit(&*set_ctx).into_set_items()?, ret)
2347        } else {
2348            let remove_ctx = ctx
2349                .removeSt()
2350                .expect("build_mutating_tail's caller already excluded mergeSt");
2351            Tail::Remove(self.visit(&*remove_ctx).into_remove_items()?, ret)
2352        };
2353        Ok((tail, order_by, skip, limit))
2354    }
2355
2356    /// `singlePartQ : readingStatement* (returnSt | updatingStatement+
2357    /// returnSt?)`. No WITH chaining at this level at all (that's
2358    /// `multiPartQ`'s job, not yet wired up -- see this file's module
2359    /// doc). Mirrors `parser.rs`'s `parse_match_stmt` for the no-WITH
2360    /// case: leading reading statements become `QueryClause`s; either a
2361    /// bare `returnSt` becomes the statement's `Tail::Return`/`ReturnStar`
2362    /// (with ORDER BY/SKIP/LIMIT at the statement level, where they
2363    /// belong for this form), or the *last* updating statement becomes the
2364    /// tail (see `build_mutating_tail`) with every earlier one just
2365    /// another `QueryClause`, unless that last one is `mergeSt` (never a
2366    /// tail -- see that function's docs), in which case a trailing
2367    /// `returnSt`, if present, becomes the statement's own `Tail::Return`
2368    /// instead.
2369    fn build_single_part_q(&mut self, ctx: &SinglePartQContext) -> Result<Statement, QueryError> {
2370        let mut clauses = Vec::new();
2371        for rs_ctx in ctx.readingStatement_all() {
2372            self.append_reading_statement(&rs_ctx, &mut clauses)?;
2373        }
2374
2375        let updating = ctx.updatingStatement_all();
2376        let return_ctx = ctx.returnSt();
2377
2378        // Bare `CREATE (...)` with nothing else at all (no leading MATCH/
2379        // UNWIND, no trailing RETURN, no other updating clause) -- mirrors
2380        // pest's `create_stmt_only` (`create_stmt ~ !(return_clause |
2381        // chainable_clause_follows)`), producing a real `Statement::Create`
2382        // directly instead of wrapping in `Statement::Match` with a
2383        // `Tail::Create`. Found via a Phase 3 dry-run behavioral test
2384        // failure (`explain_never_mutates_even_a_write_statement`):
2385        // `explain.rs`'s "no query plan" output depends on this exact
2386        // shape distinction, not just equivalent semantics.
2387        if clauses.is_empty() && return_ctx.is_none() && updating.len() == 1 {
2388            if let Some(create_ctx) = updating[0].createSt() {
2389                let patterns = self.visit(&*create_ctx).into_create_patterns()?;
2390                return Ok(Statement::Create(patterns));
2391            }
2392        }
2393
2394        let mut tail = None;
2395        let mut order_by = None;
2396        let mut skip = None;
2397        let mut limit = None;
2398        let mut consumed_return = false;
2399
2400        if let Some((last, earlier)) = updating.split_last() {
2401            for us_ctx in earlier {
2402                clauses.push(self.build_updating_statement_as_clause(us_ctx)?);
2403            }
2404            if last.mergeSt().is_some() {
2405                clauses.push(self.build_updating_statement_as_clause(last)?);
2406            } else {
2407                let (t, ob, sk, lim) = self.build_mutating_tail(last, return_ctx.as_deref())?;
2408                tail = Some(t);
2409                order_by = ob;
2410                skip = sk;
2411                limit = lim;
2412                consumed_return = return_ctx.is_some();
2413            }
2414        }
2415
2416        if !consumed_return {
2417            if let Some(return_ctx) = return_ctx {
2418                let c = self.visit(&*return_ctx).into_return_clause()?;
2419                tail = Some(c.tail);
2420                order_by = c.order_by;
2421                skip = c.skip;
2422                limit = c.limit;
2423            }
2424        }
2425
2426        if tail.is_none() && !clauses.iter().any(|c| matches!(c, QueryClause::Merge(_))) {
2427            return Err(QueryError::Syntax(
2428                "a query needs a RETURN/DELETE/SET tail, unless it has a MERGE clause with nothing after it".into(),
2429            ));
2430        }
2431
2432        Ok(Statement::Match {
2433            clauses,
2434            tail,
2435            order_by,
2436            skip: skip.map(Box::new),
2437            limit: limit.map(Box::new),
2438        })
2439    }
2440
2441    /// `multiPartQ : readingStatement* ((readingStatement | updatingStatement)*
2442    /// withSt)+ singlePartQ` -- one or more WITH boundaries, each preceded by
2443    /// zero or more reading/updating statements, followed by a final
2444    /// `singlePartQ` (itself another `readingStatement*` run plus the
2445    /// statement's real tail). The grammar's typed accessors
2446    /// (`readingStatement_all`/`updatingStatement_all`/`withSt_all`) each
2447    /// flatten across every group, losing which items came before which
2448    /// `withSt` -- recovered by sorting all three by source position
2449    /// (`start().get_token_index()`) instead of walking raw children (which
2450    /// would need runtime downcasting to tell a `readingStatement` from an
2451    /// `updatingStatement` from a `withSt`).
2452    ///
2453    /// A `withSt` attaches to the immediately preceding MATCH/UNWIND/MERGE
2454    /// clause's own `with` field (only the *last* one, for a comma
2455    /// cross-join `matchSt`) -- same as `parser.rs`'s `parse_match_part`/
2456    /// `parse_merge_clause`/`parse_unwind_clause`. If nothing attachable
2457    /// immediately precedes it (statement-leading, or right after a
2458    /// SET/DELETE/REMOVE/CREATE -- none of which have a `with` field on
2459    /// their `QueryClause` variant -- or right after another `withSt`), it
2460    /// becomes its own standalone `QueryClause::With` entry, mirroring
2461    /// pest's `clause = { ... | with_clause | ... }` alternative.
2462    fn build_multi_part_q(&mut self, ctx: &MultiPartQContext) -> Result<Statement, QueryError> {
2463        enum Item<'i> {
2464            Reading(Rc<ReadingStatementContextAll<'i>>),
2465            Updating(Rc<UpdatingStatementContextAll<'i>>),
2466            With(Rc<WithStContext<'i>>),
2467        }
2468        let mut items: Vec<(isize, Item)> = Vec::new();
2469        for rs in ctx.readingStatement_all() {
2470            let idx = rs.start().get_token_index();
2471            items.push((idx, Item::Reading(rs)));
2472        }
2473        for us in ctx.updatingStatement_all() {
2474            let idx = us.start().get_token_index();
2475            items.push((idx, Item::Updating(us)));
2476        }
2477        for w in ctx.withSt_all() {
2478            let idx = w.start().get_token_index();
2479            items.push((idx, Item::With(w)));
2480        }
2481        items.sort_by_key(|(idx, _)| *idx);
2482
2483        let mut clauses: Vec<QueryClause> = Vec::new();
2484        let mut attach_target: Option<usize> = None;
2485        for (_, item) in items {
2486            match item {
2487                Item::Reading(rs) => {
2488                    self.append_reading_statement(&rs, &mut clauses)?;
2489                    attach_target = Some(clauses.len() - 1);
2490                }
2491                Item::Updating(us) => {
2492                    let clause = self.build_updating_statement_as_clause(&us)?;
2493                    let can_attach = matches!(clause, QueryClause::Merge(_));
2494                    clauses.push(clause);
2495                    attach_target = can_attach.then_some(clauses.len() - 1);
2496                }
2497                Item::With(w) => {
2498                    let with = self.visit(&*w).into_with_clause()?;
2499                    match attach_target.take() {
2500                        Some(i) => match &mut clauses[i] {
2501                            QueryClause::Match(part) => part.with = Some(with),
2502                            QueryClause::Unwind(u) => u.with = Some(with),
2503                            QueryClause::Merge(m) => m.with = Some(with),
2504                            QueryClause::Call(call) => call.with = Some(with),
2505                            _ => unreachable!(
2506                                "attach_target is only ever set right after pushing a Match/Unwind/Merge/Call clause"
2507                            ),
2508                        },
2509                        None => clauses.push(QueryClause::With(with)),
2510                    }
2511                }
2512            }
2513        }
2514
2515        let sp_ctx = ctx
2516            .singlePartQ()
2517            .expect("multiPartQ always ends in a singlePartQ");
2518        // `build_single_part_q` can also return a bare `Statement::Create`
2519        // directly (its own "CREATE with nothing else at all" special
2520        // case, mirroring pest's `create_stmt_only`) -- but nested inside
2521        // a `multiPartQ` (past at least one `WITH` boundary already),
2522        // that's still just this statement's final `Tail::Create`, same
2523        // as an ordinary trailing `CREATE` would be. Only a genuinely
2524        // top-level, whole-statement bare CREATE gets the dedicated
2525        // `Statement::Create` shape (`explain.rs`'s "no query plan" case).
2526        let (tail_clauses, tail, order_by, skip, limit) =
2527            match self.build_single_part_q(&sp_ctx)? {
2528                Statement::Match {
2529                    clauses,
2530                    tail,
2531                    order_by,
2532                    skip,
2533                    limit,
2534                } => (clauses, tail, order_by, skip, limit),
2535                Statement::Create(patterns) => {
2536                    (Vec::new(), Some(Tail::Create(patterns, None)), None, None, None)
2537                }
2538                other => unreachable!(
2539                    "build_single_part_q only ever returns Statement::Match or Statement::Create, got {other:?}"
2540                ),
2541            };
2542        clauses.extend(tail_clauses);
2543        Ok(Statement::Match {
2544            clauses,
2545            tail,
2546            order_by,
2547            skip,
2548            limit,
2549        })
2550    }
2551
2552    /// `explainSt : EXPLAIN (createIndexSt | regularQuery)` -- mars-specific
2553    /// grammar extension (this file's own local addition, not from
2554    /// upstream `antlr/grammars-v4/cypher`; see `grammar/README.md`), no
2555    /// real openCypher equivalent. Mirrors `parser.rs`'s `parse_explain_stmt`.
2556    fn build_explain_st(&mut self, ctx: &ExplainStContext) -> Result<Statement, QueryError> {
2557        let inner = match ctx.createIndexSt() {
2558            Some(ci_ctx) => self.build_create_index_st(&ci_ctx)?,
2559            None => {
2560                let rq_ctx = ctx
2561                    .regularQuery()
2562                    .expect("explainSt always has a createIndexSt or regularQuery");
2563                self.visit(&*rq_ctx).into_statement()?
2564            }
2565        };
2566        Ok(Statement::Explain(Box::new(inner)))
2567    }
2568
2569    /// `createIndexSt : CREATE INDEX ON COLON name LPAREN name RPAREN
2570    /// UNIQUE?` -- same mars-specific-extension caveat as `build_explain_st`
2571    /// above. Mirrors `parser.rs`'s `parse_create_index_stmt`; `name_all()`
2572    /// returns the label then the property name in source order (the only
2573    /// two `name` children this rule ever has).
2574    fn build_create_index_st(
2575        &mut self,
2576        ctx: &CreateIndexStContext,
2577    ) -> Result<Statement, QueryError> {
2578        let names = ctx.name_all();
2579        let label = name_text(
2580            names
2581                .first()
2582                .expect("createIndexSt always has a label name"),
2583        );
2584        let prop = name_text(
2585            names
2586                .get(1)
2587                .expect("createIndexSt always has a property name"),
2588        );
2589        Ok(Statement::CreateIndex {
2590            label,
2591            prop,
2592            unique: ctx.UNIQUE().is_some(),
2593        })
2594    }
2595
2596    /// `regularQuery : singleQuery unionSt*`. No `unionSt` at all just
2597    /// passes the single `Statement` straight through -- `singleQuery`
2598    /// itself (`singlePartQ | multiPartQ`) needs no override, default
2599    /// dispatch already routes to whichever of those two produced the
2600    /// `Statement`. Otherwise mirrors `parser.rs`'s `parse_union_stmt`:
2601    /// every `unionSt`'s `ALL` presence must agree (real Cypher rejects
2602    /// mixing bare `UNION` and `UNION ALL` in one statement), checked here
2603    /// rather than in the grammar since it's only knowable once every
2604    /// occurrence is in hand.
2605    fn build_regular_query(&mut self, ctx: &RegularQueryContext) -> Result<Statement, QueryError> {
2606        let sq_ctx = ctx
2607            .singleQuery()
2608            .expect("regularQuery always has a singleQuery");
2609        let first = self.visit(&*sq_ctx).into_statement()?;
2610        let unions = ctx.unionSt_all();
2611        if unions.is_empty() {
2612            return Ok(first);
2613        }
2614        let mut parts = vec![first];
2615        let mut all: Option<bool> = None;
2616        for u_ctx in unions {
2617            let this_all = u_ctx.ALL().is_some();
2618            match all {
2619                None => all = Some(this_all),
2620                Some(prev) if prev != this_all => {
2621                    return Err(QueryError::Syntax(
2622                        "can't mix UNION and UNION ALL in the same statement".into(),
2623                    ));
2624                }
2625                Some(_) => {}
2626            }
2627            let part_sq = u_ctx
2628                .singleQuery()
2629                .expect("unionSt always has a singleQuery");
2630            parts.push(self.visit(&*part_sq).into_statement()?);
2631        }
2632        Ok(Statement::Union {
2633            parts,
2634            all: all.unwrap_or(false),
2635        })
2636    }
2637}
2638
2639/// The real implementation behind `lib.rs`'s public `parse` -- the
2640/// pest-based `parser.rs`/`cypher.pest` this replaced are gone (see
2641/// `grammar/README.md`).
2642pub fn parse_antlr(input: &str) -> Result<Statement, QueryError> {
2643    // Session-transaction extension (`Statement::Begin`'s docs):
2644    // recognized before the grammar runs. A whole statement that is
2645    // exactly one of these keyword forms (case-insensitive, any
2646    // whitespace between words, optional trailing `;` -- `script :
2647    // query SEMI? EOF` tolerates one the same way) can never be valid
2648    // Cypher otherwise, so this can't shadow anything the grammar would
2649    // have accepted. `BEGIN TRANSACTION` is an accepted alias for
2650    // `BEGIN` -- the two-word form is what other embedded graph
2651    // engines' Cypher dialects use, and rejecting it over one word
2652    // would be pure friction. (No `READ ONLY` variant: MarsDB has no
2653    // read-only session transactions -- reads outside a transaction
2654    // already run on their own snapshots.)
2655    let trimmed = input.trim();
2656    let trimmed = trimmed
2657        .strip_suffix(';')
2658        .map(str::trim_end)
2659        .unwrap_or(trimmed);
2660    let mut words = trimmed.split_whitespace();
2661    match (words.next(), words.next(), words.next()) {
2662        (Some(begin), rest, None)
2663            if begin.eq_ignore_ascii_case("BEGIN")
2664                && rest.is_none_or(|w| w.eq_ignore_ascii_case("TRANSACTION")) =>
2665        {
2666            return Ok(Statement::Begin);
2667        }
2668        (Some(commit), None, None) if commit.eq_ignore_ascii_case("COMMIT") => {
2669            return Ok(Statement::Commit);
2670        }
2671        (Some(rollback), None, None) if rollback.eq_ignore_ascii_case("ROLLBACK") => {
2672            return Ok(Statement::Rollback);
2673        }
2674        _ => {}
2675    }
2676    use crate::generated::cypherlexer::CypherLexer;
2677    use crate::generated::cypherparser::{CypherParser, ScriptContextAttrs};
2678    use antlr4rust::common_token_stream::CommonTokenStream;
2679    use antlr4rust::error_listener::ErrorListener;
2680    use antlr4rust::recognizer::Recognizer;
2681    use antlr4rust::token_factory::TokenFactory;
2682    use antlr4rust::InputStream;
2683    use antlr4rust::Parser as _;
2684    use std::cell::RefCell;
2685
2686    struct CollectErrors(Rc<RefCell<Vec<String>>>);
2687    impl<'a, T: Recognizer<'a>> ErrorListener<'a, T> for CollectErrors {
2688        fn syntax_error(
2689            &self,
2690            _recognizer: &T,
2691            _offending_symbol: Option<&<T::TF as TokenFactory<'a>>::Inner>,
2692            line: isize,
2693            column: isize,
2694            msg: &str,
2695            _e: Option<&antlr4rust::errors::ANTLRError>,
2696        ) {
2697            self.0
2698                .borrow_mut()
2699                .push(format!("line {line}:{column} {msg}"));
2700        }
2701    }
2702
2703    let errors = Rc::new(RefCell::new(Vec::new()));
2704    let stream = InputStream::new(input);
2705    let mut lexer = CypherLexer::new(stream);
2706    lexer.remove_error_listeners();
2707    lexer.add_error_listener(Box::new(CollectErrors(errors.clone())));
2708    let tokens = CommonTokenStream::new(lexer);
2709    let mut parser = CypherParser::new(tokens);
2710    parser.remove_error_listeners();
2711    parser.add_error_listener(Box::new(CollectErrors(errors.clone())));
2712    let ctx = parser
2713        .script()
2714        .map_err(|e| QueryError::Syntax(e.to_string()))?;
2715    if let Some(msg) = errors.borrow().first() {
2716        return Err(QueryError::Syntax(format!("syntax error: {msg}")));
2717    }
2718    // `script : query SEMI? EOF` -- visiting the whole tree would run
2719    // straight into the default `aggregate_results`' unconditional
2720    // "last child wins" rule (not "last *non-default*", despite this
2721    // file's other alternation rules getting away with relying on that
2722    // distinction -- see this function's own docs): the trailing `EOF`
2723    // terminal has no `visit_X` hook of its own, so it'd overwrite
2724    // `query`'s real result with `AstNode::None`. Visiting `query`
2725    // directly sidesteps it -- `script`'s own job (rejecting trailing
2726    // garbage after a valid query) is already done by the `parser.script()`
2727    // call above succeeding.
2728    let query_ctx = ctx.query().expect("script always has a query");
2729    AstBuilder::new().visit(&*query_ctx).into_statement()
2730}
2731
2732/// The real implementation behind `lib.rs`'s public `parse_many` --
2733/// parses a `;`-separated batch of one or more statements (`"CREATE (a);
2734/// CREATE (b); MATCH (n) RETURN n"`). Splits the input into individual
2735/// statements itself (`split_statements`, respecting Cypher's quoting
2736/// rules) and parses each one independently via `parse_antlr`, rather
2737/// than parsing the whole batch as one shared ANTLR tree the way the
2738/// grammar's own `queries : query (SEMI query)* EOF` rule (a
2739/// mars-specific extension, see `grammar/README.md`) would: building one
2740/// tree for a large batch means every statement's tree is alive in
2741/// memory simultaneously until the last one is converted to a
2742/// lightweight `Statement` and the whole tree can finally drop.
2743/// Confirmed via `/usr/bin/time -l`: a real 29MB/9,771-statement import
2744/// script peaked at 13GB RSS in the parse step alone (before any
2745/// execution) parsed the old way. Splitting first means only the
2746/// *largest single statement's* tree is ever alive at once.
2747///
2748/// Also strips a single genuinely-trailing `;` first, same as before --
2749/// `script : query SEMI? EOF` (what `parse_antlr` uses per statement)
2750/// already tolerates one, but stripping it here first keeps
2751/// `split_statements` from ever seeing a trailing empty segment.
2752pub fn parse_antlr_many(input: &str) -> Result<Vec<Statement>, QueryError> {
2753    let trimmed = input.trim_end();
2754    let trimmed = trimmed.strip_suffix(';').unwrap_or(trimmed);
2755    split_statements(trimmed)
2756        .into_iter()
2757        .map(parse_antlr)
2758        .collect()
2759}
2760
2761/// Splits `;`-separated statement text into individual statement slices
2762/// without building any parse tree -- a `;` inside a single-quoted
2763/// (`'...'`), double-quoted (`"..."`), or backtick-quoted (`` `...` ``)
2764/// region is never treated as a separator, matching exactly what the
2765/// lexer's own `CHAR_LITERAL`/`STRING_LITERAL`/`ESC_LITERAL` rules
2766/// consider part of the literal (see `grammar/CypherLexer.g4`).
2767/// Backtick-quoted identifiers have no escape sequences in this grammar
2768/// (`ESC_LITERAL : '`' .*? '`'`) -- a backslash there is just a literal
2769/// character, not an escape introducer, unlike inside the other two.
2770/// Doesn't validate escape sequences itself (that's `parse_antlr`'s job
2771/// once each slice is actually parsed) -- only tracks "am I currently
2772/// inside a quoted region" well enough to find the real separators.
2773pub fn split_statements(input: &str) -> Vec<&str> {
2774    let bytes = input.as_bytes();
2775    let mut starts = vec![0usize];
2776    let mut semicolons = Vec::new();
2777    let mut quote: Option<u8> = None;
2778    let mut i = 0;
2779    while i < bytes.len() {
2780        let b = bytes[i];
2781        match quote {
2782            Some(q) => {
2783                if b == b'\\' && q != b'`' {
2784                    i += 1; // skip the escaped character too
2785                } else if b == q {
2786                    quote = None;
2787                }
2788            }
2789            None => match b {
2790                b'\'' | b'"' | b'`' => quote = Some(b),
2791                b';' => {
2792                    semicolons.push(i);
2793                    starts.push(i + 1);
2794                }
2795                _ => {}
2796            },
2797        }
2798        i += 1;
2799    }
2800    starts
2801        .iter()
2802        .enumerate()
2803        .map(|(idx, &start)| {
2804            let end = semicolons.get(idx).copied().unwrap_or(bytes.len());
2805            &input[start..end]
2806        })
2807        .collect()
2808}
2809
2810/// `where`'s grammar reuses the same `expression` rule as everywhere else
2811/// (unlike pest, which has a separate, narrower `with_expr` grammar chain
2812/// building `WithExpr` directly) -- so a full `ReturnExpr` has to be built
2813/// first and then folded down into `WithExpr` here. Only the variants with
2814/// an exact `WithExpr` counterpart (`And`/`Or`/`Not`/`Compare`/`IsNull`)
2815/// unwrap recursively; everything else (including `Xor`, which `WithExpr`
2816/// has no variant for at all) becomes `Bare` -- `WithExpr::Bare`'s own
2817/// docs already cover "any boolean-valued expression used directly as a
2818/// predicate", which this falls under regardless of its exact shape.
2819fn return_expr_to_with_expr(expr: ReturnExpr) -> WithExpr {
2820    match expr {
2821        ReturnExpr::And(l, r) => WithExpr::And(
2822            Box::new(return_expr_to_with_expr(*l)),
2823            Box::new(return_expr_to_with_expr(*r)),
2824        ),
2825        ReturnExpr::Or(l, r) => WithExpr::Or(
2826            Box::new(return_expr_to_with_expr(*l)),
2827            Box::new(return_expr_to_with_expr(*r)),
2828        ),
2829        ReturnExpr::Not(inner) => WithExpr::Not(Box::new(return_expr_to_with_expr(*inner))),
2830        ReturnExpr::Compare(l, op, r) => WithExpr::Compare(*l, op, *r),
2831        ReturnExpr::IsNull(inner) => WithExpr::IsNull(*inner),
2832        other => WithExpr::Bare(other),
2833    }
2834}
2835
2836/// `matchSt`'s `where`, like `withSt`'s, reuses the same generic
2837/// `expression` rule as everywhere else (unlike pest, which has dedicated
2838/// narrower grammar rules -- `comparison`/`general_comparison`/
2839/// `label_predicate`/`var_compare` -- picking the right `Expr` variant
2840/// directly at parse time). So the same fold-down-after-the-fact approach
2841/// as `return_expr_to_with_expr` applies here too, just against `Expr`'s
2842/// wider shape: a `Compare` between two bare `Prop`s becomes `PropCompare`,
2843/// a `Prop` compared to a `Lit` keeps the planner-fusable `Compare` variant
2844/// pest's `comparison` rule reserves for exactly that shape, two bare
2845/// `Var`s becomes identity comparison (`VarEq`/`Not(VarEq)`, matching
2846/// pest's `var_compare`'s restriction to `=`/`<>` — anything else is a real
2847/// error, not a silent `GeneralCompare` fallback, since no ordering exists
2848/// between two nodes/relationships), anything else falls back to
2849/// `GeneralCompare`. Similarly `IsNull` on a bare `Prop` keeps the narrow
2850/// variant, anything else becomes `GeneralIsNull`. `HasLabel` folds
2851/// multiple labels into a `HasLabel` `And` chain exactly like pest's
2852/// `parse_label_predicate`. Everything else becomes `GeneralBare`.
2853fn return_expr_to_expr(expr: ReturnExpr) -> Result<Expr, QueryError> {
2854    Ok(match expr {
2855        ReturnExpr::And(l, r) => Expr::And(
2856            Box::new(return_expr_to_expr(*l)?),
2857            Box::new(return_expr_to_expr(*r)?),
2858        ),
2859        ReturnExpr::Or(l, r) => Expr::Or(
2860            Box::new(return_expr_to_expr(*l)?),
2861            Box::new(return_expr_to_expr(*r)?),
2862        ),
2863        ReturnExpr::Not(inner) => Expr::Not(Box::new(return_expr_to_expr(*inner)?)),
2864        ReturnExpr::Compare(l, op, r) => match (*l, *r) {
2865            (ReturnExpr::Prop(pa), ReturnExpr::Lit(lit)) => Expr::Compare(pa, op, lit),
2866            (ReturnExpr::Prop(pa1), ReturnExpr::Prop(pa2)) => Expr::PropCompare(pa1, op, pa2),
2867            (ReturnExpr::Var(a), ReturnExpr::Var(b)) => match op {
2868                CompareOp::Eq => Expr::VarEq(a, b),
2869                CompareOp::Ne => Expr::Not(Box::new(Expr::VarEq(a, b))),
2870                _ => {
2871                    return Err(QueryError::Syntax(format!(
2872                        "{a} {op:?} {b}: only = and <> are meaningful for comparing two \
2873                         nodes/relationships by identity (no ordering exists between them)"
2874                    )))
2875                }
2876            },
2877            (l, r) => Expr::GeneralCompare(l, op, r),
2878        },
2879        ReturnExpr::IsNull(inner) => match *inner {
2880            ReturnExpr::Prop(pa) => Expr::IsNull(pa),
2881            other => Expr::GeneralIsNull(other),
2882        },
2883        ReturnExpr::HasLabel(var, labels) => {
2884            let mut labels = labels.into_iter();
2885            let first = labels
2886                .next()
2887                .expect("HasLabel always carries at least one label");
2888            labels.fold(Expr::HasLabel(var.clone(), first), |acc, label| {
2889                Expr::And(Box::new(acc), Box::new(Expr::HasLabel(var.clone(), label)))
2890            })
2891        }
2892        ReturnExpr::PatternPredicate(pattern) => Expr::Pattern(pattern),
2893        ReturnExpr::ExistsPattern {
2894            pattern,
2895            where_clause,
2896        } => Expr::Exists {
2897            pattern,
2898            where_clause,
2899        },
2900        ReturnExpr::ExistsSubquery(stmt) => Expr::ExistsSubquery(stmt),
2901        other => Expr::GeneralBare(other),
2902    })
2903}
2904
2905#[cfg(test)]
2906mod tests {
2907    use super::*;
2908    use crate::generated::cypherlexer::CypherLexer;
2909    use crate::generated::cypherparser::CypherParser;
2910    use antlr4rust::common_token_stream::CommonTokenStream;
2911    use antlr4rust::InputStream;
2912
2913    fn parse_literal_expr(input: &str) -> Result<Literal, QueryError> {
2914        let stream = InputStream::new(input);
2915        let lexer = CypherLexer::new(stream);
2916        let tokens = CommonTokenStream::new(lexer);
2917        let mut parser = CypherParser::new(tokens);
2918        let ctx = parser
2919            .literal()
2920            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `literal`: {e:?}"));
2921        AstBuilder::new().visit(&*ctx).into_literal()
2922    }
2923
2924    fn parse_pattern(input: &str) -> Result<Pattern, QueryError> {
2925        let stream = InputStream::new(input);
2926        let lexer = CypherLexer::new(stream);
2927        let tokens = CommonTokenStream::new(lexer);
2928        let mut parser = CypherParser::new(tokens);
2929        let ctx = parser
2930            .patternElem()
2931            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `patternElem`: {e:?}"));
2932        AstBuilder::new().visit(&*ctx).into_pattern()
2933    }
2934
2935    fn parse_match(input: &str) -> Result<Vec<QueryPart>, QueryError> {
2936        let stream = InputStream::new(input);
2937        let lexer = CypherLexer::new(stream);
2938        let tokens = CommonTokenStream::new(lexer);
2939        let mut parser = CypherParser::new(tokens);
2940        let ctx = parser
2941            .matchSt()
2942            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `matchSt`: {e:?}"));
2943        AstBuilder::new().visit(&*ctx).into_query_parts()
2944    }
2945
2946    fn parse_expr(input: &str) -> Result<ReturnExpr, QueryError> {
2947        let stream = InputStream::new(input);
2948        let lexer = CypherLexer::new(stream);
2949        let tokens = CommonTokenStream::new(lexer);
2950        let mut parser = CypherParser::new(tokens);
2951        let ctx = parser
2952            .expression()
2953            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `expression`: {e:?}"));
2954        AstBuilder::new().visit(&*ctx).into_return_expr()
2955    }
2956
2957    fn parse_return(input: &str) -> Result<ParsedReturnClause, QueryError> {
2958        let stream = InputStream::new(input);
2959        let lexer = CypherLexer::new(stream);
2960        let tokens = CommonTokenStream::new(lexer);
2961        let mut parser = CypherParser::new(tokens);
2962        let ctx = parser
2963            .returnSt()
2964            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `returnSt`: {e:?}"));
2965        AstBuilder::new().visit(&*ctx).into_return_clause()
2966    }
2967
2968    fn parse_with(input: &str) -> Result<WithClause, QueryError> {
2969        let stream = InputStream::new(input);
2970        let lexer = CypherLexer::new(stream);
2971        let tokens = CommonTokenStream::new(lexer);
2972        let mut parser = CypherParser::new(tokens);
2973        let ctx = parser
2974            .withSt()
2975            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `withSt`: {e:?}"));
2976        AstBuilder::new().visit(&*ctx).into_with_clause()
2977    }
2978
2979    fn parse_unwind(input: &str) -> Result<UnwindClause, QueryError> {
2980        let stream = InputStream::new(input);
2981        let lexer = CypherLexer::new(stream);
2982        let tokens = CommonTokenStream::new(lexer);
2983        let mut parser = CypherParser::new(tokens);
2984        let ctx = parser
2985            .unwindSt()
2986            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `unwindSt`: {e:?}"));
2987        AstBuilder::new().visit(&*ctx).into_unwind_clause()
2988    }
2989
2990    fn parse_set(input: &str) -> Result<Vec<SetItem>, QueryError> {
2991        let stream = InputStream::new(input);
2992        let lexer = CypherLexer::new(stream);
2993        let tokens = CommonTokenStream::new(lexer);
2994        let mut parser = CypherParser::new(tokens);
2995        let ctx = parser
2996            .setSt()
2997            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `setSt`: {e:?}"));
2998        AstBuilder::new().visit(&*ctx).into_set_items()
2999    }
3000
3001    fn parse_delete(input: &str) -> Result<ParsedDelete, QueryError> {
3002        let stream = InputStream::new(input);
3003        let lexer = CypherLexer::new(stream);
3004        let tokens = CommonTokenStream::new(lexer);
3005        let mut parser = CypherParser::new(tokens);
3006        let ctx = parser
3007            .deleteSt()
3008            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `deleteSt`: {e:?}"));
3009        AstBuilder::new().visit(&*ctx).into_delete_items()
3010    }
3011
3012    fn parse_remove(input: &str) -> Result<Vec<RemoveItem>, QueryError> {
3013        let stream = InputStream::new(input);
3014        let lexer = CypherLexer::new(stream);
3015        let tokens = CommonTokenStream::new(lexer);
3016        let mut parser = CypherParser::new(tokens);
3017        let ctx = parser
3018            .removeSt()
3019            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `removeSt`: {e:?}"));
3020        AstBuilder::new().visit(&*ctx).into_remove_items()
3021    }
3022
3023    fn parse_create(input: &str) -> Result<Vec<Pattern>, QueryError> {
3024        let stream = InputStream::new(input);
3025        let lexer = CypherLexer::new(stream);
3026        let tokens = CommonTokenStream::new(lexer);
3027        let mut parser = CypherParser::new(tokens);
3028        let ctx = parser
3029            .createSt()
3030            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `createSt`: {e:?}"));
3031        AstBuilder::new().visit(&*ctx).into_create_patterns()
3032    }
3033
3034    fn parse_merge(input: &str) -> Result<MergeClause, QueryError> {
3035        let stream = InputStream::new(input);
3036        let lexer = CypherLexer::new(stream);
3037        let tokens = CommonTokenStream::new(lexer);
3038        let mut parser = CypherParser::new(tokens);
3039        let ctx = parser
3040            .mergeSt()
3041            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `mergeSt`: {e:?}"));
3042        AstBuilder::new().visit(&*ctx).into_merge_clause()
3043    }
3044
3045    fn parse_statement(input: &str) -> Result<Statement, QueryError> {
3046        let stream = InputStream::new(input);
3047        let lexer = CypherLexer::new(stream);
3048        let tokens = CommonTokenStream::new(lexer);
3049        let mut parser = CypherParser::new(tokens);
3050        let ctx = parser
3051            .singlePartQ()
3052            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `singlePartQ`: {e:?}"));
3053        AstBuilder::new().visit(&*ctx).into_statement()
3054    }
3055
3056    fn parse_multi_part_statement(input: &str) -> Result<Statement, QueryError> {
3057        let stream = InputStream::new(input);
3058        let lexer = CypherLexer::new(stream);
3059        let tokens = CommonTokenStream::new(lexer);
3060        let mut parser = CypherParser::new(tokens);
3061        let ctx = parser
3062            .multiPartQ()
3063            .unwrap_or_else(|e| panic!("failed to parse {input:?} as `multiPartQ`: {e:?}"));
3064        AstBuilder::new().visit(&*ctx).into_statement()
3065    }
3066
3067    #[test]
3068    fn bool_literals() {
3069        assert_eq!(parse_literal_expr("true").unwrap(), Literal::Bool(true));
3070        assert_eq!(parse_literal_expr("FALSE").unwrap(), Literal::Bool(false));
3071    }
3072
3073    #[test]
3074    fn null_literal() {
3075        assert_eq!(parse_literal_expr("null").unwrap(), Literal::Null);
3076    }
3077
3078    #[test]
3079    fn decimal_int() {
3080        assert_eq!(parse_literal_expr("42").unwrap(), Literal::Int(42));
3081        assert_eq!(parse_literal_expr("007").unwrap(), Literal::Int(7));
3082    }
3083
3084    #[test]
3085    fn hex_and_octal_int() {
3086        assert_eq!(parse_literal_expr("0x1A").unwrap(), Literal::Int(26));
3087        assert_eq!(parse_literal_expr("0o17").unwrap(), Literal::Int(15));
3088    }
3089
3090    // `i64::MIN`'s two's-complement edge case (`-9223372036854775808`) is
3091    // exercised once sign-folding lands at the `unaryAddSubExpression`
3092    // level (see this file's module doc) -- unlike pest's `int_literal`,
3093    // which included an optional leading `-` in the literal token itself,
3094    // this grammar's `literal`/`numLit` never carries a sign at all; `-`
3095    // is strictly `unaryAddSubExpression`'s prefix operator, one level up.
3096    // `parse_int_literal` (reused from `parser.rs`) already handles the
3097    // two's-complement case correctly given a leading `-` in its input --
3098    // that part's covered; only the fold-sign-into-literal-vs-build-a-Neg-
3099    // node decision at the expression level remains.
3100
3101    #[test]
3102    fn float_literals() {
3103        assert_eq!(parse_literal_expr("2.5").unwrap(), Literal::Float(2.5));
3104        assert_eq!(parse_literal_expr("1e10").unwrap(), Literal::Float(1e10));
3105        assert_eq!(parse_literal_expr(".5").unwrap(), Literal::Float(0.5));
3106    }
3107
3108    #[test]
3109    fn float_overflow_errors() {
3110        assert!(parse_literal_expr("1e999").is_err());
3111    }
3112
3113    #[test]
3114    fn string_and_char_literals() {
3115        assert_eq!(
3116            parse_literal_expr("\"hello\"").unwrap(),
3117            Literal::String("hello".to_string())
3118        );
3119        assert_eq!(
3120            parse_literal_expr("'a string with spaces and a hyphen-in-it'").unwrap(),
3121            Literal::String("a string with spaces and a hyphen-in-it".to_string())
3122        );
3123    }
3124
3125    #[test]
3126    fn string_escapes() {
3127        assert_eq!(
3128            parse_literal_expr(r#"'line1\nline2'"#).unwrap(),
3129            Literal::String("line1\nline2".to_string())
3130        );
3131        assert_eq!(
3132            parse_literal_expr(r#"'é'"#).unwrap(),
3133            Literal::String("é".to_string())
3134        );
3135    }
3136
3137    #[test]
3138    fn single_node() {
3139        let p = parse_pattern("(a:Person)").unwrap();
3140        assert_eq!(p.start.var.as_deref(), Some("a"));
3141        assert_eq!(p.start.labels, vec!["Person".to_string()]);
3142        assert!(p.hops.is_empty());
3143    }
3144
3145    #[test]
3146    fn anonymous_node() {
3147        let p = parse_pattern("()").unwrap();
3148        assert_eq!(p.start.var, None);
3149        assert!(p.start.labels.is_empty());
3150    }
3151
3152    #[test]
3153    fn multiple_labels() {
3154        let p = parse_pattern("(a:Person:Employee)").unwrap();
3155        assert_eq!(
3156            p.start.labels,
3157            vec!["Person".to_string(), "Employee".to_string()]
3158        );
3159    }
3160
3161    #[test]
3162    fn escaped_identifier() {
3163        let p = parse_pattern("(`weird name`)").unwrap();
3164        assert_eq!(p.start.var.as_deref(), Some("weird name"));
3165    }
3166
3167    #[test]
3168    fn directions() {
3169        assert_eq!(
3170            parse_pattern("(a)-->(b)").unwrap().hops[0].0.direction,
3171            RelDirection::Right
3172        );
3173        assert_eq!(
3174            parse_pattern("(a)<--(b)").unwrap().hops[0].0.direction,
3175            RelDirection::Left
3176        );
3177        assert_eq!(
3178            parse_pattern("(a)--(b)").unwrap().hops[0].0.direction,
3179            RelDirection::Either
3180        );
3181        // Both arrowheads (`<-...->`) is the same undirected/either shape
3182        // as neither -- regression found via the TCK (Match6 [12]/
3183        // Create2 [20]): used to silently resolve to Left,
3184        // checking LT before GT and never noticing GT was also present.
3185        assert_eq!(
3186            parse_pattern("(a)<-->(b)").unwrap().hops[0].0.direction,
3187            RelDirection::Either
3188        );
3189    }
3190
3191    #[test]
3192    fn rel_type_and_var() {
3193        let p = parse_pattern("(a)-[r:KNOWS]->(b)").unwrap();
3194        let (rel, node) = &p.hops[0];
3195        assert_eq!(rel.var.as_deref(), Some("r"));
3196        assert_eq!(rel.rel_types, vec!["KNOWS".to_string()]);
3197        assert_eq!(node.var.as_deref(), Some("b"));
3198        assert_eq!(rel.hop_range, None);
3199    }
3200
3201    #[test]
3202    fn multiple_rel_types() {
3203        let p = parse_pattern("(a)-[:KNOWS|LIKES]->(b)").unwrap();
3204        assert_eq!(
3205            p.hops[0].0.rel_types,
3206            vec!["KNOWS".to_string(), "LIKES".to_string()]
3207        );
3208    }
3209
3210    #[test]
3211    fn var_length_bounds() {
3212        // Exercises the DIGIT/ID lexer fixes end to end -- `*0`/`*2` used
3213        // to hard-fail before those were fixed upstream.
3214        assert_eq!(
3215            parse_pattern("(a)-[*0]->(b)").unwrap().hops[0].0.hop_range,
3216            Some((0, Some(0)))
3217        );
3218        assert_eq!(
3219            parse_pattern("(a)-[*2]->(b)").unwrap().hops[0].0.hop_range,
3220            Some((2, Some(2)))
3221        );
3222        assert_eq!(
3223            parse_pattern("(a)-[*1..3]->(b)").unwrap().hops[0]
3224                .0
3225                .hop_range,
3226            Some((1, Some(3)))
3227        );
3228        assert_eq!(
3229            parse_pattern("(a)-[*]->(b)").unwrap().hops[0].0.hop_range,
3230            Some((1, None))
3231        );
3232    }
3233
3234    #[test]
3235    fn multi_hop_chain() {
3236        let p = parse_pattern("(a)-[:KNOWS]->(b)<-[:LIKES]-(c)").unwrap();
3237        assert_eq!(p.hops.len(), 2);
3238        assert_eq!(p.hops[0].0.direction, RelDirection::Right);
3239        assert_eq!(p.hops[1].0.direction, RelDirection::Left);
3240    }
3241
3242    #[test]
3243    fn node_pattern_properties() {
3244        let pattern = parse_pattern("(a {name: 'x', age: 1 + 1})").unwrap();
3245        assert_eq!(
3246            pattern.start.props,
3247            vec![
3248                (
3249                    "name".to_string(),
3250                    ReturnExpr::Lit(Literal::String("x".to_string()))
3251                ),
3252                (
3253                    "age".to_string(),
3254                    ReturnExpr::Arith(
3255                        Box::new(ReturnExpr::Lit(Literal::Int(1))),
3256                        ArithOp::Add,
3257                        Box::new(ReturnExpr::Lit(Literal::Int(1))),
3258                    )
3259                ),
3260            ]
3261        );
3262    }
3263
3264    #[test]
3265    fn rel_pattern_properties() {
3266        let pattern = parse_pattern("(a)-[:T {weight: 5}]->(b)").unwrap();
3267        assert_eq!(
3268            pattern.hops[0].0.props,
3269            vec![("weight".to_string(), ReturnExpr::Lit(Literal::Int(5)))]
3270        );
3271    }
3272
3273    #[test]
3274    fn pattern_properties_parameter_not_supported() {
3275        assert!(parse_pattern("(a $props)").is_err());
3276    }
3277
3278    #[test]
3279    fn simple_match() {
3280        let parts = parse_match("MATCH (a:Person)-[:KNOWS]->(b)").unwrap();
3281        assert_eq!(parts.len(), 1);
3282        assert!(!parts[0].optional);
3283        assert_eq!(parts[0].path_var, None);
3284        assert_eq!(parts[0].pattern.start.var.as_deref(), Some("a"));
3285        assert_eq!(parts[0].pattern.hops.len(), 1);
3286    }
3287
3288    #[test]
3289    fn optional_match() {
3290        let parts = parse_match("OPTIONAL MATCH (a)").unwrap();
3291        assert!(parts[0].optional);
3292    }
3293
3294    #[test]
3295    fn named_path() {
3296        let parts = parse_match("MATCH p = (a)-->(b)").unwrap();
3297        assert_eq!(parts.len(), 1);
3298        assert_eq!(parts[0].path_var.as_deref(), Some("p"));
3299    }
3300
3301    #[test]
3302    fn comma_pattern_shared_node_merges_into_one_linear_chain() {
3303        // `(a)-->(b), (b)-->(c)` shares `b` -- one QueryPart, three-node
3304        // chain, not two disjoint ones. Exercises group_into_linear_patterns.
3305        let parts = parse_match("MATCH (a)-->(b), (b)-->(c)").unwrap();
3306        assert_eq!(parts.len(), 1);
3307        assert_eq!(parts[0].pattern.hops.len(), 2);
3308    }
3309
3310    #[test]
3311    fn comma_pattern_disjoint_becomes_multiple_query_parts() {
3312        let parts = parse_match("MATCH (a), (b)").unwrap();
3313        assert_eq!(parts.len(), 2);
3314    }
3315
3316    #[test]
3317    fn named_path_over_disjoint_cross_join_errors() {
3318        assert!(parse_match("MATCH p = (a), (b)").is_err());
3319    }
3320
3321    #[test]
3322    fn shortest_path() {
3323        let parts = parse_match("MATCH shortestPath((a)-[*1..3]->(b))").unwrap();
3324        assert_eq!(parts.len(), 1);
3325        assert!(parts[0].shortest_path);
3326        assert_eq!(parts[0].pattern.hops.len(), 1);
3327    }
3328
3329    #[test]
3330    fn shortest_path_with_named_path_capture() {
3331        let parts = parse_match("MATCH p = shortestPath((a)-[*1..3]->(b))").unwrap();
3332        assert_eq!(parts[0].path_var.as_deref(), Some("p"));
3333        assert!(parts[0].shortest_path);
3334    }
3335
3336    #[test]
3337    fn shortest_path_requires_variable_length_hop() {
3338        assert!(parse_match("MATCH shortestPath((a)-->(b))").is_err());
3339    }
3340
3341    #[test]
3342    fn shortest_path_not_first_in_cross_join_errors() {
3343        assert!(parse_match("MATCH (c), shortestPath((a)-[*1..3]->(b))").is_err());
3344    }
3345
3346    #[test]
3347    fn shortest_path_over_disjoint_cross_join_errors() {
3348        assert!(parse_match("MATCH shortestPath((a)-[*1..3]->(b)), (c)").is_err());
3349    }
3350
3351    #[test]
3352    fn shortest_path_not_valid_in_create() {
3353        assert!(parse_statement("CREATE shortestPath((a)-[*1..3]->(b))").is_err());
3354    }
3355
3356    #[test]
3357    fn shortest_path_not_valid_in_merge() {
3358        assert!(parse_merge("MERGE shortestPath((a)-[*1..3]->(b))").is_err());
3359    }
3360
3361    #[test]
3362    fn named_path_over_a_single_variable_length_hop_is_supported() {
3363        // TCK's Quantifier1-4 [8]/[9] -- a single variable-length hop is
3364        // fine; only *mixing* one with another hop stays rejected (see
3365        // the next test).
3366        let parts = parse_match("MATCH p = (a)-[*1..3]->(b)").unwrap();
3367        assert_eq!(parts[0].path_var.as_deref(), Some("p"));
3368    }
3369
3370    #[test]
3371    fn named_path_over_variable_length_mixed_with_another_hop_is_supported() {
3372        // Was rejected until the `LogicalPlan::VarExpand` edge-isomorphism
3373        // gap was fixed -- see `validate_named_path_pattern`'s docs.
3374        let parts = parse_match("MATCH p = (a)-[*1..3]->(b)-->(c)").unwrap();
3375        assert_eq!(parts[0].path_var.as_deref(), Some("p"));
3376    }
3377
3378    #[test]
3379    fn match_where() {
3380        let parts = parse_match("MATCH (a) WHERE a.x = 1").unwrap();
3381        assert_eq!(parts.len(), 1);
3382        assert!(matches!(
3383            parts[0].where_clause,
3384            Some(Expr::Compare(
3385                PropAccess { .. },
3386                CompareOp::Eq,
3387                Literal::Int(1),
3388            ))
3389        ));
3390    }
3391
3392    #[test]
3393    fn match_where_var_eq() {
3394        let parts = parse_match("MATCH (a), (b) WHERE a = b").unwrap();
3395        assert!(matches!(parts[1].where_clause, Some(Expr::VarEq(_, _))));
3396    }
3397
3398    #[test]
3399    fn match_where_label_predicate() {
3400        let parts = parse_match("MATCH (a) WHERE a:A:B").unwrap();
3401        assert!(matches!(parts[0].where_clause, Some(Expr::And(_, _))));
3402    }
3403
3404    #[test]
3405    fn match_where_pattern_predicate() {
3406        let parts = parse_match("MATCH (n) WHERE (n)-[]->() RETURN n")
3407            .unwrap_or_else(|e| panic!("expected pattern predicate to parse, got {e:?}"));
3408        let Some(Expr::Pattern(pattern)) = &parts[0].where_clause else {
3409            panic!("expected Expr::Pattern");
3410        };
3411        assert_eq!(pattern.hops.len(), 1);
3412    }
3413
3414    #[test]
3415    fn match_where_pattern_predicate_combined_with_and() {
3416        let parts = parse_match("MATCH (n) WHERE (n)-->() AND n.x = 1").unwrap();
3417        let Some(Expr::And(l, r)) = &parts[0].where_clause else {
3418            panic!("expected Expr::And");
3419        };
3420        assert!(matches!(**l, Expr::Pattern(_)));
3421        assert!(matches!(**r, Expr::Compare(..)));
3422    }
3423
3424    #[test]
3425    fn pattern_predicate_outside_where_still_parses() {
3426        // Grammatically legal anywhere an expression is (real Cypher
3427        // restricts it to WHERE) -- parses fine as a ReturnExpr;
3428        // semantic::infer_expr is what rejects it outside a WHERE-folded
3429        // position, at compile time (see that function's own docs).
3430        let expr = parse_expr("(n)-->()").unwrap();
3431        assert!(matches!(expr, ReturnExpr::PatternPredicate(_)));
3432    }
3433
3434    #[test]
3435    fn match_where_on_last_group_of_cross_join() {
3436        let parts = parse_match("MATCH (a), (b) WHERE b.x = 1").unwrap();
3437        assert_eq!(parts.len(), 2);
3438        assert!(parts[0].where_clause.is_none());
3439        assert!(parts[1].where_clause.is_some());
3440    }
3441
3442    #[test]
3443    fn arithmetic_precedence() {
3444        // 1 + 2 * 3 = 7, not 9 -- * binds tighter than +.
3445        assert_eq!(
3446            parse_expr("1 + 2 * 3").unwrap(),
3447            ReturnExpr::Arith(
3448                Box::new(ReturnExpr::Lit(Literal::Int(1))),
3449                ArithOp::Add,
3450                Box::new(ReturnExpr::Arith(
3451                    Box::new(ReturnExpr::Lit(Literal::Int(2))),
3452                    ArithOp::Mul,
3453                    Box::new(ReturnExpr::Lit(Literal::Int(3))),
3454                )),
3455            )
3456        );
3457    }
3458
3459    #[test]
3460    fn arithmetic_left_associative() {
3461        // 10 - 2 - 3 = (10 - 2) - 3 = 5, not 10 - (2 - 3) = 11.
3462        assert_eq!(
3463            parse_expr("10 - 2 - 3").unwrap(),
3464            ReturnExpr::Arith(
3465                Box::new(ReturnExpr::Arith(
3466                    Box::new(ReturnExpr::Lit(Literal::Int(10))),
3467                    ArithOp::Sub,
3468                    Box::new(ReturnExpr::Lit(Literal::Int(2))),
3469                )),
3470                ArithOp::Sub,
3471                Box::new(ReturnExpr::Lit(Literal::Int(3))),
3472            )
3473        );
3474    }
3475
3476    #[test]
3477    fn power_left_associative() {
3478        assert_eq!(
3479            parse_expr("4 ^ 3 ^ 2").unwrap(),
3480            ReturnExpr::Arith(
3481                Box::new(ReturnExpr::Arith(
3482                    Box::new(ReturnExpr::Lit(Literal::Int(4))),
3483                    ArithOp::Pow,
3484                    Box::new(ReturnExpr::Lit(Literal::Int(3))),
3485                )),
3486                ArithOp::Pow,
3487                Box::new(ReturnExpr::Lit(Literal::Int(2))),
3488            )
3489        );
3490    }
3491
3492    #[test]
3493    fn binary_minus_no_whitespace() {
3494        // Exercises the DIGIT-sign-removal grammar fix end to end: `5-1`
3495        // used to tokenize as two adjacent DIGIT tokens with no operator.
3496        assert_eq!(
3497            parse_expr("5-1").unwrap(),
3498            ReturnExpr::Arith(
3499                Box::new(ReturnExpr::Lit(Literal::Int(5))),
3500                ArithOp::Sub,
3501                Box::new(ReturnExpr::Lit(Literal::Int(1))),
3502            )
3503        );
3504    }
3505
3506    #[test]
3507    fn unary_minus_on_variable() {
3508        assert_eq!(
3509            parse_expr("-x").unwrap(),
3510            ReturnExpr::Neg(Box::new(ReturnExpr::Var("x".to_string())))
3511        );
3512    }
3513
3514    #[test]
3515    fn unary_minus_folds_into_literal() {
3516        assert_eq!(parse_expr("-5").unwrap(), ReturnExpr::Lit(Literal::Int(-5)));
3517        assert_eq!(
3518            parse_expr("-5.5").unwrap(),
3519            ReturnExpr::Lit(Literal::Float(-5.5))
3520        );
3521    }
3522
3523    #[test]
3524    fn unary_minus_int_min_two_complement_edge_case() {
3525        // 9223372036854775808 (2^63) doesn't fit in a positive i64 at all
3526        // -- only i64::MIN's magnitude does. Folding the sign directly
3527        // into the literal (rather than building Neg(Lit(Int(...)))) is
3528        // what makes this representable.
3529        assert_eq!(
3530            parse_expr("-9223372036854775808").unwrap(),
3531            ReturnExpr::Lit(Literal::Int(i64::MIN))
3532        );
3533    }
3534
3535    #[test]
3536    fn comparison_chain_folds_into_nested_and() {
3537        // 1 < x < 3 -> (1 < x) AND (x < 3), real Cypher's chained-
3538        // comparison semantics, not a separate AST shape.
3539        assert_eq!(
3540            parse_expr("1 < x < 3").unwrap(),
3541            ReturnExpr::And(
3542                Box::new(ReturnExpr::Compare(
3543                    Box::new(ReturnExpr::Lit(Literal::Int(1))),
3544                    CompareOp::Lt,
3545                    Box::new(ReturnExpr::Var("x".to_string())),
3546                )),
3547                Box::new(ReturnExpr::Compare(
3548                    Box::new(ReturnExpr::Var("x".to_string())),
3549                    CompareOp::Lt,
3550                    Box::new(ReturnExpr::Lit(Literal::Int(3))),
3551                )),
3552            )
3553        );
3554    }
3555
3556    #[test]
3557    fn boolean_operators() {
3558        assert_eq!(
3559            parse_expr("true AND false").unwrap(),
3560            ReturnExpr::And(
3561                Box::new(ReturnExpr::Lit(Literal::Bool(true))),
3562                Box::new(ReturnExpr::Lit(Literal::Bool(false))),
3563            )
3564        );
3565        assert_eq!(
3566            parse_expr("true OR false").unwrap(),
3567            ReturnExpr::Or(
3568                Box::new(ReturnExpr::Lit(Literal::Bool(true))),
3569                Box::new(ReturnExpr::Lit(Literal::Bool(false))),
3570            )
3571        );
3572        assert_eq!(
3573            parse_expr("true XOR false").unwrap(),
3574            ReturnExpr::Xor(
3575                Box::new(ReturnExpr::Lit(Literal::Bool(true))),
3576                Box::new(ReturnExpr::Lit(Literal::Bool(false))),
3577            )
3578        );
3579    }
3580
3581    #[test]
3582    fn double_negation() {
3583        // Exercises the notExpression NOT* grammar fix end to end.
3584        assert_eq!(
3585            parse_expr("NOT NOT true").unwrap(),
3586            ReturnExpr::Not(Box::new(ReturnExpr::Not(Box::new(ReturnExpr::Lit(
3587                Literal::Bool(true)
3588            )))))
3589        );
3590    }
3591
3592    #[test]
3593    fn is_null() {
3594        assert_eq!(
3595            parse_expr("x IS NULL").unwrap(),
3596            ReturnExpr::IsNull(Box::new(ReturnExpr::Var("x".to_string())))
3597        );
3598        assert_eq!(
3599            parse_expr("x IS NOT NULL").unwrap(),
3600            ReturnExpr::Not(Box::new(ReturnExpr::IsNull(Box::new(ReturnExpr::Var(
3601                "x".to_string()
3602            )))))
3603        );
3604    }
3605
3606    #[test]
3607    fn in_operator() {
3608        assert_eq!(
3609            parse_expr("x IN y").unwrap(),
3610            ReturnExpr::In(
3611                Box::new(ReturnExpr::Var("x".to_string())),
3612                Box::new(ReturnExpr::Var("y".to_string())),
3613            )
3614        );
3615    }
3616
3617    #[test]
3618    fn is_null_binds_looser_than_arithmetic() {
3619        // Precedence bug found via a Phase 3 behavioral dry-run: `IS
3620        // NULL`/`IN`/`STARTS WITH` etc must bind above `+`/`-`/`*`/`/`/`^`
3621        // (openCypher.bnf's <comparison predicate> chain), so `x + 0 IS
3622        // NULL` is `(x + 0) IS NULL`, not `x + (0 IS NULL)`.
3623        assert_eq!(
3624            parse_expr("x + 0 IS NULL").unwrap(),
3625            ReturnExpr::IsNull(Box::new(ReturnExpr::Arith(
3626                Box::new(ReturnExpr::Var("x".to_string())),
3627                ArithOp::Add,
3628                Box::new(ReturnExpr::Lit(Literal::Int(0))),
3629            )))
3630        );
3631    }
3632
3633    #[test]
3634    fn in_binds_looser_than_arithmetic_and_operand_can_be_sliced() {
3635        assert_eq!(
3636            parse_expr("3 IN [1, 2, 3][0..2]").unwrap(),
3637            ReturnExpr::In(
3638                Box::new(ReturnExpr::Lit(Literal::Int(3))),
3639                Box::new(ReturnExpr::Slice(
3640                    Box::new(ReturnExpr::ListLit(vec![
3641                        ReturnExpr::Lit(Literal::Int(1)),
3642                        ReturnExpr::Lit(Literal::Int(2)),
3643                        ReturnExpr::Lit(Literal::Int(3)),
3644                    ])),
3645                    Some(Box::new(ReturnExpr::Lit(Literal::Int(0)))),
3646                    Some(Box::new(ReturnExpr::Lit(Literal::Int(2)))),
3647                ))
3648            )
3649        );
3650    }
3651
3652    #[test]
3653    fn starts_with_operand_can_be_an_arithmetic_expression() {
3654        assert_eq!(
3655            parse_expr("x STARTS WITH y + z").unwrap(),
3656            ReturnExpr::Compare(
3657                Box::new(ReturnExpr::Var("x".to_string())),
3658                CompareOp::StartsWith,
3659                Box::new(ReturnExpr::Arith(
3660                    Box::new(ReturnExpr::Var("y".to_string())),
3661                    ArithOp::Add,
3662                    Box::new(ReturnExpr::Var("z".to_string())),
3663                )),
3664            )
3665        );
3666    }
3667
3668    #[test]
3669    fn chained_index_postfix_still_works() {
3670        assert_eq!(
3671            parse_expr("[[1, 2], [3, 4]][0][1]").unwrap(),
3672            ReturnExpr::Index(
3673                Box::new(ReturnExpr::Index(
3674                    Box::new(ReturnExpr::ListLit(vec![
3675                        ReturnExpr::ListLit(vec![
3676                            ReturnExpr::Lit(Literal::Int(1)),
3677                            ReturnExpr::Lit(Literal::Int(2)),
3678                        ]),
3679                        ReturnExpr::ListLit(vec![
3680                            ReturnExpr::Lit(Literal::Int(3)),
3681                            ReturnExpr::Lit(Literal::Int(4)),
3682                        ]),
3683                    ])),
3684                    Box::new(ReturnExpr::Lit(Literal::Int(0))),
3685                )),
3686                Box::new(ReturnExpr::Lit(Literal::Int(1))),
3687            )
3688        );
3689    }
3690
3691    #[test]
3692    fn case_searched_form() {
3693        assert_eq!(
3694            parse_expr("CASE WHEN x > 1 THEN 'big' WHEN x > 0 THEN 'small' ELSE 'none' END")
3695                .unwrap(),
3696            ReturnExpr::Case {
3697                test: None,
3698                whens: vec![
3699                    (
3700                        ReturnExpr::Compare(
3701                            Box::new(ReturnExpr::Var("x".to_string())),
3702                            CompareOp::Gt,
3703                            Box::new(ReturnExpr::Lit(Literal::Int(1))),
3704                        ),
3705                        ReturnExpr::Lit(Literal::String("big".to_string())),
3706                    ),
3707                    (
3708                        ReturnExpr::Compare(
3709                            Box::new(ReturnExpr::Var("x".to_string())),
3710                            CompareOp::Gt,
3711                            Box::new(ReturnExpr::Lit(Literal::Int(0))),
3712                        ),
3713                        ReturnExpr::Lit(Literal::String("small".to_string())),
3714                    ),
3715                ],
3716                else_: Some(Box::new(ReturnExpr::Lit(Literal::String(
3717                    "none".to_string()
3718                )))),
3719            }
3720        );
3721    }
3722
3723    #[test]
3724    fn case_simple_form_with_test_no_else() {
3725        assert_eq!(
3726            parse_expr("CASE x WHEN 1 THEN 'one' WHEN 2 THEN 'two' END").unwrap(),
3727            ReturnExpr::Case {
3728                test: Some(Box::new(ReturnExpr::Var("x".to_string()))),
3729                whens: vec![
3730                    (
3731                        ReturnExpr::Lit(Literal::Int(1)),
3732                        ReturnExpr::Lit(Literal::String("one".to_string())),
3733                    ),
3734                    (
3735                        ReturnExpr::Lit(Literal::Int(2)),
3736                        ReturnExpr::Lit(Literal::String("two".to_string())),
3737                    ),
3738                ],
3739                else_: None,
3740            }
3741        );
3742    }
3743
3744    #[test]
3745    fn quantifier_none() {
3746        assert_eq!(
3747            parse_expr("none(x IN [1,2] WHERE x > 1)").unwrap(),
3748            ReturnExpr::Quantifier {
3749                kind: QuantifierKind::None,
3750                var: "x".to_string(),
3751                source: Box::new(ReturnExpr::ListLit(vec![
3752                    ReturnExpr::Lit(Literal::Int(1)),
3753                    ReturnExpr::Lit(Literal::Int(2)),
3754                ])),
3755                where_clause: Some(Box::new(ReturnExpr::Compare(
3756                    Box::new(ReturnExpr::Var("x".to_string())),
3757                    CompareOp::Gt,
3758                    Box::new(ReturnExpr::Lit(Literal::Int(1))),
3759                ))),
3760            }
3761        );
3762    }
3763
3764    #[test]
3765    fn quantifier_all_any_single_no_where() {
3766        assert!(matches!(
3767            parse_expr("all(x IN [1]) ").unwrap(),
3768            ReturnExpr::Quantifier {
3769                kind: QuantifierKind::All,
3770                where_clause: None,
3771                ..
3772            }
3773        ));
3774        assert!(matches!(
3775            parse_expr("any(x IN [1])").unwrap(),
3776            ReturnExpr::Quantifier {
3777                kind: QuantifierKind::Any,
3778                ..
3779            }
3780        ));
3781        assert!(matches!(
3782            parse_expr("single(x IN [1])").unwrap(),
3783            ReturnExpr::Quantifier {
3784                kind: QuantifierKind::Single,
3785                ..
3786            }
3787        ));
3788    }
3789
3790    #[test]
3791    fn list_comprehension_with_projection() {
3792        assert_eq!(
3793            parse_expr("[x IN [1,2] WHERE x > 1 | x * 2]").unwrap(),
3794            ReturnExpr::ListComp {
3795                var: "x".to_string(),
3796                source: Box::new(ReturnExpr::ListLit(vec![
3797                    ReturnExpr::Lit(Literal::Int(1)),
3798                    ReturnExpr::Lit(Literal::Int(2)),
3799                ])),
3800                where_clause: Some(Box::new(ReturnExpr::Compare(
3801                    Box::new(ReturnExpr::Var("x".to_string())),
3802                    CompareOp::Gt,
3803                    Box::new(ReturnExpr::Lit(Literal::Int(1))),
3804                ))),
3805                project: Some(Box::new(ReturnExpr::Arith(
3806                    Box::new(ReturnExpr::Var("x".to_string())),
3807                    ArithOp::Mul,
3808                    Box::new(ReturnExpr::Lit(Literal::Int(2))),
3809                ))),
3810            }
3811        );
3812    }
3813
3814    #[test]
3815    fn list_comprehension_with_where_no_project() {
3816        assert_eq!(
3817            parse_expr("[x IN [1,2] WHERE x > 1]").unwrap(),
3818            ReturnExpr::ListComp {
3819                var: "x".to_string(),
3820                source: Box::new(ReturnExpr::ListLit(vec![
3821                    ReturnExpr::Lit(Literal::Int(1)),
3822                    ReturnExpr::Lit(Literal::Int(2)),
3823                ])),
3824                where_clause: Some(Box::new(ReturnExpr::Compare(
3825                    Box::new(ReturnExpr::Var("x".to_string())),
3826                    CompareOp::Gt,
3827                    Box::new(ReturnExpr::Lit(Literal::Int(1))),
3828                ))),
3829                project: None,
3830            }
3831        );
3832    }
3833
3834    #[test]
3835    fn list_comprehension_bare_identity_no_where_no_project() {
3836        // `[x IN list]` (neither WHERE nor `| project`) is genuinely
3837        // ambiguous with a one-element `listLit` containing the boolean
3838        // `x IN list` membership test -- `atom`'s alternatives are
3839        // ordered so `listComprehension` wins (real, spec-valid Cypher on
3840        // its own per openCypher.bnf's `<list comprehension>`, whose
3841        // filter/projection half is optional; found wrong via a Phase 3
3842        // behavioral dry-run, not the TCK).
3843        assert_eq!(
3844            parse_expr("[x IN [1, 2, 3]]").unwrap(),
3845            ReturnExpr::ListComp {
3846                var: "x".to_string(),
3847                source: Box::new(ReturnExpr::ListLit(vec![
3848                    ReturnExpr::Lit(Literal::Int(1)),
3849                    ReturnExpr::Lit(Literal::Int(2)),
3850                    ReturnExpr::Lit(Literal::Int(3)),
3851                ])),
3852                where_clause: None,
3853                project: None,
3854            }
3855        );
3856    }
3857
3858    #[test]
3859    fn string_predicates() {
3860        assert_eq!(
3861            parse_expr("x STARTS WITH y").unwrap(),
3862            ReturnExpr::Compare(
3863                Box::new(ReturnExpr::Var("x".to_string())),
3864                CompareOp::StartsWith,
3865                Box::new(ReturnExpr::Var("y".to_string())),
3866            )
3867        );
3868        assert_eq!(
3869            parse_expr("x ENDS WITH y").unwrap(),
3870            ReturnExpr::Compare(
3871                Box::new(ReturnExpr::Var("x".to_string())),
3872                CompareOp::EndsWith,
3873                Box::new(ReturnExpr::Var("y".to_string())),
3874            )
3875        );
3876        assert_eq!(
3877            parse_expr("x CONTAINS y").unwrap(),
3878            ReturnExpr::Compare(
3879                Box::new(ReturnExpr::Var("x".to_string())),
3880                CompareOp::Contains,
3881                Box::new(ReturnExpr::Var("y".to_string())),
3882            )
3883        );
3884    }
3885
3886    #[test]
3887    fn index_and_slice() {
3888        assert_eq!(
3889            parse_expr("list[0]").unwrap(),
3890            ReturnExpr::Index(
3891                Box::new(ReturnExpr::Var("list".to_string())),
3892                Box::new(ReturnExpr::Lit(Literal::Int(0))),
3893            )
3894        );
3895        assert_eq!(
3896            parse_expr("list[1..3]").unwrap(),
3897            ReturnExpr::Slice(
3898                Box::new(ReturnExpr::Var("list".to_string())),
3899                Some(Box::new(ReturnExpr::Lit(Literal::Int(1)))),
3900                Some(Box::new(ReturnExpr::Lit(Literal::Int(3)))),
3901            )
3902        );
3903        assert_eq!(
3904            parse_expr("list[..3]").unwrap(),
3905            ReturnExpr::Slice(
3906                Box::new(ReturnExpr::Var("list".to_string())),
3907                None,
3908                Some(Box::new(ReturnExpr::Lit(Literal::Int(3)))),
3909            )
3910        );
3911        assert_eq!(
3912            parse_expr("list[1..]").unwrap(),
3913            ReturnExpr::Slice(
3914                Box::new(ReturnExpr::Var("list".to_string())),
3915                Some(Box::new(ReturnExpr::Lit(Literal::Int(1)))),
3916                None,
3917            )
3918        );
3919    }
3920
3921    #[test]
3922    fn property_access() {
3923        assert_eq!(
3924            parse_expr("n.name").unwrap(),
3925            ReturnExpr::Prop(PropAccess {
3926                var: "n".to_string(),
3927                prop: "name".to_string(),
3928            })
3929        );
3930    }
3931
3932    #[test]
3933    fn property_access_with_backtick_escaped_name() {
3934        // Regression (found via the TCK, Map1 [5]): `.get_text()` on the
3935        // `name` context kept the surrounding backticks as part of the
3936        // property name (`` `name` `` instead of `name`), so this always
3937        // looked up the wrong key. `name_text` strips them, same as
3938        // `symbol_text` already does for backtick-escaped variable names.
3939        assert_eq!(
3940            parse_expr("n.`weird name`").unwrap(),
3941            ReturnExpr::Prop(PropAccess {
3942                var: "n".to_string(),
3943                prop: "weird name".to_string(),
3944            })
3945        );
3946    }
3947
3948    #[test]
3949    fn property_access_on_computed_expr_becomes_prop_of() {
3950        // `<expr>.prop` where `<expr>` isn't a bare variable -- `ReturnExpr::
3951        // PropOf`, evaluated by evaluating the base first, then looking the
3952        // property up on whatever `Value` it produced (TCK's Graph6 [4]/
3953        // [8], Map1 [3], Merge5 [11]).
3954        let expr = parse_expr("duration.between(a, b).days").unwrap();
3955        let ReturnExpr::PropOf(base, prop) = expr else {
3956            panic!("expected PropOf, got {expr:?}");
3957        };
3958        assert_eq!(prop, "days");
3959        assert!(matches!(*base, ReturnExpr::Call { .. }));
3960    }
3961
3962    #[test]
3963    fn chained_property_access_folds_left_to_right() {
3964        // `a.b.c` -> `PropOf(Prop{a,b}, c)` -- TCK's With2 [2].
3965        let expr = parse_expr("a.b.c").unwrap();
3966        let ReturnExpr::PropOf(base, prop) = expr else {
3967            panic!("expected PropOf, got {expr:?}");
3968        };
3969        assert_eq!(prop, "c");
3970        assert_eq!(
3971            *base,
3972            ReturnExpr::Prop(PropAccess {
3973                var: "a".to_string(),
3974                prop: "b".to_string(),
3975            })
3976        );
3977    }
3978
3979    #[test]
3980    fn has_label() {
3981        assert_eq!(
3982            parse_expr("n:Person").unwrap(),
3983            ReturnExpr::HasLabel("n".to_string(), vec!["Person".to_string()])
3984        );
3985    }
3986
3987    #[test]
3988    fn function_call() {
3989        assert_eq!(
3990            parse_expr("size(list)").unwrap(),
3991            ReturnExpr::Call {
3992                name: "size".to_string(),
3993                args: vec![ReturnExpr::Var("list".to_string())],
3994                distinct: false,
3995            }
3996        );
3997    }
3998
3999    #[test]
4000    fn namespaced_function_call() {
4001        assert_eq!(
4002            parse_expr("duration.between(a, b)").unwrap(),
4003            ReturnExpr::Call {
4004                name: "duration.between".to_string(),
4005                args: vec![
4006                    ReturnExpr::Var("a".to_string()),
4007                    ReturnExpr::Var("b".to_string())
4008                ],
4009                distinct: false,
4010            }
4011        );
4012    }
4013
4014    #[test]
4015    fn count_star() {
4016        assert_eq!(parse_expr("count(*)").unwrap(), ReturnExpr::CountStar);
4017    }
4018
4019    #[test]
4020    fn aggregate_distinct() {
4021        assert_eq!(
4022            parse_expr("count(DISTINCT x)").unwrap(),
4023            ReturnExpr::Call {
4024                name: "count".to_string(),
4025                args: vec![ReturnExpr::Var("x".to_string())],
4026                distinct: true,
4027            }
4028        );
4029    }
4030
4031    #[test]
4032    fn distinct_on_non_aggregate_errors() {
4033        assert!(parse_expr("size(DISTINCT x)").is_err());
4034    }
4035
4036    #[test]
4037    fn distinct_on_namespaced_call_errors() {
4038        assert!(parse_expr("duration.between(DISTINCT a, b)").is_err());
4039    }
4040
4041    #[test]
4042    fn parameter_by_name() {
4043        assert_eq!(
4044            parse_expr("$name").unwrap(),
4045            ReturnExpr::Lit(Literal::Param("name".to_string()))
4046        );
4047    }
4048
4049    #[test]
4050    fn parameter_by_position() {
4051        assert_eq!(
4052            parse_expr("$0").unwrap(),
4053            ReturnExpr::Lit(Literal::Param("0".to_string()))
4054        );
4055    }
4056
4057    #[test]
4058    fn parenthesized_expression() {
4059        assert_eq!(
4060            parse_expr("(1 + 2) * 3").unwrap(),
4061            ReturnExpr::Arith(
4062                Box::new(ReturnExpr::Arith(
4063                    Box::new(ReturnExpr::Lit(Literal::Int(1))),
4064                    ArithOp::Add,
4065                    Box::new(ReturnExpr::Lit(Literal::Int(2))),
4066                )),
4067                ArithOp::Mul,
4068                Box::new(ReturnExpr::Lit(Literal::Int(3))),
4069            )
4070        );
4071    }
4072
4073    #[test]
4074    fn return_simple_items() {
4075        let c = parse_return("RETURN a, b.name AS name").unwrap();
4076        let Tail::Return(items, distinct) = c.tail else {
4077            panic!("expected Tail::Return");
4078        };
4079        assert!(!distinct);
4080        assert_eq!(items.len(), 2);
4081        assert_eq!(items[0].expr, ReturnExpr::Var("a".to_string()));
4082        assert_eq!(items[0].alias, None);
4083        assert_eq!(
4084            items[1].expr,
4085            ReturnExpr::Prop(PropAccess {
4086                var: "b".to_string(),
4087                prop: "name".to_string(),
4088            })
4089        );
4090        assert_eq!(items[1].alias.as_deref(), Some("name"));
4091    }
4092
4093    #[test]
4094    fn return_distinct() {
4095        let c = parse_return("RETURN DISTINCT a").unwrap();
4096        let Tail::Return(_, distinct) = c.tail else {
4097            panic!("expected Tail::Return");
4098        };
4099        assert!(distinct);
4100    }
4101
4102    #[test]
4103    fn return_star() {
4104        let c = parse_return("RETURN *").unwrap();
4105        assert!(matches!(c.tail, Tail::ReturnStar(false)));
4106    }
4107
4108    #[test]
4109    fn return_order_by_skip_limit() {
4110        let c = parse_return("RETURN a ORDER BY a DESC SKIP 5 LIMIT 10").unwrap();
4111        let order_by = c.order_by.unwrap();
4112        assert_eq!(order_by.len(), 1);
4113        assert_eq!(order_by[0].0, ReturnExpr::Var("a".to_string()));
4114        assert_eq!(order_by[0].1, SortDir::Desc);
4115        assert_eq!(c.skip, Some(ReturnExpr::Lit(Literal::Int(5))));
4116        assert_eq!(c.limit, Some(ReturnExpr::Lit(Literal::Int(10))));
4117    }
4118
4119    #[test]
4120    fn order_by_default_ascending() {
4121        let c = parse_return("RETURN a ORDER BY a").unwrap();
4122        assert_eq!(c.order_by.unwrap()[0].1, SortDir::Asc);
4123    }
4124
4125    #[test]
4126    fn limit_accepts_arbitrary_expression() {
4127        // skipSt/limitSt grammar-allow any expression -- SKIP/LIMIT no
4128        // longer restrict to a literal integer at parse time (real Cypher
4129        // permits `SKIP $n`/`LIMIT toInteger(rand()*9)`, TCK's
4130        // `ReturnSkipLimit1 [2]`/`[3]`); the non-negative-integer check
4131        // happens once at execution time instead (see
4132        // `executor::resolve_skip_limit`).
4133        let c = parse_return("RETURN a LIMIT 1 + 1").unwrap();
4134        assert!(c.limit.is_some());
4135    }
4136
4137    #[test]
4138    fn return_star_with_extra_items_errors() {
4139        // projectionItems syntactically allows `* , x` (MULT then a
4140        // COMMA'd projectionItem), but Tail::ReturnStar has no field to
4141        // carry the extra item -- must error, not silently drop it.
4142        assert!(parse_return("RETURN *, x AS y").is_err());
4143    }
4144
4145    #[test]
4146    fn with_items() {
4147        let c = parse_with("WITH a, b.name AS name").unwrap();
4148        assert!(!c.star);
4149        assert!(!c.distinct);
4150        assert_eq!(c.items.len(), 2);
4151        assert_eq!(c.items[0].expr, ReturnExpr::Var("a".to_string()));
4152        assert_eq!(c.items[1].alias.as_deref(), Some("name"));
4153    }
4154
4155    #[test]
4156    fn with_star() {
4157        let c = parse_with("WITH *").unwrap();
4158        assert!(c.star);
4159        assert!(c.items.is_empty());
4160    }
4161
4162    #[test]
4163    fn with_star_and_items() {
4164        // Unlike RETURN *, WithClause has both `star` and `items` fields
4165        // -- real Cypher's `WITH *, x AS y` is fully representable.
4166        let c = parse_with("WITH *, x AS y").unwrap();
4167        assert!(c.star);
4168        assert_eq!(c.items.len(), 1);
4169        assert_eq!(c.items[0].alias.as_deref(), Some("y"));
4170    }
4171
4172    #[test]
4173    fn with_distinct_order_skip_limit() {
4174        let c = parse_with("WITH DISTINCT a ORDER BY a SKIP 1 LIMIT 2").unwrap();
4175        assert!(c.distinct);
4176        assert!(c.order_by.is_some());
4177        assert_eq!(c.skip, Some(ReturnExpr::Lit(Literal::Int(1))));
4178        assert_eq!(c.limit, Some(ReturnExpr::Lit(Literal::Int(2))));
4179    }
4180
4181    #[test]
4182    fn with_where_compare() {
4183        let c = parse_with("WITH a WHERE a.x = 1").unwrap();
4184        let WithExpr::Compare(lhs, op, rhs) = c.where_clause.unwrap() else {
4185            panic!("expected WithExpr::Compare");
4186        };
4187        assert_eq!(
4188            lhs,
4189            ReturnExpr::Prop(PropAccess {
4190                var: "a".to_string(),
4191                prop: "x".to_string()
4192            })
4193        );
4194        assert_eq!(op, CompareOp::Eq);
4195        assert_eq!(rhs, ReturnExpr::Lit(Literal::Int(1)));
4196    }
4197
4198    #[test]
4199    fn with_where_and_or_not() {
4200        let c = parse_with("WITH a WHERE NOT (a.x = 1 AND a.y = 2)").unwrap();
4201        assert!(matches!(c.where_clause.unwrap(), WithExpr::Not(_)));
4202
4203        let c = parse_with("WITH a WHERE a.x = 1 OR a.y = 2").unwrap();
4204        assert!(matches!(c.where_clause.unwrap(), WithExpr::Or(_, _)));
4205    }
4206
4207    #[test]
4208    fn with_where_is_null() {
4209        let c = parse_with("WITH a WHERE a IS NULL").unwrap();
4210        assert!(matches!(c.where_clause.unwrap(), WithExpr::IsNull(_)));
4211    }
4212
4213    #[test]
4214    fn with_where_bare_expression() {
4215        // A boolean-valued expression with no comparison operator at all
4216        // (here: a HasLabel check) -- no exact WithExpr variant, so it
4217        // falls back to Bare rather than erroring.
4218        let c = parse_with("WITH n WHERE n:Person").unwrap();
4219        assert!(matches!(c.where_clause.unwrap(), WithExpr::Bare(_)));
4220    }
4221
4222    #[test]
4223    fn with_where_xor_becomes_bare() {
4224        // WithExpr has no Xor variant at all -- confirmed falls back to
4225        // Bare rather than silently dropping the XOR semantics.
4226        let c = parse_with("WITH a WHERE a.x XOR a.y").unwrap();
4227        assert!(matches!(c.where_clause.unwrap(), WithExpr::Bare(_)));
4228    }
4229
4230    #[test]
4231    fn unwind_basic() {
4232        let c = parse_unwind("UNWIND [1, 2, 3] AS x").unwrap();
4233        assert_eq!(c.var, "x");
4234        assert_eq!(
4235            c.source.0,
4236            ReturnExpr::ListLit(vec![
4237                ReturnExpr::Lit(Literal::Int(1)),
4238                ReturnExpr::Lit(Literal::Int(2)),
4239                ReturnExpr::Lit(Literal::Int(3)),
4240            ])
4241        );
4242        assert!(c.where_clause.is_none());
4243        assert!(c.with.is_none());
4244    }
4245
4246    #[test]
4247    fn set_prop() {
4248        let items = parse_set("SET n.name = 'x'").unwrap();
4249        assert_eq!(items.len(), 1);
4250        let SetItem::Prop(prop, value) = &items[0] else {
4251            panic!("expected SetItem::Prop");
4252        };
4253        assert_eq!(prop.var, "n");
4254        assert_eq!(prop.prop, "name");
4255        assert_eq!(*value, ReturnExpr::Lit(Literal::String("x".to_string())));
4256    }
4257
4258    #[test]
4259    fn set_labels() {
4260        let items = parse_set("SET n:A:B").unwrap();
4261        let SetItem::Labels(var, labels) = &items[0] else {
4262            panic!("expected SetItem::Labels");
4263        };
4264        assert_eq!(var, "n");
4265        assert_eq!(labels, &vec!["A".to_string(), "B".to_string()]);
4266    }
4267
4268    #[test]
4269    fn set_map_assign() {
4270        let items = parse_set("SET n = {a: 1}").unwrap();
4271        let SetItem::MapAssign { var, merge, .. } = &items[0] else {
4272            panic!("expected SetItem::MapAssign");
4273        };
4274        assert_eq!(var, "n");
4275        assert!(!merge);
4276
4277        let items = parse_set("SET n += {a: 1}").unwrap();
4278        let SetItem::MapAssign { merge, .. } = &items[0] else {
4279            panic!("expected SetItem::MapAssign");
4280        };
4281        assert!(merge);
4282    }
4283
4284    #[test]
4285    fn set_multiple_items() {
4286        assert_eq!(parse_set("SET n.a = 1, n.b = 2").unwrap().len(), 2);
4287    }
4288
4289    #[test]
4290    fn delete_items() {
4291        let d = parse_delete("DELETE n, r").unwrap();
4292        assert!(!d.detach);
4293        assert_eq!(d.items.len(), 2);
4294    }
4295
4296    #[test]
4297    fn detach_delete() {
4298        let d = parse_delete("DETACH DELETE n").unwrap();
4299        assert!(d.detach);
4300    }
4301
4302    #[test]
4303    fn remove_prop() {
4304        let items = parse_remove("REMOVE n.name").unwrap();
4305        let RemoveItem::Prop(prop) = &items[0] else {
4306            panic!("expected RemoveItem::Prop");
4307        };
4308        assert_eq!(prop.var, "n");
4309        assert_eq!(prop.prop, "name");
4310    }
4311
4312    #[test]
4313    fn remove_labels() {
4314        let items = parse_remove("REMOVE n:A:B").unwrap();
4315        let RemoveItem::Labels(var, labels) = &items[0] else {
4316            panic!("expected RemoveItem::Labels");
4317        };
4318        assert_eq!(var, "n");
4319        assert_eq!(labels, &vec!["A".to_string(), "B".to_string()]);
4320    }
4321
4322    #[test]
4323    fn create_single_pattern() {
4324        let patterns = parse_create("CREATE (a:Person)").unwrap();
4325        assert_eq!(patterns.len(), 1);
4326        assert_eq!(patterns[0].start.var.as_deref(), Some("a"));
4327    }
4328
4329    #[test]
4330    fn create_comma_patterns_stay_separate() {
4331        // Unlike MATCH, CREATE never splices shared-node comma patterns
4332        // into one linear chain -- each stays its own Pattern.
4333        let patterns = parse_create("CREATE (a), (a)-->(b)").unwrap();
4334        assert_eq!(patterns.len(), 2);
4335    }
4336
4337    #[test]
4338    fn create_named_path_errors() {
4339        assert!(parse_create("CREATE p = (a)-->(b)").is_err());
4340    }
4341
4342    #[test]
4343    fn merge_single_hop() {
4344        let m = parse_merge("MERGE (a)-[:KNOWS]->(b)").unwrap();
4345        assert_eq!(m.pattern.hops.len(), 1);
4346        assert!(m.on_create.is_empty());
4347        assert!(m.on_match.is_empty());
4348    }
4349
4350    #[test]
4351    fn merge_multi_hop_errors() {
4352        assert!(parse_merge("MERGE (a)-->(b)-->(c)").is_err());
4353    }
4354
4355    #[test]
4356    fn merge_named_path_capture() {
4357        let m = parse_merge("MERGE p = (a)-->(b)").unwrap();
4358        assert_eq!(m.path_var.as_deref(), Some("p"));
4359    }
4360
4361    #[test]
4362    fn merge_on_create_on_match() {
4363        let m = parse_merge("MERGE (a) ON CREATE SET a.created = true ON MATCH SET a.seen = true")
4364            .unwrap();
4365        assert_eq!(m.on_create.len(), 1);
4366        assert_eq!(m.on_match.len(), 1);
4367    }
4368
4369    #[test]
4370    fn merge_duplicate_on_create_errors() {
4371        assert!(parse_merge("MERGE (a) ON CREATE SET a.x = 1 ON CREATE SET a.y = 2").is_err());
4372    }
4373
4374    #[test]
4375    fn merge_duplicate_on_match_errors() {
4376        assert!(parse_merge("MERGE (a) ON MATCH SET a.x = 1 ON MATCH SET a.y = 2").is_err());
4377    }
4378
4379    #[test]
4380    fn statement_match_return() {
4381        let s = parse_statement("MATCH (a) RETURN a").unwrap();
4382        let Statement::Match {
4383            clauses,
4384            tail,
4385            order_by,
4386            skip,
4387            limit,
4388        } = s
4389        else {
4390            panic!("expected Statement::Match");
4391        };
4392        assert_eq!(clauses.len(), 1);
4393        assert!(matches!(clauses[0], QueryClause::Match(_)));
4394        assert!(matches!(tail, Some(Tail::Return(_, false))));
4395        assert!(order_by.is_none());
4396        assert!(skip.is_none());
4397        assert!(limit.is_none());
4398    }
4399
4400    #[test]
4401    fn statement_return_star() {
4402        let s = parse_statement("MATCH (a) RETURN *").unwrap();
4403        let Statement::Match { tail, .. } = s else {
4404            panic!("expected Statement::Match");
4405        };
4406        assert!(matches!(tail, Some(Tail::ReturnStar(false))));
4407    }
4408
4409    #[test]
4410    fn statement_order_by_skip_limit_on_bare_return() {
4411        let s = parse_statement("MATCH (a) RETURN a ORDER BY a SKIP 1 LIMIT 2").unwrap();
4412        let Statement::Match {
4413            order_by,
4414            skip,
4415            limit,
4416            ..
4417        } = s
4418        else {
4419            panic!("expected Statement::Match");
4420        };
4421        assert!(order_by.is_some());
4422        assert_eq!(skip, Some(Box::new(ReturnExpr::Lit(Literal::Int(1)))));
4423        assert_eq!(limit, Some(Box::new(ReturnExpr::Lit(Literal::Int(2)))));
4424    }
4425
4426    #[test]
4427    fn statement_multiple_reading_clauses() {
4428        let s = parse_statement("MATCH (a) UNWIND [1,2] AS x RETURN a, x").unwrap();
4429        let Statement::Match { clauses, .. } = s else {
4430            panic!("expected Statement::Match");
4431        };
4432        assert_eq!(clauses.len(), 2);
4433        assert!(matches!(clauses[0], QueryClause::Match(_)));
4434        assert!(matches!(clauses[1], QueryClause::Unwind(_)));
4435    }
4436
4437    #[test]
4438    fn statement_set_becomes_tail_with_return_tail() {
4439        let s = parse_statement("MATCH (n) SET n.x = 1 RETURN n").unwrap();
4440        let Statement::Match { clauses, tail, .. } = s else {
4441            panic!("expected Statement::Match");
4442        };
4443        assert_eq!(clauses.len(), 1);
4444        let Some(Tail::Set(items, Some(ret))) = tail else {
4445            panic!("expected Tail::Set with a ReturnTail");
4446        };
4447        assert_eq!(items.len(), 1);
4448        assert_eq!(ret.items.len(), 1);
4449    }
4450
4451    #[test]
4452    fn statement_set_without_trailing_return() {
4453        let s = parse_statement("MATCH (n) SET n.x = 1").unwrap();
4454        let Statement::Match { tail, .. } = s else {
4455            panic!("expected Statement::Match");
4456        };
4457        assert!(matches!(tail, Some(Tail::Set(_, None))));
4458    }
4459
4460    #[test]
4461    fn statement_detach_delete_tail() {
4462        let s = parse_statement("MATCH (n) DETACH DELETE n").unwrap();
4463        let Statement::Match { tail, .. } = s else {
4464            panic!("expected Statement::Match");
4465        };
4466        assert!(matches!(tail, Some(Tail::DetachDelete(_, None))));
4467    }
4468
4469    #[test]
4470    fn statement_two_updating_clauses_last_becomes_tail() {
4471        // SET is just another QueryClause; DELETE (last) becomes the Tail.
4472        let s = parse_statement("MATCH (n) SET n.x = 1 DELETE n RETURN count(n)").unwrap();
4473        let Statement::Match { clauses, tail, .. } = s else {
4474            panic!("expected Statement::Match");
4475        };
4476        assert_eq!(clauses.len(), 2);
4477        assert!(matches!(clauses[1], QueryClause::Set(_)));
4478        assert!(matches!(tail, Some(Tail::Delete(_, Some(_)))));
4479    }
4480
4481    #[test]
4482    fn statement_bare_merge_no_tail() {
4483        // MERGE alone (no RETURN) is the one case a missing Tail is valid
4484        // -- MERGE never becomes the Tail itself (no Tail::Merge variant).
4485        let s = parse_statement("MERGE (a)").unwrap();
4486        let Statement::Match { clauses, tail, .. } = s else {
4487            panic!("expected Statement::Match");
4488        };
4489        assert!(matches!(clauses[0], QueryClause::Merge(_)));
4490        assert!(tail.is_none());
4491    }
4492
4493    #[test]
4494    fn statement_merge_with_trailing_return() {
4495        // MERGE followed by RETURN: MERGE is a QueryClause, RETURN becomes
4496        // the statement's own full Tail::Return (order/skip/limit-capable),
4497        // not a narrower embedded ReturnTail the way SET/DELETE/REMOVE/
4498        // CREATE consume their own trailing RETURN.
4499        let s = parse_statement("MERGE (a) RETURN a ORDER BY a").unwrap();
4500        let Statement::Match {
4501            clauses,
4502            tail,
4503            order_by,
4504            ..
4505        } = s
4506        else {
4507            panic!("expected Statement::Match");
4508        };
4509        assert!(matches!(clauses[0], QueryClause::Merge(_)));
4510        assert!(matches!(tail, Some(Tail::Return(_, false))));
4511        assert!(order_by.is_some());
4512    }
4513
4514    #[test]
4515    fn statement_bare_match_without_tail_errors() {
4516        // Unlike MERGE, a bare MATCH with nothing after it is almost
4517        // certainly a mistake, not a deliberate no-op.
4518        assert!(parse_statement("MATCH (n)").is_err());
4519    }
4520
4521    #[test]
4522    fn statement_mutating_tail_order_by_skip_limit_apply_at_statement_level() {
4523        // ReturnTail itself (SET/DELETE/REMOVE/CREATE's own trailing
4524        // RETURN) has no room for ORDER BY/SKIP/LIMIT -- but real Cypher
4525        // still allows them here (TCK's Delete6/Remove3 "Persistence of
4526        // .../remove clause side effects"), applying to the *statement*,
4527        // same as pest's own grammar keeps them as siblings of tail_clause
4528        // rather than nested inside the RETURN.
4529        let s =
4530            parse_statement("MATCH (n) SET n.x = 1 RETURN n ORDER BY n.x SKIP 1 LIMIT 2").unwrap();
4531        let Statement::Match {
4532            tail,
4533            order_by,
4534            skip,
4535            limit,
4536            ..
4537        } = s
4538        else {
4539            panic!("expected Statement::Match");
4540        };
4541        assert!(matches!(tail, Some(Tail::Set(_, Some(_)))));
4542        assert!(order_by.is_some());
4543        assert_eq!(skip, Some(Box::new(ReturnExpr::Lit(Literal::Int(1)))));
4544        assert_eq!(limit, Some(Box::new(ReturnExpr::Lit(Literal::Int(2)))));
4545    }
4546
4547    #[test]
4548    fn statement_mutating_tail_return_star_errors() {
4549        assert!(parse_statement("MATCH (n) SET n.x = 1 RETURN *").is_err());
4550    }
4551
4552    #[test]
4553    fn statement_create_tail() {
4554        let s = parse_statement("CREATE (a) RETURN a").unwrap();
4555        let Statement::Match { tail, .. } = s else {
4556            panic!("expected Statement::Match");
4557        };
4558        assert!(matches!(tail, Some(Tail::Create(_, Some(_)))));
4559    }
4560
4561    #[test]
4562    fn statement_bare_create_is_not_wrapped_in_match() {
4563        // `CREATE (...)` with nothing else at all mirrors pest's
4564        // `create_stmt_only` -- a real `Statement::Create` directly, not
4565        // `Statement::Match{tail: Some(Tail::Create(...))}`. Found via a
4566        // Phase 3 dry-run: `explain.rs`'s "no query plan" output depends
4567        // on this exact shape distinction.
4568        let s = parse_antlr("CREATE (a);").unwrap();
4569        assert!(matches!(s, Statement::Create(_)));
4570    }
4571
4572    #[test]
4573    fn statement_remove_tail() {
4574        let s = parse_statement("MATCH (n) REMOVE n.x").unwrap();
4575        let Statement::Match { tail, .. } = s else {
4576            panic!("expected Statement::Match");
4577        };
4578        assert!(matches!(tail, Some(Tail::Remove(_, None))));
4579    }
4580
4581    #[test]
4582    fn multi_part_with_attaches_to_preceding_match() {
4583        let s = parse_multi_part_statement("MATCH (a:A) WITH a MATCH (b:B) RETURN a, b").unwrap();
4584        let Statement::Match { clauses, tail, .. } = s else {
4585            panic!("expected Statement::Match");
4586        };
4587        assert_eq!(clauses.len(), 2);
4588        let QueryClause::Match(first) = &clauses[0] else {
4589            panic!("expected first clause to be Match");
4590        };
4591        assert!(first.with.is_some());
4592        assert!(matches!(clauses[1], QueryClause::Match(_)));
4593        assert!(matches!(tail, Some(Tail::Return(_, false))));
4594    }
4595
4596    #[test]
4597    fn multi_part_chained_with_second_one_standalone() {
4598        // TCK's chained `WITH x AS y WITH y % 3 AS y` shape: the first WITH
4599        // attaches to the preceding MATCH, the second has nothing
4600        // attachable immediately before it (another WITH, not a fresh
4601        // clause) so it becomes its own standalone `QueryClause::With`.
4602        let s = parse_multi_part_statement("MATCH (a:A) WITH a.num AS x WITH x % 3 AS x RETURN x")
4603            .unwrap();
4604        let Statement::Match { clauses, .. } = s else {
4605            panic!("expected Statement::Match");
4606        };
4607        assert_eq!(clauses.len(), 2);
4608        let QueryClause::Match(first) = &clauses[0] else {
4609            panic!("expected first clause to be Match");
4610        };
4611        assert!(first.with.is_some());
4612        assert!(matches!(clauses[1], QueryClause::With(_)));
4613    }
4614
4615    #[test]
4616    fn multi_part_set_then_with_stays_separate_entries() {
4617        // SET has no `with` field on its `QueryClause` variant -- a
4618        // following WITH always becomes its own standalone entry, never
4619        // folded into the SET.
4620        let s = parse_multi_part_statement(
4621            "MATCH (n:N) WITH n, n.num AS num DELETE n WITH num WHERE num % 2 = 0 RETURN num",
4622        )
4623        .unwrap();
4624        let Statement::Match { clauses, tail, .. } = s else {
4625            panic!("expected Statement::Match");
4626        };
4627        assert_eq!(clauses.len(), 3);
4628        assert!(matches!(clauses[0], QueryClause::Match(_)));
4629        assert!(matches!(clauses[1], QueryClause::Delete { .. }));
4630        assert!(matches!(clauses[2], QueryClause::With(_)));
4631        assert!(matches!(tail, Some(Tail::Return(_, false))));
4632    }
4633
4634    #[test]
4635    fn multi_part_create_with_star_create_create_tail() {
4636        let s =
4637            parse_multi_part_statement("CREATE (a) WITH a WITH * CREATE (b) CREATE (a)<-[:T]-(b)")
4638                .unwrap();
4639        let Statement::Match { clauses, tail, .. } = s else {
4640            panic!("expected Statement::Match");
4641        };
4642        // Create(a), With(a) folded away into... no: Create has no `with`
4643        // field, so the first WITH is standalone; the second WITH (WITH *)
4644        // is likewise standalone (nothing attachable precedes it either).
4645        assert_eq!(clauses.len(), 4);
4646        assert!(matches!(clauses[0], QueryClause::Create(_)));
4647        assert!(matches!(clauses[1], QueryClause::With(_)));
4648        assert!(matches!(clauses[2], QueryClause::With(_)));
4649        assert!(matches!(clauses[3], QueryClause::Create(_)));
4650        assert!(matches!(tail, Some(Tail::Create(_, None))));
4651    }
4652
4653    #[test]
4654    fn multi_part_merge_with_attaches() {
4655        let s = parse_multi_part_statement("MERGE (a:A) WITH a MATCH (b:B) RETURN a, b").unwrap();
4656        let Statement::Match { clauses, .. } = s else {
4657            panic!("expected Statement::Match");
4658        };
4659        assert_eq!(clauses.len(), 2);
4660        let QueryClause::Merge(m) = &clauses[0] else {
4661            panic!("expected first clause to be Merge");
4662        };
4663        assert!(m.with.is_some());
4664    }
4665
4666    #[test]
4667    fn multi_part_trailing_bare_create_becomes_tail_not_top_level_statement() {
4668        // Regression: build_single_part_q's "bare CREATE with nothing
4669        // else" special case (-> Statement::Create directly) must NOT
4670        // leak out of multiPartQ's own trailing singlePartQ -- past at
4671        // least one WITH boundary, a trailing CREATE is still just this
4672        // statement's Tail::Create, same as any other trailing CREATE.
4673        // Previously panicked (found via a full TCK execution run).
4674        let s = parse_multi_part_statement("MATCH (a) WITH a CREATE (b)").unwrap();
4675        let Statement::Match { clauses, tail, .. } = s else {
4676            panic!("expected Statement::Match");
4677        };
4678        assert_eq!(clauses.len(), 1);
4679        assert!(matches!(clauses[0], QueryClause::Match(_)));
4680        assert!(matches!(tail, Some(Tail::Create(_, None))));
4681    }
4682
4683    #[test]
4684    fn parse_antlr_no_union_passes_through() {
4685        let s = parse_antlr("MATCH (a) RETURN a;").unwrap();
4686        assert!(matches!(s, Statement::Match { .. }));
4687    }
4688
4689    #[test]
4690    fn parse_antlr_union() {
4691        let s = parse_antlr("MATCH (a) RETURN a UNION MATCH (b) RETURN b;").unwrap();
4692        let Statement::Union { parts, all } = s else {
4693            panic!("expected Statement::Union");
4694        };
4695        assert_eq!(parts.len(), 2);
4696        assert!(!all);
4697    }
4698
4699    #[test]
4700    fn parse_antlr_union_all() {
4701        let s = parse_antlr("MATCH (a) RETURN a UNION ALL MATCH (b) RETURN b;").unwrap();
4702        let Statement::Union { parts, all } = s else {
4703            panic!("expected Statement::Union");
4704        };
4705        assert_eq!(parts.len(), 2);
4706        assert!(all);
4707    }
4708
4709    #[test]
4710    fn parse_antlr_union_three_parts() {
4711        let s =
4712            parse_antlr("MATCH (a) RETURN a UNION MATCH (b) RETURN b UNION MATCH (c) RETURN c;")
4713                .unwrap();
4714        let Statement::Union { parts, .. } = s else {
4715            panic!("expected Statement::Union");
4716        };
4717        assert_eq!(parts.len(), 3);
4718    }
4719
4720    #[test]
4721    fn parse_antlr_mixed_union_and_union_all_errors() {
4722        let err = parse_antlr(
4723            "MATCH (a) RETURN a UNION MATCH (b) RETURN b UNION ALL MATCH (c) RETURN c;",
4724        )
4725        .unwrap_err();
4726        assert!(matches!(err, QueryError::Syntax(_)));
4727    }
4728
4729    #[test]
4730    fn parse_antlr_standalone_call() {
4731        let stmt = parse_antlr("CALL db.labels() YIELD label").unwrap();
4732        let Statement::StandaloneCall(call) = stmt else {
4733            panic!("expected a Statement::StandaloneCall, got {stmt:?}");
4734        };
4735        assert_eq!(call.name, "db.labels");
4736        assert_eq!(call.args, Some(vec![]));
4737        assert!(matches!(
4738            call.yield_items,
4739            Some(CallYield::Items(items, None)) if items == vec![("label".to_string(), None)]
4740        ));
4741    }
4742
4743    #[test]
4744    fn parse_antlr_syntax_error() {
4745        assert!(parse_antlr("MATCH (a RETURN a;").is_err());
4746    }
4747
4748    #[test]
4749    fn parse_antlr_many_basic() {
4750        let stmts = parse_antlr_many("CREATE (a); CREATE (b); MATCH (n) RETURN n").unwrap();
4751        assert_eq!(stmts.len(), 3);
4752        // Bare `CREATE (...)` with nothing else is `Statement::Create`
4753        // directly, not `Statement::Match` -- see `build_single_part_q`'s
4754        // own docs.
4755        assert!(matches!(stmts[0], Statement::Create(_)));
4756        assert!(matches!(stmts[2], Statement::Match { .. }));
4757    }
4758
4759    #[test]
4760    fn parse_antlr_many_single_statement() {
4761        let stmts = parse_antlr_many("RETURN 1").unwrap();
4762        assert_eq!(stmts.len(), 1);
4763    }
4764
4765    #[test]
4766    fn parse_antlr_many_strips_single_trailing_semicolon() {
4767        let stmts = parse_antlr_many("CREATE (a);").unwrap();
4768        assert_eq!(stmts.len(), 1);
4769    }
4770
4771    #[test]
4772    fn parse_antlr_many_semicolon_inside_string_literal_not_a_separator() {
4773        let stmts = parse_antlr_many("RETURN ';'").unwrap();
4774        assert_eq!(stmts.len(), 1);
4775    }
4776
4777    #[test]
4778    fn split_statements_respects_all_three_quote_forms() {
4779        // Single-quoted, double-quoted, and backtick-quoted (identifier)
4780        // -- a `;` inside any of them is content, not a separator.
4781        assert_eq!(
4782            split_statements("RETURN ';'; RETURN 1"),
4783            vec!["RETURN ';'", " RETURN 1"]
4784        );
4785        assert_eq!(
4786            split_statements(r#"RETURN ";"; RETURN 1"#),
4787            vec![r#"RETURN ";""#, " RETURN 1"]
4788        );
4789        assert_eq!(
4790            split_statements("MATCH (`a;b`) RETURN 1; RETURN 2"),
4791            vec!["MATCH (`a;b`) RETURN 1", " RETURN 2"]
4792        );
4793    }
4794
4795    #[test]
4796    fn split_statements_handles_escaped_quotes_inside_a_literal() {
4797        // An escaped closing quote (`\'`) doesn't end the string early --
4798        // the real `;` separator is the *second* one, past both escaped
4799        // quotes.
4800        assert_eq!(
4801            split_statements(r"RETURN 'it\'s; a test'; RETURN 1"),
4802            vec![r"RETURN 'it\'s; a test'", " RETURN 1"]
4803        );
4804    }
4805
4806    #[test]
4807    fn split_statements_backtick_literal_has_no_escapes() {
4808        // Unlike '...'/"...", a backtick-quoted identifier has no escape
4809        // sequences in this grammar (`ESC_LITERAL : '`' .*? '`'`) -- a
4810        // backslash inside one is just a literal character, the *very
4811        // next* backtick closes it regardless of what precedes it.
4812        assert_eq!(
4813            split_statements(r"MATCH (`a\`) RETURN 1; RETURN 2"),
4814            vec![r"MATCH (`a\`) RETURN 1", " RETURN 2"]
4815        );
4816    }
4817
4818    #[test]
4819    fn parse_antlr_create_index() {
4820        let s = parse_antlr("CREATE INDEX ON :Person(name);").unwrap();
4821        let Statement::CreateIndex {
4822            label,
4823            prop,
4824            unique,
4825        } = s
4826        else {
4827            panic!("expected Statement::CreateIndex");
4828        };
4829        assert_eq!(label, "Person");
4830        assert_eq!(prop, "name");
4831        assert!(!unique);
4832    }
4833
4834    #[test]
4835    fn parse_antlr_create_index_unique() {
4836        let s = parse_antlr("CREATE INDEX ON :Person(name) UNIQUE;").unwrap();
4837        let Statement::CreateIndex { unique, .. } = s else {
4838            panic!("expected Statement::CreateIndex");
4839        };
4840        assert!(unique);
4841    }
4842
4843    #[test]
4844    fn parse_antlr_explain_match() {
4845        let s = parse_antlr("EXPLAIN MATCH (a) RETURN a;").unwrap();
4846        let Statement::Explain(inner) = s else {
4847            panic!("expected Statement::Explain");
4848        };
4849        assert!(matches!(*inner, Statement::Match { .. }));
4850    }
4851
4852    #[test]
4853    fn parse_antlr_explain_create_index() {
4854        let s = parse_antlr("EXPLAIN CREATE INDEX ON :Person(name);").unwrap();
4855        let Statement::Explain(inner) = s else {
4856            panic!("expected Statement::Explain");
4857        };
4858        assert!(matches!(*inner, Statement::CreateIndex { .. }));
4859    }
4860
4861    #[test]
4862    fn parse_antlr_index_still_usable_as_property_name() {
4863        // `INDEX`/`EXPLAIN` becoming real keyword tokens (needed for
4864        // `createIndexSt`/`explainSt`) must not break their use as
4865        // ordinary property/label names elsewhere -- `name : symbol |
4866        // reservedWord` still absorbs them there.
4867        let s = parse_antlr("MATCH (a) RETURN a.index;").unwrap();
4868        assert!(matches!(s, Statement::Match { .. }));
4869    }
4870}