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