Skip to main content

uni_cypher/
ast.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use uni_common::Value;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum TimeTravelSpec {
8    /// VERSION AS OF 'snapshot_id'
9    Version(String),
10    /// TIMESTAMP AS OF '2025-02-01T12:00:00Z'
11    Timestamp(String),
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15pub enum Query {
16    Single(Statement),
17    Union {
18        left: Box<Query>,
19        right: Box<Query>,
20        all: bool,
21    },
22    Schema(Box<SchemaCommand>),
23    Explain(Box<Query>),
24    /// Query with time-travel: wraps any query with a VERSION/TIMESTAMP AS OF clause.
25    /// Resolved at the API layer before planning.
26    TimeTravel {
27        query: Box<Query>,
28        spec: TimeTravelSpec,
29    },
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub enum SchemaCommand {
34    CreateVectorIndex(CreateVectorIndex),
35    CreateFullTextIndex(CreateFullTextIndex),
36    CreateScalarIndex(CreateScalarIndex),
37    CreateJsonFtsIndex(CreateJsonFtsIndex),
38    DropIndex(DropIndex),
39    CreateConstraint(CreateConstraint),
40    DropConstraint(DropConstraint),
41    CreateLabel(CreateLabel),
42    CreateEdgeType(CreateEdgeType),
43    AlterLabel(AlterLabel),
44    AlterEdgeType(AlterEdgeType),
45    DropLabel(DropLabel),
46    DropEdgeType(DropEdgeType),
47    ShowConstraints(ShowConstraints),
48    ShowIndexes(ShowIndexes),
49    ShowDatabase,
50    ShowConfig,
51    ShowStatistics,
52    Vacuum,
53    Checkpoint,
54    Backup { path: String },
55    CopyTo(CopyToCommand),
56    CopyFrom(CopyFromCommand),
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60pub struct CreateVectorIndex {
61    pub name: String,
62    pub label: String,
63    pub property: String,
64    pub options: HashMap<String, Value>,
65    pub if_not_exists: bool,
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct CreateFullTextIndex {
70    pub name: String,
71    pub label: String,
72    pub properties: Vec<String>,
73    pub options: HashMap<String, Value>,
74    pub if_not_exists: bool,
75}
76
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct CreateScalarIndex {
79    pub name: String,
80    pub label: String,
81    pub expressions: Vec<Expr>,
82    pub where_clause: Option<Expr>,
83    pub options: HashMap<String, Value>,
84    pub if_not_exists: bool,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct CreateJsonFtsIndex {
89    pub name: String,
90    pub label: String,
91    pub column: String,
92    pub options: HashMap<String, Value>,
93    pub if_not_exists: bool,
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
97pub struct CreateLabel {
98    pub name: String,
99    pub properties: Vec<PropertyDefinition>,
100    pub if_not_exists: bool,
101}
102
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct CreateEdgeType {
105    pub name: String,
106    pub src_labels: Vec<String>,
107    pub dst_labels: Vec<String>,
108    pub properties: Vec<PropertyDefinition>,
109    pub if_not_exists: bool,
110}
111
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct AlterLabel {
114    pub name: String,
115    pub action: AlterAction,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119pub struct AlterEdgeType {
120    pub name: String,
121    pub action: AlterAction,
122}
123
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub enum AlterAction {
126    AddProperty(PropertyDefinition),
127    DropProperty(String),
128    RenameProperty { old_name: String, new_name: String },
129}
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct DropLabel {
133    pub name: String,
134    pub if_exists: bool,
135}
136
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct DropEdgeType {
139    pub name: String,
140    pub if_exists: bool,
141}
142
143#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
144pub struct ShowConstraints {
145    pub target: Option<ConstraintTarget>,
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
149pub enum ConstraintTarget {
150    Label(String),
151    EdgeType(String),
152}
153
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct ShowIndexes {
156    pub filter: Option<String>,
157}
158
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct CopyToCommand {
161    pub label: String,
162    pub path: String,
163    pub format: String,
164    pub options: HashMap<String, Value>,
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct CopyFromCommand {
169    pub label: String,
170    pub path: String,
171    pub format: String,
172    pub options: HashMap<String, Value>,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub struct PropertyDefinition {
177    pub name: String,
178    pub data_type: String, // String representation of type
179    pub nullable: bool,
180    pub unique: bool,
181    pub default: Option<Expr>,
182}
183
184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct DropIndex {
186    pub name: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190pub struct CreateConstraint {
191    pub name: Option<String>,
192    pub constraint_type: ConstraintType,
193    pub label: String,
194    pub properties: Vec<String>,
195    pub expression: Option<Expr>,
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct DropConstraint {
200    pub name: String,
201}
202
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204pub enum ConstraintType {
205    Unique,
206    NodeKey,
207    Exists,
208    Check,
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct Statement {
213    pub clauses: Vec<Clause>,
214}
215
216#[derive(Debug, Clone, PartialEq)]
217pub enum ConstraintDef {
218    Unique(String),
219    NodeKey(Vec<String>),
220    Exists(String),
221    Check(Expr),
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
225pub enum Clause {
226    Match(MatchClause),
227    Create(CreateClause),
228    Merge(MergeClause),
229    With(WithClause),
230    WithRecursive(WithRecursiveClause),
231    Unwind(UnwindClause),
232    Return(ReturnClause),
233    Delete(DeleteClause),
234    Set(SetClause),
235    Remove(RemoveClause),
236    Call(CallClause),
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240pub struct MatchClause {
241    pub optional: bool,
242    pub pattern: Pattern,
243    pub where_clause: Option<Expr>,
244}
245
246#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
247pub struct CreateClause {
248    pub pattern: Pattern,
249}
250
251#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
252pub struct MergeClause {
253    pub pattern: Pattern,
254    pub on_match: Vec<SetItem>,
255    pub on_create: Vec<SetItem>,
256}
257
258#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
259pub struct WithClause {
260    pub distinct: bool,
261    pub items: Vec<ReturnItem>,
262    pub order_by: Option<Vec<SortItem>>,
263    pub skip: Option<Expr>,
264    pub limit: Option<Expr>,
265    pub where_clause: Option<Expr>,
266}
267
268#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
269pub struct WithRecursiveClause {
270    pub name: String,
271    pub query: Box<Query>,
272    pub items: Vec<ReturnItem>,
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub struct ReturnClause {
277    pub distinct: bool,
278    pub items: Vec<ReturnItem>,
279    pub order_by: Option<Vec<SortItem>>,
280    pub skip: Option<Expr>,
281    pub limit: Option<Expr>,
282}
283
284#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
285pub enum ReturnItem {
286    /// RETURN * - return all variables
287    All,
288    /// RETURN expr [AS alias]
289    Expr {
290        expr: Expr,
291        alias: Option<String>,
292        /// Original source text of the expression (for column naming).
293        #[serde(skip_serializing_if = "Option::is_none", default)]
294        source_text: Option<String>,
295    },
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299pub struct UnwindClause {
300    pub expr: Expr,
301    pub variable: String,
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
305pub struct DeleteClause {
306    pub detach: bool,
307    pub items: Vec<Expr>,
308}
309
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct SetClause {
312    pub items: Vec<SetItem>,
313}
314
315#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
316pub enum SetItem {
317    Property {
318        expr: Expr, // Expected to be a property access
319        value: Expr,
320    },
321    Labels {
322        variable: String,
323        labels: Vec<String>,
324    },
325    Variable {
326        variable: String,
327        value: Expr,
328    },
329    VariablePlus {
330        variable: String,
331        value: Expr,
332    },
333}
334
335#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
336pub struct RemoveClause {
337    pub items: Vec<RemoveItem>,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
341pub enum RemoveItem {
342    Property(Expr),
343    Labels {
344        variable: String,
345        labels: Vec<String>,
346    },
347}
348
349#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
350pub struct CallClause {
351    pub kind: CallKind,
352    pub yield_items: Vec<YieldItem>,
353    pub where_clause: Option<Expr>,
354}
355
356#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
357pub enum CallKind {
358    Procedure {
359        procedure: String,
360        arguments: Vec<Expr>,
361    },
362    Subquery(Box<Query>),
363}
364
365#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
366pub struct YieldItem {
367    pub name: String,
368    pub alias: Option<String>,
369}
370
371#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
372pub struct Pattern {
373    pub paths: Vec<PathPattern>,
374}
375
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct PathPattern {
378    pub variable: Option<String>,
379    pub elements: Vec<PatternElement>,
380    pub shortest_path_mode: Option<ShortestPathMode>,
381}
382
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
384pub enum ShortestPathMode {
385    Shortest,    // shortestPath(...)
386    AllShortest, // allShortestPaths(...)
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub enum PatternElement {
391    Node(NodePattern),
392    Relationship(RelationshipPattern),
393    Parenthesized {
394        pattern: Box<PathPattern>,
395        range: Option<Range>,
396    },
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub struct NodePattern {
401    pub variable: Option<String>,
402    pub labels: Vec<String>,
403    pub properties: Option<Expr>,   // Map literal
404    pub where_clause: Option<Expr>, // Inline WHERE clause
405}
406
407#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
408pub struct RelationshipPattern {
409    pub variable: Option<String>,
410    pub types: Vec<String>,
411    pub direction: Direction,
412    pub range: Option<Range>,
413    pub properties: Option<Expr>,   // Map literal
414    pub where_clause: Option<Expr>, // Inline WHERE clause
415}
416
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub enum Direction {
419    Outgoing,
420    Incoming,
421    Both,
422}
423
424#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
425pub struct Range {
426    pub min: Option<u32>,
427    pub max: Option<u32>,
428}
429
430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
431pub struct SortItem {
432    pub expr: Expr,
433    pub ascending: bool,
434}
435
436/// Window specification for window functions (OVER clause)
437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
438pub struct WindowSpec {
439    pub partition_by: Vec<Expr>,
440    pub order_by: Vec<SortItem>,
441}
442
443/// A typed Cypher literal value, replacing `serde_json::Value` in the AST.
444///
445/// This makes impossible states unrepresentable: no arrays/objects (those are
446/// `Expr::List`/`Expr::Map`), and integer vs. float is always known.
447#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
448pub enum CypherLiteral {
449    Null,
450    Bool(bool),
451    Integer(i64),
452    Float(f64),
453    String(String),
454    /// Pre-encoded CypherValue bytes (LargeBinary).
455    /// Used when a runtime Value (e.g. list or map) must round-trip through the
456    /// AST while preserving its CypherValue-encoded storage format.
457    Bytes(Vec<u8>),
458}
459
460impl CypherLiteral {
461    /// Convert to `uni_common::Value` for the executor.
462    pub fn to_value(&self) -> Value {
463        match self {
464            CypherLiteral::Null => Value::Null,
465            CypherLiteral::Bool(b) => Value::Bool(*b),
466            CypherLiteral::Integer(i) => Value::Int(*i),
467            CypherLiteral::Float(f) => Value::Float(*f),
468            CypherLiteral::String(s) => Value::String(s.clone()),
469            CypherLiteral::Bytes(b) => {
470                uni_common::cypher_value_codec::decode(b).unwrap_or(Value::Null)
471            }
472        }
473    }
474}
475
476impl std::fmt::Display for CypherLiteral {
477    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478        match self {
479            CypherLiteral::Null => f.write_str("null"),
480            CypherLiteral::Bool(b) => write!(f, "{b}"),
481            CypherLiteral::Integer(i) => write!(f, "{i}"),
482            CypherLiteral::Float(v) => write!(f, "{v}"),
483            CypherLiteral::String(s) => write!(f, "\"{s}\""),
484            CypherLiteral::Bytes(b) => write!(f, "<bytes:{}>", b.len()),
485        }
486    }
487}
488
489impl From<i64> for CypherLiteral {
490    fn from(v: i64) -> Self {
491        Self::Integer(v)
492    }
493}
494
495impl From<f64> for CypherLiteral {
496    fn from(v: f64) -> Self {
497        Self::Float(v)
498    }
499}
500
501impl From<bool> for CypherLiteral {
502    fn from(v: bool) -> Self {
503        Self::Bool(v)
504    }
505}
506
507impl From<String> for CypherLiteral {
508    fn from(v: String) -> Self {
509        Self::String(v)
510    }
511}
512
513impl From<&str> for CypherLiteral {
514    fn from(v: &str) -> Self {
515        Self::String(v.to_string())
516    }
517}
518
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520pub enum Expr {
521    Literal(CypherLiteral),
522    Parameter(String),
523    Variable(String),
524    Wildcard,
525    Property(Box<Expr>, String),
526    List(Vec<Expr>),
527    Map(Vec<(String, Expr)>),
528    FunctionCall {
529        name: String,
530        args: Vec<Expr>,
531        distinct: bool,
532        window_spec: Option<WindowSpec>,
533    },
534    BinaryOp {
535        left: Box<Expr>,
536        op: BinaryOp,
537        right: Box<Expr>,
538    },
539    UnaryOp {
540        op: UnaryOp,
541        expr: Box<Expr>,
542    },
543    Case {
544        expr: Option<Box<Expr>>,
545        when_then: Vec<(Expr, Expr)>,
546        else_expr: Option<Box<Expr>>,
547    },
548    Exists {
549        query: Box<Query>,
550        /// True when created from a bare pattern predicate `(n)-->()` in expression context.
551        from_pattern_predicate: bool,
552    },
553    CountSubquery(Box<Query>),
554    CollectSubquery(Box<Query>),
555    IsNull(Box<Expr>),
556    IsNotNull(Box<Expr>),
557    IsUnique(Box<Expr>),
558    In {
559        expr: Box<Expr>,
560        list: Box<Expr>,
561    },
562    // Array/list indexing and slicing
563    ArrayIndex {
564        array: Box<Expr>,
565        index: Box<Expr>,
566    },
567    ArraySlice {
568        array: Box<Expr>,
569        start: Option<Box<Expr>>,
570        end: Option<Box<Expr>>,
571    },
572    // Quantifier expressions: ALL, ANY, SINGLE, NONE
573    Quantifier {
574        quantifier: Quantifier,
575        variable: String,
576        list: Box<Expr>,
577        predicate: Box<Expr>,
578    },
579    // REDUCE expression: REDUCE(acc = init, var IN list | expr)
580    Reduce {
581        accumulator: String,
582        init: Box<Expr>,
583        variable: String,
584        list: Box<Expr>,
585        expr: Box<Expr>,
586    },
587    // List comprehension: [x IN list WHERE pred | expr]
588    ListComprehension {
589        variable: String,
590        list: Box<Expr>,
591        where_clause: Option<Box<Expr>>,
592        map_expr: Box<Expr>,
593    },
594    // Pattern comprehension: [p = (n)-->(m) WHERE pred | expr]
595    PatternComprehension {
596        path_variable: Option<String>,
597        pattern: Pattern,
598        where_clause: Option<Box<Expr>>,
599        map_expr: Box<Expr>,
600    },
601    // VALID_AT macro: e VALID_AT timestamp or e VALID_AT(timestamp, 'start', 'end')
602    ValidAt {
603        entity: Box<Expr>,
604        timestamp: Box<Expr>,
605        start_prop: Option<String>,
606        end_prop: Option<String>,
607    },
608    // Map projection: node{.name, .age, city: node.address.city}
609    MapProjection {
610        base: Box<Expr>,
611        items: Vec<MapProjectionItem>,
612    },
613    /// Label check expression: `a:B` or conjunctive `a:A:B`
614    LabelCheck {
615        expr: Box<Expr>,
616        labels: Vec<String>,
617    },
618}
619
620#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
621pub enum MapProjectionItem {
622    Property(String),                // .name
623    AllProperties,                   // .*
624    LiteralEntry(String, Box<Expr>), // key: expr
625    Variable(String),                // variable
626}
627
628impl MapProjectionItem {
629    fn to_string_repr(&self) -> String {
630        match self {
631            MapProjectionItem::Property(prop) => format!(".{prop}"),
632            MapProjectionItem::AllProperties => ".*".to_string(),
633            MapProjectionItem::LiteralEntry(key, expr) => {
634                format!("{key}: {}", expr.to_string_repr())
635            }
636            MapProjectionItem::Variable(v) => v.clone(),
637        }
638    }
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
642pub enum Quantifier {
643    All,
644    Any,
645    Single,
646    None,
647}
648
649impl std::fmt::Display for Quantifier {
650    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651        match self {
652            Quantifier::All => f.write_str("ALL"),
653            Quantifier::Any => f.write_str("ANY"),
654            Quantifier::Single => f.write_str("SINGLE"),
655            Quantifier::None => f.write_str("NONE"),
656        }
657    }
658}
659
660#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
661pub enum BinaryOp {
662    Add,
663    Sub,
664    Mul,
665    Div,
666    Mod,
667    Pow,
668    Eq,
669    NotEq,
670    Lt,
671    LtEq,
672    Gt,
673    GtEq,
674    And,
675    Or,
676    Xor,
677    Regex,
678    Contains,
679    StartsWith,
680    EndsWith,
681    ApproxEq,
682}
683
684impl std::fmt::Display for BinaryOp {
685    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686        let s = match self {
687            BinaryOp::Add => "+",
688            BinaryOp::Sub => "-",
689            BinaryOp::Mul => "*",
690            BinaryOp::Div => "/",
691            BinaryOp::Mod => "%",
692            BinaryOp::Pow => "^",
693            BinaryOp::Eq => "=",
694            BinaryOp::NotEq => "<>",
695            BinaryOp::Lt => "<",
696            BinaryOp::LtEq => "<=",
697            BinaryOp::Gt => ">",
698            BinaryOp::GtEq => ">=",
699            BinaryOp::And => "AND",
700            BinaryOp::Or => "OR",
701            BinaryOp::Xor => "XOR",
702            BinaryOp::Regex => "=~",
703            BinaryOp::Contains => "CONTAINS",
704            BinaryOp::StartsWith => "STARTS WITH",
705            BinaryOp::EndsWith => "ENDS WITH",
706            BinaryOp::ApproxEq => "~=",
707        };
708        f.write_str(s)
709    }
710}
711
712#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
713pub enum UnaryOp {
714    Not,
715    Neg,
716}
717
718impl std::fmt::Display for UnaryOp {
719    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
720        match self {
721            UnaryOp::Not => f.write_str("NOT "),
722            UnaryOp::Neg => f.write_str("-"),
723        }
724    }
725}
726
727// ============================================================================
728// Parser Helper Types (Internal - Used for resolving [Identifier ambiguity)
729// ============================================================================
730
731/// Intermediate type for resolving [Identifier ambiguity in list expressions.
732/// After parsing "[" Identifier, we branch based on the next token to determine
733/// whether this is a list comprehension or a list literal.
734#[derive(Debug, Clone)]
735pub enum ListAfterIdentifier {
736    /// [x IN list WHERE pred | expr] - List comprehension
737    Comprehension {
738        list: Expr,
739        filter: Option<Expr>,
740        projection: Box<Expr>,
741    },
742
743    /// List literal: \[id, ...\], \[id.prop, ...\], or \[id + 1, ...\]
744    /// Empty suffixes means the identifier stands alone as the first element.
745    ExpressionTail {
746        suffix: Vec<ExprSuffix>,
747        more: Vec<Expr>,
748    },
749}
750
751impl ListAfterIdentifier {
752    /// Resolve this intermediate representation into a final Expr, given the identifier.
753    pub fn resolve(self, id: String) -> Expr {
754        match self {
755            ListAfterIdentifier::Comprehension {
756                list,
757                filter,
758                projection,
759            } => Expr::ListComprehension {
760                variable: id,
761                list: Box::new(list),
762                where_clause: filter.map(Box::new),
763                map_expr: projection,
764            },
765            ListAfterIdentifier::ExpressionTail { suffix, more } => {
766                let first = apply_suffixes(Expr::Variable(id), suffix);
767                let items = std::iter::once(first).chain(more).collect();
768                Expr::List(items)
769            }
770        }
771    }
772}
773
774/// Expression suffix for building complex expressions after an identifier.
775/// Used to parse things like: id.prop, id\[0\], id(), id+1, etc.
776#[derive(Debug, Clone)]
777pub enum ExprSuffix {
778    Property(String),
779    Index(Expr),
780    Slice {
781        start: Option<Expr>,
782        end: Option<Expr>,
783    },
784    FunctionCall(Vec<Expr>),
785    IsNull,
786    IsNotNull,
787    Binary(BinaryOp, Expr),
788    In(Expr),
789}
790
791/// Postfix operations for building expressions from primary expressions.
792///
793/// Used by the parser's `PostfixExpression` rule to collect operations like
794/// property access (`.prop`), function calls (`(args)`), and indexing (`[i]`).
795/// This approach avoids LR(1) conflicts that would occur with left-recursive
796/// grammar rules for dotted function names like `uni.vector.query()`.
797///
798/// Note: This is separate from `ExprSuffix` which serves the list comprehension
799/// factoring logic (resolving `[Identifier ...` ambiguity).
800#[derive(Debug, Clone, PartialEq)]
801pub enum PostfixSuffix {
802    Property(String),
803    Call {
804        args: Vec<Expr>,
805        distinct: bool,
806        window_spec: Option<WindowSpec>,
807    },
808    Index(Expr),
809    Slice {
810        start: Option<Expr>,
811        end: Option<Expr>,
812    },
813    MapProjection(Vec<MapProjectionItem>),
814}
815
816/// Extracts a dotted name from a variable or property chain.
817///
818/// Used to convert property access chains into dotted function names.
819///
820/// # Examples
821///
822/// - `Variable("func")` => `Some("func")`
823/// - `Property(Variable("uni"), "validAt")` => `Some("uni.validAt")`
824/// - `Property(Property(Variable("db"), "idx"), "query")` => `Some("db.idx.query")`
825///
826/// Returns `None` for expressions that are not simple identifier chains.
827pub fn extract_dotted_name(expr: &Expr) -> Option<String> {
828    match expr {
829        Expr::Variable(name) => Some(name.clone()),
830        Expr::Property(base, prop) => {
831            let base_name = extract_dotted_name(base)?;
832            Some(format!("{base_name}.{prop}"))
833        }
834        _ => None,
835    }
836}
837
838/// Applies a postfix suffix to an expression, building a new expression.
839///
840/// This function handles the key transformation for dotted function names:
841/// when a `Call` suffix follows a property chain like `db.idx.vector`,
842/// it extracts the dotted name and creates a `FunctionCall` expression.
843pub fn apply_suffix(expr: Expr, suffix: PostfixSuffix) -> Expr {
844    match suffix {
845        PostfixSuffix::Property(prop) => Expr::Property(Box::new(expr), prop),
846
847        PostfixSuffix::Call {
848            args,
849            distinct,
850            window_spec,
851        } => {
852            let name = extract_dotted_name(&expr).unwrap_or_else(|| {
853                panic!(
854                    "apply_suffix: function call requires variable or property chain, got: {expr:?}"
855                )
856            });
857            Expr::FunctionCall {
858                name,
859                args,
860                distinct,
861                window_spec,
862            }
863        }
864
865        PostfixSuffix::Index(index) => Expr::ArrayIndex {
866            array: Box::new(expr),
867            index: Box::new(index),
868        },
869
870        PostfixSuffix::Slice { start, end } => Expr::ArraySlice {
871            array: Box::new(expr),
872            start: start.map(Box::new),
873            end: end.map(Box::new),
874        },
875
876        PostfixSuffix::MapProjection(items) => Expr::MapProjection {
877            base: Box::new(expr),
878            items,
879        },
880    }
881}
882
883/// Apply a sequence of expression suffixes to build a complete expression.
884/// Example: id.prop[0] + 1 => ((id.prop)[0]) + 1
885fn apply_suffixes(mut expr: Expr, suffixes: Vec<ExprSuffix>) -> Expr {
886    for suffix in suffixes {
887        expr = match suffix {
888            ExprSuffix::Property(name) => Expr::Property(Box::new(expr), name),
889
890            ExprSuffix::Index(idx) => Expr::ArrayIndex {
891                array: Box::new(expr),
892                index: Box::new(idx),
893            },
894
895            ExprSuffix::Slice { start, end } => Expr::ArraySlice {
896                array: Box::new(expr),
897                start: start.map(Box::new),
898                end: end.map(Box::new),
899            },
900
901            ExprSuffix::FunctionCall(args) => {
902                let name = extract_dotted_name(&expr)
903                    .unwrap_or_else(|| panic!("Function call suffix requires variable or property chain expression, got: {expr:?}"));
904                Expr::FunctionCall {
905                    name,
906                    args,
907                    distinct: false,
908                    window_spec: None,
909                }
910            }
911
912            ExprSuffix::IsNull => Expr::IsNull(Box::new(expr)),
913            ExprSuffix::IsNotNull => Expr::IsNotNull(Box::new(expr)),
914
915            ExprSuffix::Binary(op, rhs) => Expr::BinaryOp {
916                left: Box::new(expr),
917                op,
918                right: Box::new(rhs),
919            },
920
921            ExprSuffix::In(right) => Expr::In {
922                expr: Box::new(expr),
923                list: Box::new(right),
924            },
925        };
926    }
927    expr
928}
929
930/// Join a slice of expressions with a separator using `to_string_repr`.
931fn join_exprs(exprs: &[Expr], sep: &str) -> String {
932    exprs
933        .iter()
934        .map(|e| e.to_string_repr())
935        .collect::<Vec<_>>()
936        .join(sep)
937}
938
939impl Expr {
940    /// Sentinel expression representing a literal `true`.
941    ///
942    /// Useful in the planner for predicate reduction: when all conjuncts have
943    /// been pushed down, the remaining predicate is replaced with this constant.
944    pub const TRUE: Expr = Expr::Literal(CypherLiteral::Bool(true));
945
946    /// Returns `true` if this expression is the literal boolean `true`.
947    pub fn is_true_literal(&self) -> bool {
948        matches!(self, Expr::Literal(CypherLiteral::Bool(true)))
949    }
950
951    /// Extract a simple variable name if this expression is just a variable reference
952    pub fn extract_variable(&self) -> Option<String> {
953        match self {
954            Expr::Variable(v) => Some(v.clone()),
955            _ => None,
956        }
957    }
958
959    /// Substitute all occurrences of a variable with a new variable name
960    pub fn substitute_variable(&self, old_var: &str, new_var: &str) -> Expr {
961        let sub = |e: &Expr| e.substitute_variable(old_var, new_var);
962        let sub_box = |e: &Expr| Box::new(sub(e));
963        let sub_opt = |o: &Option<Box<Expr>>| o.as_ref().map(|e| sub_box(e));
964        let sub_vec = |v: &[Expr]| v.iter().map(sub).collect();
965
966        match self {
967            Expr::Variable(v) if v == old_var => Expr::Variable(new_var.to_string()),
968            Expr::Variable(_) | Expr::Literal(_) | Expr::Parameter(_) | Expr::Wildcard => {
969                self.clone()
970            }
971
972            Expr::Property(base, prop) => Expr::Property(sub_box(base), prop.clone()),
973
974            Expr::List(exprs) => Expr::List(sub_vec(exprs)),
975
976            Expr::Map(entries) => {
977                Expr::Map(entries.iter().map(|(k, v)| (k.clone(), sub(v))).collect())
978            }
979
980            Expr::FunctionCall {
981                name,
982                args,
983                distinct,
984                window_spec,
985            } => Expr::FunctionCall {
986                name: name.clone(),
987                args: sub_vec(args),
988                distinct: *distinct,
989                window_spec: window_spec.clone(),
990            },
991
992            Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
993                left: sub_box(left),
994                op: *op,
995                right: sub_box(right),
996            },
997
998            Expr::UnaryOp { op, expr } => Expr::UnaryOp {
999                op: *op,
1000                expr: sub_box(expr),
1001            },
1002
1003            Expr::Case {
1004                expr,
1005                when_then,
1006                else_expr,
1007            } => Expr::Case {
1008                expr: sub_opt(expr),
1009                when_then: when_then.iter().map(|(w, t)| (sub(w), sub(t))).collect(),
1010                else_expr: sub_opt(else_expr),
1011            },
1012
1013            // Don't substitute inside subqueries
1014            Expr::Exists { .. } | Expr::CountSubquery(_) | Expr::CollectSubquery(_) => self.clone(),
1015
1016            Expr::IsNull(e) => Expr::IsNull(sub_box(e)),
1017            Expr::IsNotNull(e) => Expr::IsNotNull(sub_box(e)),
1018            Expr::IsUnique(e) => Expr::IsUnique(sub_box(e)),
1019
1020            Expr::In { expr, list } => Expr::In {
1021                expr: sub_box(expr),
1022                list: sub_box(list),
1023            },
1024
1025            Expr::ArrayIndex { array, index } => Expr::ArrayIndex {
1026                array: sub_box(array),
1027                index: sub_box(index),
1028            },
1029
1030            Expr::ArraySlice { array, start, end } => Expr::ArraySlice {
1031                array: sub_box(array),
1032                start: sub_opt(start),
1033                end: sub_opt(end),
1034            },
1035
1036            Expr::Quantifier {
1037                quantifier,
1038                variable,
1039                list,
1040                predicate,
1041            } => {
1042                let shadowed = variable == old_var;
1043                Expr::Quantifier {
1044                    quantifier: *quantifier,
1045                    variable: variable.clone(),
1046                    list: sub_box(list),
1047                    predicate: if shadowed {
1048                        predicate.clone()
1049                    } else {
1050                        sub_box(predicate)
1051                    },
1052                }
1053            }
1054
1055            Expr::Reduce {
1056                accumulator,
1057                init,
1058                variable,
1059                list,
1060                expr,
1061            } => {
1062                let shadowed = variable == old_var || accumulator == old_var;
1063                Expr::Reduce {
1064                    accumulator: accumulator.clone(),
1065                    init: sub_box(init),
1066                    variable: variable.clone(),
1067                    list: sub_box(list),
1068                    expr: if shadowed {
1069                        expr.clone()
1070                    } else {
1071                        sub_box(expr)
1072                    },
1073                }
1074            }
1075
1076            Expr::ListComprehension {
1077                variable,
1078                list,
1079                where_clause,
1080                map_expr,
1081            } => {
1082                let shadowed = variable == old_var;
1083                Expr::ListComprehension {
1084                    variable: variable.clone(),
1085                    list: sub_box(list),
1086                    where_clause: if shadowed {
1087                        where_clause.clone()
1088                    } else {
1089                        sub_opt(where_clause)
1090                    },
1091                    map_expr: if shadowed {
1092                        map_expr.clone()
1093                    } else {
1094                        sub_box(map_expr)
1095                    },
1096                }
1097            }
1098
1099            Expr::PatternComprehension {
1100                path_variable,
1101                pattern,
1102                where_clause,
1103                map_expr,
1104            } => {
1105                if path_variable.as_deref() == Some(old_var) {
1106                    self.clone()
1107                } else {
1108                    Expr::PatternComprehension {
1109                        path_variable: path_variable.clone(),
1110                        pattern: pattern.clone(),
1111                        where_clause: sub_opt(where_clause),
1112                        map_expr: sub_box(map_expr),
1113                    }
1114                }
1115            }
1116
1117            Expr::ValidAt {
1118                entity,
1119                timestamp,
1120                start_prop,
1121                end_prop,
1122            } => Expr::ValidAt {
1123                entity: sub_box(entity),
1124                timestamp: sub_box(timestamp),
1125                start_prop: start_prop.clone(),
1126                end_prop: end_prop.clone(),
1127            },
1128
1129            Expr::MapProjection { base, items } => Expr::MapProjection {
1130                base: sub_box(base),
1131                items: items
1132                    .iter()
1133                    .map(|item| match item {
1134                        MapProjectionItem::LiteralEntry(key, expr) => {
1135                            MapProjectionItem::LiteralEntry(key.clone(), sub_box(expr))
1136                        }
1137                        MapProjectionItem::Variable(v) if v == old_var => {
1138                            MapProjectionItem::Variable(new_var.to_string())
1139                        }
1140                        other => other.clone(),
1141                    })
1142                    .collect(),
1143            },
1144
1145            Expr::LabelCheck { expr, labels } => Expr::LabelCheck {
1146                expr: sub_box(expr),
1147                labels: labels.clone(),
1148            },
1149        }
1150    }
1151
1152    /// Check if this expression contains an aggregate function
1153    pub fn is_aggregate(&self) -> bool {
1154        match self {
1155            Expr::FunctionCall {
1156                name, window_spec, ..
1157            } => {
1158                window_spec.is_none()
1159                    && matches!(
1160                        name.to_lowercase().as_str(),
1161                        "count"
1162                            | "sum"
1163                            | "avg"
1164                            | "min"
1165                            | "max"
1166                            | "collect"
1167                            | "stdev"
1168                            | "stdevp"
1169                            | "percentiledisc"
1170                            | "percentilecont"
1171                    )
1172            }
1173            Expr::CountSubquery(_) | Expr::CollectSubquery(_) => true,
1174            Expr::Property(base, _) => base.is_aggregate(),
1175            Expr::List(exprs) => exprs.iter().any(|e| e.is_aggregate()),
1176            Expr::Map(entries) => entries.iter().any(|(_, v)| v.is_aggregate()),
1177            Expr::BinaryOp { left, right, .. } => left.is_aggregate() || right.is_aggregate(),
1178            Expr::UnaryOp { expr, .. } => expr.is_aggregate(),
1179            Expr::Case {
1180                expr,
1181                when_then,
1182                else_expr,
1183            } => {
1184                expr.as_ref().is_some_and(|e| e.is_aggregate())
1185                    || when_then
1186                        .iter()
1187                        .any(|(w, t)| w.is_aggregate() || t.is_aggregate())
1188                    || else_expr.as_ref().is_some_and(|e| e.is_aggregate())
1189            }
1190            Expr::In { expr, list } => expr.is_aggregate() || list.is_aggregate(),
1191            Expr::IsNull(e) | Expr::IsNotNull(e) | Expr::IsUnique(e) => e.is_aggregate(),
1192            Expr::ArrayIndex { array, index } => array.is_aggregate() || index.is_aggregate(),
1193            Expr::ArraySlice { array, start, end } => {
1194                array.is_aggregate()
1195                    || start.as_ref().is_some_and(|e| e.is_aggregate())
1196                    || end.as_ref().is_some_and(|e| e.is_aggregate())
1197            }
1198            Expr::Quantifier {
1199                list, predicate, ..
1200            } => list.is_aggregate() || predicate.is_aggregate(),
1201            Expr::Reduce {
1202                init, list, expr, ..
1203            } => init.is_aggregate() || list.is_aggregate() || expr.is_aggregate(),
1204            Expr::ListComprehension {
1205                list,
1206                where_clause,
1207                map_expr,
1208                ..
1209            } => {
1210                list.is_aggregate()
1211                    || where_clause.as_ref().is_some_and(|e| e.is_aggregate())
1212                    || map_expr.is_aggregate()
1213            }
1214            Expr::PatternComprehension {
1215                where_clause,
1216                map_expr,
1217                ..
1218            } => where_clause.as_ref().is_some_and(|e| e.is_aggregate()) || map_expr.is_aggregate(),
1219            _ => false,
1220        }
1221    }
1222
1223    /// Generate a string representation of this expression for debugging/display
1224    pub fn to_string_repr(&self) -> String {
1225        match self {
1226            Expr::Literal(v) => v.to_string(),
1227            Expr::Parameter(p) => format!("${p}"),
1228            Expr::Variable(v) => v.clone(),
1229            Expr::Wildcard => "*".to_string(),
1230            Expr::Property(base, prop) => {
1231                format!("{}.{prop}", base.to_string_repr())
1232            }
1233            Expr::List(exprs) => format!("[{}]", join_exprs(exprs, ", ")),
1234            Expr::Map(entries) => {
1235                let items = entries
1236                    .iter()
1237                    .map(|(k, v)| format!("{k}: {}", v.to_string_repr()))
1238                    .collect::<Vec<_>>()
1239                    .join(", ");
1240                format!("{{{items}}}")
1241            }
1242            Expr::FunctionCall {
1243                name,
1244                args,
1245                distinct,
1246                window_spec,
1247            } => {
1248                let args_str = join_exprs(args, ", ");
1249                let distinct_str = if *distinct { "DISTINCT " } else { "" };
1250                let base = format!("{name}({distinct_str}{args_str})");
1251                let Some(window) = window_spec else {
1252                    return base;
1253                };
1254                let mut parts = Vec::new();
1255                if !window.partition_by.is_empty() {
1256                    parts.push(format!(
1257                        "PARTITION BY {}",
1258                        join_exprs(&window.partition_by, ", ")
1259                    ));
1260                }
1261                if !window.order_by.is_empty() {
1262                    let items = window
1263                        .order_by
1264                        .iter()
1265                        .map(|s| {
1266                            let dir = if s.ascending { "ASC" } else { "DESC" };
1267                            format!("{} {dir}", s.expr.to_string_repr())
1268                        })
1269                        .collect::<Vec<_>>()
1270                        .join(", ");
1271                    parts.push(format!("ORDER BY {items}"));
1272                }
1273                format!("{base} OVER ({})", parts.join(" "))
1274            }
1275            Expr::BinaryOp { left, op, right } => {
1276                format!(
1277                    "{} {} {}",
1278                    left.to_string_repr(),
1279                    op,
1280                    right.to_string_repr()
1281                )
1282            }
1283            Expr::UnaryOp { op, expr } => {
1284                format!("{op}{}", expr.to_string_repr())
1285            }
1286            Expr::Case {
1287                expr,
1288                when_then,
1289                else_expr,
1290            } => {
1291                let mut s = "CASE".to_string();
1292                if let Some(e) = expr {
1293                    s.push_str(&format!(" {}", e.to_string_repr()));
1294                }
1295                for (w, t) in when_then {
1296                    s.push_str(&format!(
1297                        " WHEN {} THEN {}",
1298                        w.to_string_repr(),
1299                        t.to_string_repr()
1300                    ));
1301                }
1302                if let Some(e) = else_expr {
1303                    s.push_str(&format!(" ELSE {}", e.to_string_repr()));
1304                }
1305                s.push_str(" END");
1306                s
1307            }
1308            Expr::Exists { .. } => "EXISTS {...}".to_string(),
1309            Expr::CountSubquery(_) => "COUNT {...}".to_string(),
1310            Expr::CollectSubquery(_) => "COLLECT {...}".to_string(),
1311            Expr::IsNull(e) => format!("{} IS NULL", e.to_string_repr()),
1312            Expr::IsNotNull(e) => format!("{} IS NOT NULL", e.to_string_repr()),
1313            Expr::IsUnique(e) => format!("{} IS UNIQUE", e.to_string_repr()),
1314            Expr::In { expr, list } => {
1315                format!("{} IN {}", expr.to_string_repr(), list.to_string_repr())
1316            }
1317            Expr::ArrayIndex { array, index } => {
1318                format!("{}[{}]", array.to_string_repr(), index.to_string_repr())
1319            }
1320            Expr::ArraySlice { array, start, end } => {
1321                let start_str = start
1322                    .as_ref()
1323                    .map(|e| e.to_string_repr())
1324                    .unwrap_or_default();
1325                let end_str = end.as_ref().map(|e| e.to_string_repr()).unwrap_or_default();
1326                format!("{}[{}..{}]", array.to_string_repr(), start_str, end_str)
1327            }
1328            Expr::Quantifier {
1329                quantifier,
1330                variable,
1331                list,
1332                predicate,
1333            } => {
1334                format!(
1335                    "{quantifier}({variable} IN {} WHERE {})",
1336                    list.to_string_repr(),
1337                    predicate.to_string_repr()
1338                )
1339            }
1340            Expr::Reduce {
1341                accumulator,
1342                init,
1343                variable,
1344                list,
1345                expr,
1346            } => {
1347                format!(
1348                    "REDUCE({accumulator} = {}, {variable} IN {} | {})",
1349                    init.to_string_repr(),
1350                    list.to_string_repr(),
1351                    expr.to_string_repr()
1352                )
1353            }
1354
1355            Expr::ListComprehension {
1356                variable,
1357                list,
1358                where_clause,
1359                map_expr,
1360            } => {
1361                let where_str = where_clause
1362                    .as_ref()
1363                    .map_or(String::new(), |e| format!(" WHERE {}", e.to_string_repr()));
1364                format!(
1365                    "[{variable} IN {}{where_str}  | {}]",
1366                    list.to_string_repr(),
1367                    map_expr.to_string_repr()
1368                )
1369            }
1370
1371            Expr::PatternComprehension {
1372                path_variable,
1373                pattern,
1374                where_clause,
1375                map_expr,
1376            } => {
1377                let var_part = path_variable
1378                    .as_ref()
1379                    .map(|v| format!("{v} = "))
1380                    .unwrap_or_default();
1381                let where_str = where_clause
1382                    .as_ref()
1383                    .map_or(String::new(), |e| format!(" WHERE {}", e.to_string_repr()));
1384                format!(
1385                    "[{var_part}{pattern:?}{where_str} | {}]",
1386                    map_expr.to_string_repr()
1387                )
1388            }
1389
1390            Expr::ValidAt {
1391                entity,
1392                timestamp,
1393                start_prop,
1394                end_prop,
1395            } => match (start_prop, end_prop) {
1396                (Some(start), Some(end)) => format!(
1397                    "{} VALID_AT({}, '{start}', '{end}')",
1398                    entity.to_string_repr(),
1399                    timestamp.to_string_repr(),
1400                ),
1401                _ => format!(
1402                    "{} VALID_AT {}",
1403                    entity.to_string_repr(),
1404                    timestamp.to_string_repr(),
1405                ),
1406            },
1407
1408            Expr::MapProjection { base, items } => {
1409                let items_str = items
1410                    .iter()
1411                    .map(MapProjectionItem::to_string_repr)
1412                    .collect::<Vec<_>>()
1413                    .join(", ");
1414                format!("{}{{{items_str}}}", base.to_string_repr())
1415            }
1416
1417            Expr::LabelCheck { expr, labels } => {
1418                let labels_str: String = labels.iter().map(|l| format!(":{l}")).collect();
1419                format!("{}{labels_str}", expr.to_string_repr())
1420            }
1421        }
1422    }
1423
1424    /// Call `f` on each direct child expression (non-recursive).
1425    ///
1426    /// Does NOT descend into subqueries (Exists, CountSubquery, CollectSubquery)
1427    /// since those have separate variable scope.
1428    pub fn for_each_child(&self, f: &mut dyn FnMut(&Expr)) {
1429        match self {
1430            Expr::Literal(_) | Expr::Parameter(_) | Expr::Variable(_) | Expr::Wildcard => {}
1431            Expr::Property(base, _) => f(base),
1432            Expr::List(items) => {
1433                for item in items {
1434                    f(item);
1435                }
1436            }
1437            Expr::Map(entries) => {
1438                for (_, expr) in entries {
1439                    f(expr);
1440                }
1441            }
1442            Expr::FunctionCall { args, .. } => {
1443                for arg in args {
1444                    f(arg);
1445                }
1446            }
1447            Expr::BinaryOp { left, right, .. } => {
1448                f(left);
1449                f(right);
1450            }
1451            Expr::UnaryOp { expr, .. } => f(expr),
1452            Expr::Case {
1453                expr,
1454                when_then,
1455                else_expr,
1456            } => {
1457                if let Some(e) = expr {
1458                    f(e);
1459                }
1460                for (w, t) in when_then {
1461                    f(w);
1462                    f(t);
1463                }
1464                if let Some(e) = else_expr {
1465                    f(e);
1466                }
1467            }
1468            Expr::Exists { .. } | Expr::CountSubquery(_) | Expr::CollectSubquery(_) => {}
1469            Expr::IsNull(e) | Expr::IsNotNull(e) | Expr::IsUnique(e) => f(e),
1470            Expr::In { expr, list } => {
1471                f(expr);
1472                f(list);
1473            }
1474            Expr::ArrayIndex { array, index } => {
1475                f(array);
1476                f(index);
1477            }
1478            Expr::ArraySlice { array, start, end } => {
1479                f(array);
1480                if let Some(s) = start {
1481                    f(s);
1482                }
1483                if let Some(e) = end {
1484                    f(e);
1485                }
1486            }
1487            Expr::Quantifier {
1488                list, predicate, ..
1489            } => {
1490                f(list);
1491                f(predicate);
1492            }
1493            Expr::Reduce {
1494                init, list, expr, ..
1495            } => {
1496                f(init);
1497                f(list);
1498                f(expr);
1499            }
1500            Expr::ListComprehension {
1501                list,
1502                where_clause,
1503                map_expr,
1504                ..
1505            } => {
1506                f(list);
1507                if let Some(w) = where_clause {
1508                    f(w);
1509                }
1510                f(map_expr);
1511            }
1512            Expr::PatternComprehension {
1513                where_clause,
1514                map_expr,
1515                ..
1516            } => {
1517                if let Some(w) = where_clause {
1518                    f(w);
1519                }
1520                f(map_expr);
1521            }
1522            Expr::ValidAt {
1523                entity, timestamp, ..
1524            } => {
1525                f(entity);
1526                f(timestamp);
1527            }
1528            Expr::MapProjection { base, items } => {
1529                f(base);
1530                for item in items {
1531                    if let MapProjectionItem::LiteralEntry(_, expr) = item {
1532                        f(expr);
1533                    }
1534                }
1535            }
1536            Expr::LabelCheck { expr, .. } => f(expr),
1537        }
1538    }
1539
1540    /// Map each direct child expression through `f`, producing a new Expr.
1541    ///
1542    /// Same scoping rules as `for_each_child`: does not descend into subqueries.
1543    pub fn map_children(self, f: &mut dyn FnMut(Expr) -> Expr) -> Expr {
1544        match self {
1545            Expr::Literal(_) | Expr::Parameter(_) | Expr::Variable(_) | Expr::Wildcard => self,
1546            Expr::Property(base, prop) => Expr::Property(Box::new(f(*base)), prop),
1547            Expr::List(items) => Expr::List(items.into_iter().map(&mut *f).collect()),
1548            Expr::Map(entries) => Expr::Map(entries.into_iter().map(|(k, v)| (k, f(v))).collect()),
1549            Expr::FunctionCall {
1550                name,
1551                args,
1552                distinct,
1553                window_spec,
1554            } => Expr::FunctionCall {
1555                name,
1556                args: args.into_iter().map(&mut *f).collect(),
1557                distinct,
1558                window_spec,
1559            },
1560            Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
1561                left: Box::new(f(*left)),
1562                op,
1563                right: Box::new(f(*right)),
1564            },
1565            Expr::UnaryOp { op, expr } => Expr::UnaryOp {
1566                op,
1567                expr: Box::new(f(*expr)),
1568            },
1569            Expr::Case {
1570                expr,
1571                when_then,
1572                else_expr,
1573            } => Expr::Case {
1574                expr: expr.map(|e| Box::new(f(*e))),
1575                when_then: when_then.into_iter().map(|(w, t)| (f(w), f(t))).collect(),
1576                else_expr: else_expr.map(|e| Box::new(f(*e))),
1577            },
1578            Expr::Exists { .. } | Expr::CountSubquery(_) | Expr::CollectSubquery(_) => self,
1579            Expr::IsNull(e) => Expr::IsNull(Box::new(f(*e))),
1580            Expr::IsNotNull(e) => Expr::IsNotNull(Box::new(f(*e))),
1581            Expr::IsUnique(e) => Expr::IsUnique(Box::new(f(*e))),
1582            Expr::In { expr, list } => Expr::In {
1583                expr: Box::new(f(*expr)),
1584                list: Box::new(f(*list)),
1585            },
1586            Expr::ArrayIndex { array, index } => Expr::ArrayIndex {
1587                array: Box::new(f(*array)),
1588                index: Box::new(f(*index)),
1589            },
1590            Expr::ArraySlice { array, start, end } => Expr::ArraySlice {
1591                array: Box::new(f(*array)),
1592                start: start.map(|s| Box::new(f(*s))),
1593                end: end.map(|e| Box::new(f(*e))),
1594            },
1595            Expr::Quantifier {
1596                quantifier,
1597                variable,
1598                list,
1599                predicate,
1600            } => Expr::Quantifier {
1601                quantifier,
1602                variable,
1603                list: Box::new(f(*list)),
1604                predicate: Box::new(f(*predicate)),
1605            },
1606            Expr::Reduce {
1607                accumulator,
1608                init,
1609                variable,
1610                list,
1611                expr,
1612            } => Expr::Reduce {
1613                accumulator,
1614                init: Box::new(f(*init)),
1615                variable,
1616                list: Box::new(f(*list)),
1617                expr: Box::new(f(*expr)),
1618            },
1619            Expr::ListComprehension {
1620                variable,
1621                list,
1622                where_clause,
1623                map_expr,
1624            } => Expr::ListComprehension {
1625                variable,
1626                list: Box::new(f(*list)),
1627                where_clause: where_clause.map(|w| Box::new(f(*w))),
1628                map_expr: Box::new(f(*map_expr)),
1629            },
1630            Expr::PatternComprehension {
1631                path_variable,
1632                pattern,
1633                where_clause,
1634                map_expr,
1635            } => Expr::PatternComprehension {
1636                path_variable,
1637                pattern,
1638                where_clause: where_clause.map(|w| Box::new(f(*w))),
1639                map_expr: Box::new(f(*map_expr)),
1640            },
1641            Expr::ValidAt {
1642                entity,
1643                timestamp,
1644                start_prop,
1645                end_prop,
1646            } => Expr::ValidAt {
1647                entity: Box::new(f(*entity)),
1648                timestamp: Box::new(f(*timestamp)),
1649                start_prop,
1650                end_prop,
1651            },
1652            Expr::MapProjection { base, items } => Expr::MapProjection {
1653                base: Box::new(f(*base)),
1654                items: items
1655                    .into_iter()
1656                    .map(|item| match item {
1657                        MapProjectionItem::LiteralEntry(key, expr) => {
1658                            MapProjectionItem::LiteralEntry(key, Box::new(f(*expr)))
1659                        }
1660                        other => other,
1661                    })
1662                    .collect(),
1663            },
1664            Expr::LabelCheck { expr, labels } => Expr::LabelCheck {
1665                expr: Box::new(f(*expr)),
1666                labels,
1667            },
1668        }
1669    }
1670}