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                            | "stdevp"
1206                            | "percentiledisc"
1207                            | "percentilecont"
1208                    ) || crate::plugin_aggregates::is_known_plugin_aggregate(name))
1209            }
1210            Expr::CountSubquery(_) | Expr::CollectSubquery(_) => true,
1211            // All other variants: recurse into direct children. `for_each_child`
1212            // already returns nothing for leaves (Literal/Parameter/Variable/Wildcard)
1213            // and skips into `Exists` — both of which used to fall through the
1214            // original wildcard `_ => false` arm with identical effect.
1215            _ => {
1216                let mut found = false;
1217                self.for_each_child(&mut |c| {
1218                    if !found && c.is_aggregate() {
1219                        found = true;
1220                    }
1221                });
1222                found
1223            }
1224        }
1225    }
1226
1227    /// Generate a string representation of this expression for debugging/display
1228    pub fn to_string_repr(&self) -> String {
1229        match self {
1230            Expr::Literal(v) => v.to_string(),
1231            Expr::Parameter(p) => format!("${p}"),
1232            Expr::Variable(v) => v.clone(),
1233            Expr::Wildcard => "*".to_string(),
1234            Expr::Property(base, prop) => {
1235                format!("{}.{prop}", base.to_string_repr())
1236            }
1237            Expr::List(exprs) => format!("[{}]", join_exprs(exprs, ", ")),
1238            Expr::Map(entries) => {
1239                let items = entries
1240                    .iter()
1241                    .map(|(k, v)| format!("{k}: {}", v.to_string_repr()))
1242                    .collect::<Vec<_>>()
1243                    .join(", ");
1244                format!("{{{items}}}")
1245            }
1246            Expr::FunctionCall {
1247                name,
1248                args,
1249                distinct,
1250                window_spec,
1251            } => {
1252                let args_str = join_exprs(args, ", ");
1253                let distinct_str = if *distinct { "DISTINCT " } else { "" };
1254                let base = format!("{name}({distinct_str}{args_str})");
1255                let Some(window) = window_spec else {
1256                    return base;
1257                };
1258                let mut parts = Vec::new();
1259                if !window.partition_by.is_empty() {
1260                    parts.push(format!(
1261                        "PARTITION BY {}",
1262                        join_exprs(&window.partition_by, ", ")
1263                    ));
1264                }
1265                if !window.order_by.is_empty() {
1266                    let items = window
1267                        .order_by
1268                        .iter()
1269                        .map(|s| {
1270                            let dir = if s.ascending { "ASC" } else { "DESC" };
1271                            format!("{} {dir}", s.expr.to_string_repr())
1272                        })
1273                        .collect::<Vec<_>>()
1274                        .join(", ");
1275                    parts.push(format!("ORDER BY {items}"));
1276                }
1277                format!("{base} OVER ({})", parts.join(" "))
1278            }
1279            Expr::BinaryOp { left, op, right } => {
1280                format!(
1281                    "{} {} {}",
1282                    left.to_string_repr(),
1283                    op,
1284                    right.to_string_repr()
1285                )
1286            }
1287            Expr::UnaryOp { op, expr } => {
1288                format!("{op}{}", expr.to_string_repr())
1289            }
1290            Expr::Case {
1291                expr,
1292                when_then,
1293                else_expr,
1294            } => {
1295                let mut s = "CASE".to_string();
1296                if let Some(e) = expr {
1297                    s.push_str(&format!(" {}", e.to_string_repr()));
1298                }
1299                for (w, t) in when_then {
1300                    s.push_str(&format!(
1301                        " WHEN {} THEN {}",
1302                        w.to_string_repr(),
1303                        t.to_string_repr()
1304                    ));
1305                }
1306                if let Some(e) = else_expr {
1307                    s.push_str(&format!(" ELSE {}", e.to_string_repr()));
1308                }
1309                s.push_str(" END");
1310                s
1311            }
1312            Expr::Exists { .. } => "EXISTS {...}".to_string(),
1313            Expr::CountSubquery(_) => "COUNT {...}".to_string(),
1314            Expr::CollectSubquery(_) => "COLLECT {...}".to_string(),
1315            Expr::IsNull(e) => format!("{} IS NULL", e.to_string_repr()),
1316            Expr::IsNotNull(e) => format!("{} IS NOT NULL", e.to_string_repr()),
1317            Expr::IsUnique(e) => format!("{} IS UNIQUE", e.to_string_repr()),
1318            Expr::In { expr, list } => {
1319                format!("{} IN {}", expr.to_string_repr(), list.to_string_repr())
1320            }
1321            Expr::ArrayIndex { array, index } => {
1322                format!("{}[{}]", array.to_string_repr(), index.to_string_repr())
1323            }
1324            Expr::ArraySlice { array, start, end } => {
1325                let start_str = start
1326                    .as_ref()
1327                    .map(|e| e.to_string_repr())
1328                    .unwrap_or_default();
1329                let end_str = end.as_ref().map(|e| e.to_string_repr()).unwrap_or_default();
1330                format!("{}[{}..{}]", array.to_string_repr(), start_str, end_str)
1331            }
1332            Expr::Quantifier {
1333                quantifier,
1334                variable,
1335                list,
1336                predicate,
1337            } => {
1338                format!(
1339                    "{quantifier}({variable} IN {} WHERE {})",
1340                    list.to_string_repr(),
1341                    predicate.to_string_repr()
1342                )
1343            }
1344            Expr::Reduce {
1345                accumulator,
1346                init,
1347                variable,
1348                list,
1349                expr,
1350            } => {
1351                format!(
1352                    "REDUCE({accumulator} = {}, {variable} IN {} | {})",
1353                    init.to_string_repr(),
1354                    list.to_string_repr(),
1355                    expr.to_string_repr()
1356                )
1357            }
1358
1359            Expr::ListComprehension {
1360                variable,
1361                list,
1362                where_clause,
1363                map_expr,
1364            } => {
1365                let where_str = where_clause
1366                    .as_ref()
1367                    .map_or(String::new(), |e| format!(" WHERE {}", e.to_string_repr()));
1368                format!(
1369                    "[{variable} IN {}{where_str}  | {}]",
1370                    list.to_string_repr(),
1371                    map_expr.to_string_repr()
1372                )
1373            }
1374
1375            Expr::PatternComprehension {
1376                path_variable,
1377                pattern,
1378                where_clause,
1379                map_expr,
1380            } => {
1381                let var_part = path_variable
1382                    .as_ref()
1383                    .map(|v| format!("{v} = "))
1384                    .unwrap_or_default();
1385                let where_str = where_clause
1386                    .as_ref()
1387                    .map_or(String::new(), |e| format!(" WHERE {}", e.to_string_repr()));
1388                format!(
1389                    "[{var_part}{pattern:?}{where_str} | {}]",
1390                    map_expr.to_string_repr()
1391                )
1392            }
1393
1394            Expr::ValidAt {
1395                entity,
1396                timestamp,
1397                start_prop,
1398                end_prop,
1399            } => match (start_prop, end_prop) {
1400                (Some(start), Some(end)) => format!(
1401                    "{} VALID_AT({}, '{start}', '{end}')",
1402                    entity.to_string_repr(),
1403                    timestamp.to_string_repr(),
1404                ),
1405                _ => format!(
1406                    "{} VALID_AT {}",
1407                    entity.to_string_repr(),
1408                    timestamp.to_string_repr(),
1409                ),
1410            },
1411
1412            Expr::MapProjection { base, items } => {
1413                let items_str = items
1414                    .iter()
1415                    .map(MapProjectionItem::to_string_repr)
1416                    .collect::<Vec<_>>()
1417                    .join(", ");
1418                format!("{}{{{items_str}}}", base.to_string_repr())
1419            }
1420
1421            Expr::LabelCheck { expr, labels } => {
1422                let labels_str: String = labels.iter().map(|l| format!(":{l}")).collect();
1423                format!("{}{labels_str}", expr.to_string_repr())
1424            }
1425        }
1426    }
1427
1428    /// Call `f` on each direct child expression (non-recursive).
1429    ///
1430    /// Does NOT descend into subqueries (Exists, CountSubquery, CollectSubquery)
1431    /// since those have separate variable scope.
1432    pub fn for_each_child(&self, f: &mut dyn FnMut(&Expr)) {
1433        match self {
1434            Expr::Literal(_) | Expr::Parameter(_) | Expr::Variable(_) | Expr::Wildcard => {}
1435            Expr::Property(base, _) => f(base),
1436            Expr::List(items) => {
1437                for item in items {
1438                    f(item);
1439                }
1440            }
1441            Expr::Map(entries) => {
1442                for (_, expr) in entries {
1443                    f(expr);
1444                }
1445            }
1446            Expr::FunctionCall { args, .. } => {
1447                for arg in args {
1448                    f(arg);
1449                }
1450            }
1451            Expr::BinaryOp { left, right, .. } => {
1452                f(left);
1453                f(right);
1454            }
1455            Expr::UnaryOp { expr, .. } => f(expr),
1456            Expr::Case {
1457                expr,
1458                when_then,
1459                else_expr,
1460            } => {
1461                if let Some(e) = expr {
1462                    f(e);
1463                }
1464                for (w, t) in when_then {
1465                    f(w);
1466                    f(t);
1467                }
1468                if let Some(e) = else_expr {
1469                    f(e);
1470                }
1471            }
1472            Expr::Exists { .. } | Expr::CountSubquery(_) | Expr::CollectSubquery(_) => {}
1473            Expr::IsNull(e) | Expr::IsNotNull(e) | Expr::IsUnique(e) => f(e),
1474            Expr::In { expr, list } => {
1475                f(expr);
1476                f(list);
1477            }
1478            Expr::ArrayIndex { array, index } => {
1479                f(array);
1480                f(index);
1481            }
1482            Expr::ArraySlice { array, start, end } => {
1483                f(array);
1484                if let Some(s) = start {
1485                    f(s);
1486                }
1487                if let Some(e) = end {
1488                    f(e);
1489                }
1490            }
1491            Expr::Quantifier {
1492                list, predicate, ..
1493            } => {
1494                f(list);
1495                f(predicate);
1496            }
1497            Expr::Reduce {
1498                init, list, expr, ..
1499            } => {
1500                f(init);
1501                f(list);
1502                f(expr);
1503            }
1504            Expr::ListComprehension {
1505                list,
1506                where_clause,
1507                map_expr,
1508                ..
1509            } => {
1510                f(list);
1511                if let Some(w) = where_clause {
1512                    f(w);
1513                }
1514                f(map_expr);
1515            }
1516            Expr::PatternComprehension {
1517                where_clause,
1518                map_expr,
1519                ..
1520            } => {
1521                if let Some(w) = where_clause {
1522                    f(w);
1523                }
1524                f(map_expr);
1525            }
1526            Expr::ValidAt {
1527                entity, timestamp, ..
1528            } => {
1529                f(entity);
1530                f(timestamp);
1531            }
1532            Expr::MapProjection { base, items } => {
1533                f(base);
1534                for item in items {
1535                    if let MapProjectionItem::LiteralEntry(_, expr) = item {
1536                        f(expr);
1537                    }
1538                }
1539            }
1540            Expr::LabelCheck { expr, .. } => f(expr),
1541        }
1542    }
1543
1544    /// Map each direct child expression through `f`, producing a new Expr.
1545    ///
1546    /// Same scoping rules as `for_each_child`: does not descend into subqueries.
1547    pub fn map_children(self, f: &mut dyn FnMut(Expr) -> Expr) -> Expr {
1548        match self {
1549            Expr::Literal(_) | Expr::Parameter(_) | Expr::Variable(_) | Expr::Wildcard => self,
1550            Expr::Property(base, prop) => Expr::Property(Box::new(f(*base)), prop),
1551            Expr::List(items) => Expr::List(items.into_iter().map(&mut *f).collect()),
1552            Expr::Map(entries) => Expr::Map(entries.into_iter().map(|(k, v)| (k, f(v))).collect()),
1553            Expr::FunctionCall {
1554                name,
1555                args,
1556                distinct,
1557                window_spec,
1558            } => Expr::FunctionCall {
1559                name,
1560                args: args.into_iter().map(&mut *f).collect(),
1561                distinct,
1562                window_spec,
1563            },
1564            Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
1565                left: Box::new(f(*left)),
1566                op,
1567                right: Box::new(f(*right)),
1568            },
1569            Expr::UnaryOp { op, expr } => Expr::UnaryOp {
1570                op,
1571                expr: Box::new(f(*expr)),
1572            },
1573            Expr::Case {
1574                expr,
1575                when_then,
1576                else_expr,
1577            } => Expr::Case {
1578                expr: expr.map(|e| Box::new(f(*e))),
1579                when_then: when_then.into_iter().map(|(w, t)| (f(w), f(t))).collect(),
1580                else_expr: else_expr.map(|e| Box::new(f(*e))),
1581            },
1582            Expr::Exists { .. } | Expr::CountSubquery(_) | Expr::CollectSubquery(_) => self,
1583            Expr::IsNull(e) => Expr::IsNull(Box::new(f(*e))),
1584            Expr::IsNotNull(e) => Expr::IsNotNull(Box::new(f(*e))),
1585            Expr::IsUnique(e) => Expr::IsUnique(Box::new(f(*e))),
1586            Expr::In { expr, list } => Expr::In {
1587                expr: Box::new(f(*expr)),
1588                list: Box::new(f(*list)),
1589            },
1590            Expr::ArrayIndex { array, index } => Expr::ArrayIndex {
1591                array: Box::new(f(*array)),
1592                index: Box::new(f(*index)),
1593            },
1594            Expr::ArraySlice { array, start, end } => Expr::ArraySlice {
1595                array: Box::new(f(*array)),
1596                start: start.map(|s| Box::new(f(*s))),
1597                end: end.map(|e| Box::new(f(*e))),
1598            },
1599            Expr::Quantifier {
1600                quantifier,
1601                variable,
1602                list,
1603                predicate,
1604            } => Expr::Quantifier {
1605                quantifier,
1606                variable,
1607                list: Box::new(f(*list)),
1608                predicate: Box::new(f(*predicate)),
1609            },
1610            Expr::Reduce {
1611                accumulator,
1612                init,
1613                variable,
1614                list,
1615                expr,
1616            } => Expr::Reduce {
1617                accumulator,
1618                init: Box::new(f(*init)),
1619                variable,
1620                list: Box::new(f(*list)),
1621                expr: Box::new(f(*expr)),
1622            },
1623            Expr::ListComprehension {
1624                variable,
1625                list,
1626                where_clause,
1627                map_expr,
1628            } => Expr::ListComprehension {
1629                variable,
1630                list: Box::new(f(*list)),
1631                where_clause: where_clause.map(|w| Box::new(f(*w))),
1632                map_expr: Box::new(f(*map_expr)),
1633            },
1634            Expr::PatternComprehension {
1635                path_variable,
1636                pattern,
1637                where_clause,
1638                map_expr,
1639            } => Expr::PatternComprehension {
1640                path_variable,
1641                pattern,
1642                where_clause: where_clause.map(|w| Box::new(f(*w))),
1643                map_expr: Box::new(f(*map_expr)),
1644            },
1645            Expr::ValidAt {
1646                entity,
1647                timestamp,
1648                start_prop,
1649                end_prop,
1650            } => Expr::ValidAt {
1651                entity: Box::new(f(*entity)),
1652                timestamp: Box::new(f(*timestamp)),
1653                start_prop,
1654                end_prop,
1655            },
1656            Expr::MapProjection { base, items } => Expr::MapProjection {
1657                base: Box::new(f(*base)),
1658                items: items
1659                    .into_iter()
1660                    .map(|item| match item {
1661                        MapProjectionItem::LiteralEntry(key, expr) => {
1662                            MapProjectionItem::LiteralEntry(key, Box::new(f(*expr)))
1663                        }
1664                        other => other,
1665                    })
1666                    .collect(),
1667            },
1668            Expr::LabelCheck { expr, labels } => Expr::LabelCheck {
1669                expr: Box::new(f(*expr)),
1670                labels,
1671            },
1672        }
1673    }
1674}