Skip to main content

lance_csr/
ast.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Abstract Syntax Tree for Cypher queries
5//!
6//! This module defines the AST nodes for representing parsed Cypher queries.
7//! The AST is designed to capture the essential graph patterns while being
8//! simple enough to translate to SQL efficiently.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// A pagination count, resolved to a nonnegative literal before logical planning.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(untagged)]
16pub enum RowCount {
17    Literal(u64),
18    Parameter(String),
19}
20
21impl RowCount {
22    /// Return a resolved count; unresolved parameters are a planning error.
23    pub fn value(&self) -> crate::error::Result<u64> {
24        match self {
25            Self::Literal(value) => Ok(*value),
26            Self::Parameter(name) => Err(crate::error::GraphError::PlanError {
27                message: format!("Unresolved pagination parameter: ${name}"),
28                location: snafu::Location::new(file!(), line!(), column!()),
29            }),
30        }
31    }
32}
33
34/// A complete Cypher query
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct CypherQuery {
37    /// READING clauses (MATCH, UNWIND, etc.)
38    pub reading_clauses: Vec<ReadingClause>,
39    /// WHERE clause (optional, before WITH if present)
40    pub where_clause: Option<WhereClause>,
41    /// WITH clause (optional) - intermediate projection/aggregation
42    pub with_clause: Option<WithClause>,
43    /// Post-WITH READING clauses
44    pub post_with_reading_clauses: Vec<ReadingClause>,
45    /// WHERE clause after WITH (optional) - filters the WITH results
46    pub post_with_where_clause: Option<WhereClause>,
47    /// Second and later `WITH ... [MATCH ...] [WHERE ...]` stages, in order. The fields
48    /// above hold the first stage, as upstream's single-WITH AST did.
49    #[serde(default)]
50    pub further_with_stages: Vec<WithStage>,
51    /// RETURN clause
52    pub return_clause: ReturnClause,
53    /// LIMIT clause (optional)
54    pub limit: Option<RowCount>,
55    /// ORDER BY clause (optional)
56    pub order_by: Option<OrderByClause>,
57    /// SKIP/OFFSET clause (optional)
58    pub skip: Option<RowCount>,
59    /// Following UNION branches, evaluated in order with independent variable scopes.
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub union_branches: Vec<UnionBranch>,
62}
63
64/// One right-hand query of UNION (distinct) or UNION ALL (bag concatenation).
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct UnionBranch {
67    pub all: bool,
68    pub query: CypherQuery,
69}
70
71impl CypherQuery {
72    pub(crate) fn all_reading_clauses(&self) -> impl Iterator<Item = &ReadingClause> {
73        self.reading_clauses
74            .iter()
75            .chain(&self.post_with_reading_clauses)
76            .chain(
77                self.further_with_stages
78                    .iter()
79                    .flat_map(|stage| &stage.reading_clauses),
80            )
81    }
82
83    /// Visit query scopes, including UNION branches and nested CALL bodies.
84    pub(crate) fn for_each_query(&self, visit: &mut dyn FnMut(&Self)) {
85        visit(self);
86        for clause in self.all_reading_clauses() {
87            if let ReadingClause::Call(query) = clause {
88                query.for_each_query(visit);
89            }
90        }
91        for branch in &self.union_branches {
92            branch.query.for_each_query(visit);
93        }
94    }
95
96    /// Extract all node labels referenced in the query
97    pub fn get_node_labels(&self) -> Vec<String> {
98        let mut labels = Vec::new();
99        // Iterate all match clauses directly
100        for clause in self.all_reading_clauses() {
101            if let ReadingClause::Call(inner) = clause {
102                for label in inner.get_node_labels() {
103                    if !labels.contains(&label) {
104                        labels.push(label);
105                    }
106                }
107            }
108            if let ReadingClause::Match(match_clause) = clause {
109                for pattern in &match_clause.patterns {
110                    match pattern {
111                        GraphPattern::Node(node) => {
112                            for label in &node.labels {
113                                if !labels.contains(label) {
114                                    labels.push(label.clone());
115                                }
116                            }
117                        }
118                        GraphPattern::Path(path) => {
119                            for label in &path.start_node.labels {
120                                if !labels.contains(label) {
121                                    labels.push(label.clone());
122                                }
123                            }
124                            for segment in &path.segments {
125                                for label in &segment.end_node.labels {
126                                    if !labels.contains(label) {
127                                        labels.push(label.clone());
128                                    }
129                                }
130                            }
131                        }
132                    }
133                }
134            }
135        }
136        for branch in &self.union_branches {
137            for label in branch.query.get_node_labels() {
138                if !labels.contains(&label) {
139                    labels.push(label);
140                }
141            }
142        }
143        labels
144    }
145
146    /// Extract all relationship types referenced in the query
147    pub fn get_relationship_types(&self) -> Vec<String> {
148        let mut types = Vec::new();
149        for clause in self.all_reading_clauses() {
150            if let ReadingClause::Call(inner) = clause {
151                for kind in inner.get_relationship_types() {
152                    if !types.contains(&kind) {
153                        types.push(kind);
154                    }
155                }
156            }
157            if let ReadingClause::Match(match_clause) = clause {
158                for pattern in &match_clause.patterns {
159                    self.collect_relationship_types_from_pattern(pattern, &mut types);
160                }
161            }
162        }
163        for branch in &self.union_branches {
164            for rel_type in branch.query.get_relationship_types() {
165                if !types.contains(&rel_type) {
166                    types.push(rel_type);
167                }
168            }
169        }
170        types
171    }
172
173    fn collect_relationship_types_from_pattern(
174        &self,
175        pattern: &GraphPattern,
176        types: &mut Vec<String>,
177    ) {
178        if let GraphPattern::Path(path) = pattern {
179            for segment in &path.segments {
180                for rel_type in &segment.relationship.types {
181                    if !types.contains(rel_type) {
182                        types.push(rel_type.clone());
183                    }
184                }
185            }
186        }
187    }
188}
189
190/// A clause that reads from the graph (MATCH, UNWIND)
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192pub enum ReadingClause {
193    Match(MatchClause),
194    Unwind(UnwindClause),
195    /// Read-only subquery, with an optional explicit importing WITH.
196    Call(Box<CypherQuery>),
197}
198
199impl CypherQuery {
200    /// Every outer variable needed by this CALL's independently scoped UNION branches.
201    pub(crate) fn call_union_imports(&self) -> crate::error::Result<Vec<String>> {
202        let mut names = self.call_imports()?;
203        for branch in &self.union_branches {
204            names.extend(branch.query.call_union_imports()?);
205        }
206        names.sort();
207        names.dedup();
208        Ok(names)
209    }
210
211    /// Names explicitly imported by a leading plain WITH in a CALL body.
212    pub(crate) fn call_imports(&self) -> crate::error::Result<Vec<String>> {
213        let Some(clause) = self
214            .with_clause
215            .as_ref()
216            .filter(|_| self.reading_clauses.is_empty())
217        else {
218            return Ok(vec![]);
219        };
220        if clause.distinct
221            || clause.order_by.is_some()
222            || clause.limit.is_some()
223            || clause.where_clause.is_some()
224        {
225            return Err(crate::error::GraphError::PlanError {
226                message: "CALL importing WITH must contain only unaliased variable names; use a second WITH for expressions or filters".into(),
227                location: snafu::Location::new(file!(), line!(), column!()),
228            });
229        }
230        clause.items.iter().map(|item| match (&item.expression, &item.alias) {
231            (ValueExpression::Variable(name), None) => Ok(name.to_lowercase()),
232            _ => Err(crate::error::GraphError::PlanError {
233                message: "CALL importing WITH must contain only unaliased variable names; use a second WITH for expressions".into(),
234                location: snafu::Location::new(file!(), line!(), column!()),
235            }),
236        }).collect()
237    }
238}
239
240/// A MATCH clause containing graph patterns
241#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
242pub struct MatchClause {
243    /// Graph patterns to match
244    pub patterns: Vec<GraphPattern>,
245    /// `OPTIONAL MATCH`: rows with no match are kept, with nulls for the new variables
246    #[serde(default)]
247    pub optional: bool,
248    /// A WHERE belonging to this clause: always the one right after an OPTIONAL MATCH
249    /// (it filters inside the optional part), and for a plain MATCH one followed by
250    /// another reading clause. A plain MATCH's last WHERE stays in the query's slot.
251    #[serde(default)]
252    pub where_clause: Option<WhereClause>,
253}
254
255/// An UNWIND clause
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
257pub struct UnwindClause {
258    /// Expression to unwind
259    pub expression: ValueExpression,
260    /// Alias for the unwound values
261    pub alias: String,
262}
263
264/// A graph pattern (nodes and relationships)
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266pub enum GraphPattern {
267    /// A single node pattern
268    Node(NodePattern),
269    /// A path pattern (node-relationship-node sequence)
270    Path(PathPattern),
271}
272
273/// A node pattern in a graph query
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275pub struct NodePattern {
276    /// Variable name for the node (e.g., 'n' in (n:Person))
277    pub variable: Option<String>,
278    /// Node labels (e.g., ['Person', 'Employee'])
279    pub labels: Vec<String>,
280    /// Property constraints (e.g., {name: 'John', age: 30})
281    pub properties: HashMap<String, PropertyValue>,
282}
283
284/// A path pattern connecting nodes through relationships
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
286pub struct PathPattern {
287    /// Named ordinary path (`p = (a)-[:R]->(b)`).
288    #[serde(default)]
289    pub variable: Option<String>,
290    /// Starting node
291    pub start_node: NodePattern,
292    /// Relationships and intermediate nodes
293    pub segments: Vec<PathSegment>,
294    /// `p = shortestPath(...)` / `allShortestPaths(...)`: one variable-length segment,
295    /// matched only by its shortest paths.
296    #[serde(default)]
297    pub shortest: Option<ShortestPathSpec>,
298}
299
300/// How a shortest-path pattern matches, and the path variable it binds.
301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
302pub struct ShortestPathSpec {
303    /// `allShortestPaths`: every path of the minimum length, not just one.
304    pub all: bool,
305    /// The path variable (`p` in `p = shortestPath(...)`), used with `length(p)` and `nodes(p)`.
306    pub variable: Option<String>,
307}
308
309/// A segment of a path (relationship + end node)
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct PathSegment {
312    /// The relationship in this segment
313    pub relationship: RelationshipPattern,
314    /// The end node of this segment
315    pub end_node: NodePattern,
316}
317
318/// A relationship pattern
319#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
320pub struct RelationshipPattern {
321    /// Variable name for the relationship (e.g., 'r' in [r:KNOWS])
322    pub variable: Option<String>,
323    /// Relationship types (e.g., ['KNOWS', 'FRIEND_OF'])
324    pub types: Vec<String>,
325    /// Direction of the relationship
326    pub direction: RelationshipDirection,
327    /// Property constraints on the relationship
328    pub properties: HashMap<String, PropertyValue>,
329    /// Length constraints (for variable-length paths)
330    pub length: Option<LengthRange>,
331}
332
333/// Direction of a relationship
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
335pub enum RelationshipDirection {
336    /// Outgoing relationship (->)
337    Outgoing,
338    /// Incoming relationship (<-)
339    Incoming,
340    /// Undirected relationship (-)
341    Undirected,
342}
343
344/// Length range for variable-length paths
345#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
346pub struct LengthRange {
347    /// Minimum length (inclusive)
348    pub min: Option<u32>,
349    /// Maximum length (inclusive)
350    pub max: Option<u32>,
351}
352
353/// Property value in patterns and expressions
354#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
355pub enum PropertyValue {
356    /// String literal
357    String(String),
358    /// Integer literal
359    Integer(i64),
360    /// Float literal
361    Float(f64),
362    /// Boolean literal
363    Boolean(bool),
364    /// Null value
365    Null,
366    /// Parameter reference (e.g., $param)
367    Parameter(String),
368    /// Property reference (e.g., node.property)
369    Property(PropertyRef),
370    /// A variable from an earlier clause, as an inline property value (`{id: tagId}`).
371    /// Like `Property` values, it is moved into the clause's WHERE before planning.
372    Variable(String),
373}
374
375/// Reference to a property of a node or relationship
376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
377pub struct PropertyRef {
378    /// Variable name (e.g., 'n' in n.name)
379    pub variable: String,
380    /// Property name
381    pub property: String,
382}
383
384/// WHERE clause for filtering
385#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
386pub struct WhereClause {
387    /// Boolean expression for filtering
388    pub expression: BooleanExpression,
389}
390
391/// Boolean expressions in WHERE clauses
392#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
393pub enum BooleanExpression {
394    /// Comparison operation (=, <>, <, >, <=, >=)
395    Comparison {
396        left: ValueExpression,
397        operator: ComparisonOperator,
398        right: ValueExpression,
399    },
400    /// Logical AND
401    And(Box<BooleanExpression>, Box<BooleanExpression>),
402    /// Logical OR
403    Or(Box<BooleanExpression>, Box<BooleanExpression>),
404    /// Logical NOT
405    Not(Box<BooleanExpression>),
406    /// Property existence check
407    Exists(PropertyRef),
408    /// `EXISTS { [MATCH] pattern [WHERE ...] }`, or a bare pattern used as a predicate
409    /// (`NOT (a)-[:R]-(b)`): true when the pattern matches from this row's variables.
410    PatternExists {
411        patterns: Vec<GraphPattern>,
412        where_clause: Option<Box<BooleanExpression>>,
413    },
414    /// IN clause
415    In {
416        expression: ValueExpression,
417        list: Vec<ValueExpression>,
418    },
419    /// `x IN list` where the list is any expression, such as a collected alias
420    /// (`friend IN friends`) rather than a literal `[...]`.
421    Member {
422        expression: ValueExpression,
423        list: ValueExpression,
424    },
425    /// LIKE pattern matching (case-sensitive)
426    Like {
427        expression: ValueExpression,
428        pattern: String,
429    },
430    /// ILIKE pattern matching (case-insensitive)
431    ILike {
432        expression: ValueExpression,
433        pattern: String,
434    },
435    /// CONTAINS substring matching
436    Contains {
437        expression: ValueExpression,
438        substring: String,
439    },
440    /// STARTS WITH prefix matching
441    StartsWith {
442        expression: ValueExpression,
443        prefix: String,
444    },
445    /// ENDS WITH suffix matching
446    EndsWith {
447        expression: ValueExpression,
448        suffix: String,
449    },
450    /// IS NULL pattern matching
451    IsNull(ValueExpression),
452    /// IS NOT NULL pattern matching
453    IsNotNull(ValueExpression),
454}
455
456/// Distance metric for vector similarity
457#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
458pub enum DistanceMetric {
459    /// Euclidean distance (L2)
460    L2,
461    /// Cosine similarity (1 - cosine distance)
462    #[default]
463    Cosine,
464    /// Dot product
465    Dot,
466}
467
468/// Comparison operators
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
470pub enum ComparisonOperator {
471    Equal,
472    NotEqual,
473    LessThan,
474    LessThanOrEqual,
475    GreaterThan,
476    GreaterThanOrEqual,
477}
478
479/// Value expressions (for comparisons and return values)
480#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
481pub enum ValueExpression {
482    /// Variable reference
483    Variable(String),
484    /// Property reference
485    Property(PropertyRef),
486    /// Literal value
487    Literal(PropertyValue),
488    /// `CASE WHEN cond THEN value ... [ELSE value] END`. The simple form
489    /// `CASE x WHEN v THEN ...` is parsed into this one, with `x = v` conditions.
490    Case {
491        whens: Vec<(BooleanExpression, ValueExpression)>,
492        otherwise: Option<Box<ValueExpression>>,
493    },
494    /// Scalar function call (toLower, upper, etc.)
495    /// These are row-level functions that operate on individual values
496    ScalarFunction {
497        name: String,
498        args: Vec<ValueExpression>,
499    },
500    /// Aggregate function call (COUNT, SUM, AVG, MIN, MAX, COLLECT)
501    /// These functions operate across multiple rows and support DISTINCT
502    AggregateFunction {
503        name: String,
504        args: Vec<ValueExpression>,
505        /// Whether DISTINCT keyword was specified (e.g., COUNT(DISTINCT x))
506        distinct: bool,
507    },
508    /// Arithmetic operation
509    Arithmetic {
510        left: Box<ValueExpression>,
511        operator: ArithmeticOperator,
512        right: Box<ValueExpression>,
513    },
514    /// Vector distance function: vector_distance(left, right, metric)
515    /// Returns the distance as a float (lower = more similar for L2/Cosine)
516    VectorDistance {
517        left: Box<ValueExpression>,
518        right: Box<ValueExpression>,
519        metric: DistanceMetric,
520    },
521    /// Vector similarity function: vector_similarity(left, right, metric)
522    /// Returns the similarity score as a float (higher = more similar)
523    VectorSimilarity {
524        left: Box<ValueExpression>,
525        right: Box<ValueExpression>,
526        metric: DistanceMetric,
527    },
528    /// Parameter reference for query parameters (e.g., $query_vector)
529    Parameter(String),
530    /// A predicate used as a value: `path IS NULL`, `NOT (a)-[:R]-(b) AS isNew`.
531    Boolean(Box<BooleanExpression>),
532    /// A map literal `{key: value, ...}`; a node as a value inside it keeps its properties.
533    Map(Vec<(String, ValueExpression)>),
534    /// `value.field` on something other than a node or relationship variable: a map key,
535    /// or a component of a date or timestamp (`birthday.month`).
536    Field {
537        base: Box<ValueExpression>,
538        field: String,
539    },
540    /// `[(a)-[:R]->(b) WHERE predicate | value]`: the values for each match of the pattern
541    /// from this row's bound variables.
542    PatternComprehension {
543        pattern: Box<PathPattern>,
544        filter: Option<Box<BooleanExpression>>,
545        map: Box<ValueExpression>,
546    },
547    /// `[x IN list WHERE predicate | value]`, both parts optional.
548    ListComprehension {
549        variable: String,
550        list: Box<ValueExpression>,
551        filter: Option<Box<BooleanExpression>>,
552        map: Option<Box<ValueExpression>>,
553    },
554    /// A lexically scoped variable in a reduce or list predicate body.
555    LocalVariable(String),
556    /// `reduce(accumulator = initial, variable IN list | expression)`.
557    Reduce {
558        accumulator: String,
559        initial: Box<ValueExpression>,
560        variable: String,
561        list: Box<ValueExpression>,
562        expression: Box<ValueExpression>,
563    },
564    /// `all/any/none/single(variable IN list WHERE predicate)`.
565    ListPredicate {
566        kind: String,
567        variable: String,
568        list: Box<ValueExpression>,
569        predicate: Box<ValueExpression>,
570    },
571    /// Vector literal: [0.1, 0.2, 0.3]
572    /// Represents an inline vector for similarity search
573    VectorLiteral(Vec<f32>),
574}
575
576/// Function type classification
577#[derive(Debug, Clone, PartialEq)]
578pub enum FunctionType {
579    /// Aggregate function (operates across multiple rows)
580    Aggregate,
581    /// Scalar function (operates on individual values)
582    Scalar,
583    /// Unknown function type
584    Unknown,
585}
586
587/// Classify a function by name
588pub fn classify_function(name: &str) -> FunctionType {
589    match name.to_lowercase().as_str() {
590        "count" | "sum" | "avg" | "min" | "max" | "collect" => FunctionType::Aggregate,
591        "tolower" | "lower" | "toupper" | "upper" | "coalesce" | "date_part" | "to_epoch_ms"
592        | "length" | "nodes" | "size" | "range" | "__index" | "__list" | "tostring" | "head"
593        | "tointeger" | "tofloat" | "date" | "datetime" | "duration.inseconds" => {
594            FunctionType::Scalar
595        }
596        // Vector functions are handled separately as special variants
597        _ => FunctionType::Unknown,
598    }
599}
600
601/// Arithmetic operators
602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
603pub enum ArithmeticOperator {
604    Add,
605    Subtract,
606    Multiply,
607    Divide,
608    Modulo,
609}
610
611/// WITH clause for intermediate projections/aggregations
612///
613/// WITH acts as a query stage boundary, projecting results that become
614/// the input for subsequent clauses.
615#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
616pub struct WithClause {
617    /// Whether DISTINCT was specified
618    #[serde(default)]
619    pub distinct: bool,
620    /// Items to project (similar to RETURN)
621    pub items: Vec<ReturnItem>,
622    /// Optional ORDER BY within WITH
623    pub order_by: Option<OrderByClause>,
624    /// Optional LIMIT within WITH
625    pub limit: Option<RowCount>,
626    /// `WITH ... WHERE`: filters the WITH output, before any following MATCH
627    #[serde(default)]
628    pub where_clause: Option<WhereClause>,
629}
630
631/// One `WITH` with the reading clauses and `WHERE` that follow it, before the next `WITH`
632/// or the `RETURN`.
633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
634pub struct WithStage {
635    pub with_clause: WithClause,
636    pub reading_clauses: Vec<ReadingClause>,
637    pub where_clause: Option<WhereClause>,
638}
639
640/// RETURN clause specifying what to return
641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
642pub struct ReturnClause {
643    /// Whether DISTINCT was specified
644    pub distinct: bool,
645    /// Items to return
646    pub items: Vec<ReturnItem>,
647}
648
649/// An item in the RETURN clause
650#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
651pub struct ReturnItem {
652    /// The expression to return
653    pub expression: ValueExpression,
654    /// Alias for the returned value
655    pub alias: Option<String>,
656}
657
658/// ORDER BY clause
659#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
660pub struct OrderByClause {
661    /// Items to order by
662    pub items: Vec<OrderByItem>,
663}
664
665/// An item in the ORDER BY clause
666#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
667pub struct OrderByItem {
668    /// Expression to order by
669    pub expression: ValueExpression,
670    /// Sort direction
671    pub direction: SortDirection,
672}
673
674/// Sort direction for ORDER BY
675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
676pub enum SortDirection {
677    Ascending,
678    Descending,
679}
680
681impl NodePattern {
682    /// Create a new node pattern
683    pub fn new(variable: Option<String>) -> Self {
684        Self {
685            variable,
686            labels: Vec::new(),
687            properties: HashMap::new(),
688        }
689    }
690
691    /// Add a label to the node pattern
692    pub fn with_label<S: Into<String>>(mut self, label: S) -> Self {
693        self.labels.push(label.into());
694        self
695    }
696
697    /// Add a property constraint to the node pattern
698    pub fn with_property<S: Into<String>>(mut self, key: S, value: PropertyValue) -> Self {
699        self.properties.insert(key.into(), value);
700        self
701    }
702}
703
704impl RelationshipPattern {
705    /// Create a new relationship pattern
706    pub fn new(direction: RelationshipDirection) -> Self {
707        Self {
708            variable: None,
709            types: Vec::new(),
710            direction,
711            properties: HashMap::new(),
712            length: None,
713        }
714    }
715
716    /// Set the variable name for the relationship
717    pub fn with_variable<S: Into<String>>(mut self, variable: S) -> Self {
718        self.variable = Some(variable.into());
719        self
720    }
721
722    /// Add a type to the relationship pattern
723    pub fn with_type<S: Into<String>>(mut self, rel_type: S) -> Self {
724        self.types.push(rel_type.into());
725        self
726    }
727
728    /// Add a property constraint to the relationship pattern
729    pub fn with_property<S: Into<String>>(mut self, key: S, value: PropertyValue) -> Self {
730        self.properties.insert(key.into(), value);
731        self
732    }
733}
734
735impl PropertyRef {
736    /// Create a new property reference
737    pub fn new<S: Into<String>>(variable: S, property: S) -> Self {
738        Self {
739            variable: variable.into(),
740            property: property.into(),
741        }
742    }
743}
744
745/// Apply `f` to every value inside `expr`, children before parents, including the values
746/// inside predicates. Patterns of EXISTS subqueries are left alone.
747pub fn transform_value(expr: &mut ValueExpression, f: &mut dyn FnMut(&mut ValueExpression)) {
748    match expr {
749        ValueExpression::ScalarFunction { args, .. }
750        | ValueExpression::AggregateFunction { args, .. } => {
751            args.iter_mut().for_each(|a| transform_value(a, f))
752        }
753        ValueExpression::Arithmetic { left, right, .. }
754        | ValueExpression::VectorDistance { left, right, .. }
755        | ValueExpression::VectorSimilarity { left, right, .. } => {
756            transform_value(left, f);
757            transform_value(right, f);
758        }
759        ValueExpression::Case { whens, otherwise } => {
760            for (condition, then) in whens {
761                transform_boolean_values(condition, f);
762                transform_value(then, f);
763            }
764            if let Some(e) = otherwise {
765                transform_value(e, f);
766            }
767        }
768        ValueExpression::Boolean(b) => transform_boolean_values(b, f),
769        ValueExpression::Map(entries) => {
770            entries.iter_mut().for_each(|(_, v)| transform_value(v, f))
771        }
772        ValueExpression::Field { base, .. } => transform_value(base, f),
773        ValueExpression::ListComprehension {
774            list, filter, map, ..
775        } => {
776            transform_value(list, f);
777            if let Some(b) = filter {
778                transform_boolean_values(b, f);
779            }
780            if let Some(m) = map {
781                transform_value(m, f);
782            }
783        }
784        ValueExpression::PatternComprehension { filter, map, .. } => {
785            if let Some(b) = filter {
786                transform_boolean_values(b, f);
787            }
788            transform_value(map, f);
789        }
790        ValueExpression::Reduce {
791            initial,
792            list,
793            expression,
794            ..
795        } => {
796            transform_value(initial, f);
797            transform_value(list, f);
798            transform_value(expression, f);
799        }
800        ValueExpression::ListPredicate {
801            list, predicate, ..
802        } => {
803            transform_value(list, f);
804            transform_value(predicate, f);
805        }
806        ValueExpression::Variable(_)
807        | ValueExpression::LocalVariable(_)
808        | ValueExpression::Property(_)
809        | ValueExpression::Literal(_)
810        | ValueExpression::Parameter(_)
811        | ValueExpression::VectorLiteral(_) => {}
812    }
813    f(expr);
814}
815
816// Mark only the body's local references; the input and initializer retain outer scope.
817pub(crate) fn mark_locals(expr: &mut ValueExpression, names: &[&str]) {
818    transform_value(expr, &mut |value| match value {
819        ValueExpression::Variable(name) if names.iter().any(|n| n.eq_ignore_ascii_case(name)) => {
820            *value = ValueExpression::LocalVariable(name.to_lowercase());
821        }
822        ValueExpression::Property(p)
823            if names.iter().any(|n| n.eq_ignore_ascii_case(&p.variable)) =>
824        {
825            *value = ValueExpression::Field {
826                base: Box::new(ValueExpression::LocalVariable(p.variable.to_lowercase())),
827                field: p.property.clone(),
828            };
829        }
830        _ => {}
831    });
832}
833
834/// Expressions that need relational planning rather than scalar lambda execution.
835pub(crate) fn needs_relational_evaluation(expr: &ValueExpression) -> bool {
836    fn boolean_has_pattern(expr: &BooleanExpression) -> bool {
837        match expr {
838            BooleanExpression::PatternExists { .. } => true,
839            BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
840                boolean_has_pattern(a) || boolean_has_pattern(b)
841            }
842            BooleanExpression::Not(b) => boolean_has_pattern(b),
843            _ => false,
844        }
845    }
846    let mut found = false;
847    transform_value(&mut expr.clone(), &mut |value| {
848        found |= match value {
849            ValueExpression::PatternComprehension { .. }
850            | ValueExpression::ListComprehension { .. } => true,
851            ValueExpression::Boolean(b) => boolean_has_pattern(b),
852            ValueExpression::Case { whens, .. } => {
853                whens.iter().any(|(b, _)| boolean_has_pattern(b))
854            }
855            _ => false,
856        };
857    });
858    found
859}
860
861/// [`transform_value`] over every value in a predicate.
862pub fn transform_boolean_values(
863    expr: &mut BooleanExpression,
864    f: &mut dyn FnMut(&mut ValueExpression),
865) {
866    match expr {
867        BooleanExpression::Comparison { left, right, .. } => {
868            transform_value(left, f);
869            transform_value(right, f);
870        }
871        BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
872            transform_boolean_values(a, f);
873            transform_boolean_values(b, f);
874        }
875        BooleanExpression::Not(inner) => transform_boolean_values(inner, f),
876        BooleanExpression::In { expression, list } => {
877            transform_value(expression, f);
878            list.iter_mut().for_each(|v| transform_value(v, f));
879        }
880        BooleanExpression::Member { expression, list } => {
881            transform_value(expression, f);
882            transform_value(list, f);
883        }
884        BooleanExpression::Like { expression, .. }
885        | BooleanExpression::ILike { expression, .. }
886        | BooleanExpression::Contains { expression, .. }
887        | BooleanExpression::StartsWith { expression, .. }
888        | BooleanExpression::EndsWith { expression, .. }
889        | BooleanExpression::IsNull(expression)
890        | BooleanExpression::IsNotNull(expression) => transform_value(expression, f),
891        BooleanExpression::Exists(_) | BooleanExpression::PatternExists { .. } => {}
892    }
893}
894
895#[cfg(test)]
896mod tests {
897    use super::*;
898
899    #[test]
900    fn test_node_pattern_creation() {
901        let node = NodePattern::new(Some("n".to_string()))
902            .with_label("Person")
903            .with_property("name", PropertyValue::String("John".to_string()));
904
905        assert_eq!(node.variable, Some("n".to_string()));
906        assert_eq!(node.labels, vec!["Person"]);
907        assert_eq!(node.properties.len(), 1);
908    }
909
910    #[test]
911    fn test_relationship_pattern_creation() {
912        let rel = RelationshipPattern::new(RelationshipDirection::Outgoing)
913            .with_variable("r")
914            .with_type("KNOWS");
915
916        assert_eq!(rel.variable, Some("r".to_string()));
917        assert_eq!(rel.types, vec!["KNOWS"]);
918        assert_eq!(rel.direction, RelationshipDirection::Outgoing);
919    }
920
921    #[test]
922    fn test_property_ref() {
923        let prop_ref = PropertyRef::new("n", "name");
924        assert_eq!(prop_ref.variable, "n");
925        assert_eq!(prop_ref.property, "name");
926    }
927}