Skip to main content

grafeo_engine/query/
plan.rs

1//! Logical query plan representation.
2//!
3//! The logical plan is the intermediate representation between parsed queries
4//! and physical execution. Both GQL and Cypher queries are translated to this
5//! common representation.
6
7use std::collections::HashMap;
8use std::fmt;
9
10use grafeo_common::types::Value;
11
12/// A count expression for SKIP/LIMIT: either a resolved literal or an unresolved parameter.
13#[derive(Debug, Clone, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum CountExpr {
16    /// A resolved integer count.
17    Literal(usize),
18    /// An unresolved parameter reference (e.g., `$limit`).
19    Parameter(String),
20}
21
22impl CountExpr {
23    /// Returns the resolved count, or panics if still a parameter reference.
24    ///
25    /// Call this only after parameter substitution has run.
26    ///
27    /// # Panics
28    ///
29    /// Panics if the expression is an unresolved `Parameter` reference.
30    pub fn value(&self) -> usize {
31        match self {
32            Self::Literal(n) => *n,
33            Self::Parameter(name) => panic!("Unresolved parameter: ${name}"),
34        }
35    }
36
37    /// Returns the resolved count, or an error if still a parameter reference.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error string if the expression is an unresolved `Parameter`.
42    pub fn try_value(&self) -> Result<usize, String> {
43        match self {
44            Self::Literal(n) => Ok(*n),
45            Self::Parameter(name) => Err(format!("Unresolved SKIP/LIMIT parameter: ${name}")),
46        }
47    }
48
49    /// Returns the count as f64 for cardinality estimation (defaults to 10 for unresolved params).
50    pub fn estimate(&self) -> f64 {
51        match self {
52            Self::Literal(n) => *n as f64,
53            Self::Parameter(_) => 10.0, // reasonable default for unresolved params
54        }
55    }
56}
57
58impl fmt::Display for CountExpr {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Literal(n) => write!(f, "{n}"),
62            Self::Parameter(name) => write!(f, "${name}"),
63        }
64    }
65}
66
67impl From<usize> for CountExpr {
68    fn from(n: usize) -> Self {
69        Self::Literal(n)
70    }
71}
72
73impl PartialEq<usize> for CountExpr {
74    fn eq(&self, other: &usize) -> bool {
75        matches!(self, Self::Literal(n) if n == other)
76    }
77}
78
79/// A logical query plan.
80#[derive(Debug, Clone)]
81pub struct LogicalPlan {
82    /// The root operator of the plan.
83    pub root: LogicalOperator,
84    /// When true, return the plan tree as text instead of executing.
85    pub explain: bool,
86    /// When true, execute the query and return per-operator runtime metrics.
87    pub profile: bool,
88    /// Default parameter values from variable declarations (e.g., GraphQL
89    /// `query($limit: Int = 2)`). The processor merges these with caller-supplied
90    /// params, giving caller values higher precedence.
91    pub default_params: HashMap<String, Value>,
92}
93
94impl LogicalPlan {
95    /// Creates a new logical plan with the given root operator.
96    pub fn new(root: LogicalOperator) -> Self {
97        Self {
98            root,
99            explain: false,
100            profile: false,
101            default_params: HashMap::new(),
102        }
103    }
104
105    /// Creates an EXPLAIN plan that returns the plan tree without executing.
106    pub fn explain(root: LogicalOperator) -> Self {
107        Self {
108            root,
109            explain: true,
110            profile: false,
111            default_params: HashMap::new(),
112        }
113    }
114
115    /// Creates a PROFILE plan that executes and returns per-operator metrics.
116    pub fn profile(root: LogicalOperator) -> Self {
117        Self {
118            root,
119            explain: false,
120            profile: true,
121            default_params: HashMap::new(),
122        }
123    }
124}
125
126/// A logical operator in the query plan.
127#[derive(Debug, Clone)]
128#[non_exhaustive]
129pub enum LogicalOperator {
130    /// Scan all nodes, optionally filtered by label.
131    NodeScan(NodeScanOp),
132
133    /// Scan all edges, optionally filtered by type.
134    EdgeScan(EdgeScanOp),
135
136    /// Expand from nodes to neighbors via edges.
137    Expand(ExpandOp),
138
139    /// Filter rows based on a predicate.
140    Filter(FilterOp),
141
142    /// Project specific columns.
143    Project(ProjectOp),
144
145    /// Join two inputs.
146    Join(JoinOp),
147
148    /// Aggregate with grouping.
149    Aggregate(AggregateOp),
150
151    /// Limit the number of results.
152    Limit(LimitOp),
153
154    /// Skip a number of results.
155    Skip(SkipOp),
156
157    /// Sort results.
158    Sort(SortOp),
159
160    /// Remove duplicate results.
161    Distinct(DistinctOp),
162
163    /// Create a new node.
164    CreateNode(CreateNodeOp),
165
166    /// Create a new edge.
167    CreateEdge(CreateEdgeOp),
168
169    /// Delete a node.
170    DeleteNode(DeleteNodeOp),
171
172    /// Delete an edge.
173    DeleteEdge(DeleteEdgeOp),
174
175    /// Set properties on a node or edge.
176    SetProperty(SetPropertyOp),
177
178    /// Add labels to a node.
179    AddLabel(AddLabelOp),
180
181    /// Remove labels from a node.
182    RemoveLabel(RemoveLabelOp),
183
184    /// Return results (terminal operator).
185    Return(ReturnOp),
186
187    /// Empty result set.
188    Empty,
189
190    // ==================== RDF/SPARQL Operators ====================
191    /// Scan RDF triples matching a pattern.
192    TripleScan(TripleScanOp),
193
194    /// Union of multiple result sets.
195    Union(UnionOp),
196
197    /// Left outer join for OPTIONAL patterns.
198    LeftJoin(LeftJoinOp),
199
200    /// Anti-join for MINUS patterns.
201    AntiJoin(AntiJoinOp),
202
203    /// Bind a variable to an expression.
204    Bind(BindOp),
205
206    /// Unwind a list into individual rows.
207    Unwind(UnwindOp),
208
209    /// Collect grouped key-value rows into a single Map value.
210    /// Used for Gremlin `groupCount()` semantics.
211    MapCollect(MapCollectOp),
212
213    /// Merge a node pattern (match or create).
214    Merge(MergeOp),
215
216    /// Merge a relationship pattern (match or create).
217    MergeRelationship(MergeRelationshipOp),
218
219    /// Find shortest path between nodes.
220    ShortestPath(ShortestPathOp),
221
222    // ==================== SPARQL Update Operators ====================
223    /// Insert RDF triples.
224    InsertTriple(InsertTripleOp),
225
226    /// Delete RDF triples.
227    DeleteTriple(DeleteTripleOp),
228
229    /// SPARQL MODIFY operation (DELETE/INSERT WHERE).
230    /// Evaluates WHERE once, applies DELETE templates, then INSERT templates.
231    Modify(ModifyOp),
232
233    /// Clear a graph (remove all triples).
234    ClearGraph(ClearGraphOp),
235
236    /// Create a new named graph.
237    CreateGraph(CreateGraphOp),
238
239    /// Drop (remove) a named graph.
240    DropGraph(DropGraphOp),
241
242    /// Load data from a URL into a graph.
243    LoadGraph(LoadGraphOp),
244
245    /// Copy triples from one graph to another.
246    CopyGraph(CopyGraphOp),
247
248    /// Move triples from one graph to another.
249    MoveGraph(MoveGraphOp),
250
251    /// Add (merge) triples from one graph to another.
252    AddGraph(AddGraphOp),
253
254    /// Per-row aggregation over a list-valued column (horizontal aggregation, GE09).
255    HorizontalAggregate(HorizontalAggregateOp),
256
257    // ==================== Vector Search Operators ====================
258    /// Scan using vector similarity search.
259    VectorScan(VectorScanOp),
260
261    /// Join graph patterns with vector similarity search.
262    ///
263    /// Computes vector distances between entities from the left input and
264    /// a query vector, then joins with similarity scores. Useful for:
265    /// - Filtering graph traversal results by vector similarity
266    /// - Computing aggregated embeddings and finding similar entities
267    /// - Combining multiple vector sources with graph structure
268    VectorJoin(VectorJoinOp),
269
270    // ==================== Set Operations ====================
271    /// Set difference: rows in left that are not in right.
272    Except(ExceptOp),
273
274    /// Set intersection: rows common to all inputs.
275    Intersect(IntersectOp),
276
277    /// Fallback: use left result if non-empty, otherwise right.
278    Otherwise(OtherwiseOp),
279
280    // ==================== Correlated Subquery ====================
281    /// Apply (lateral join): evaluate a subplan per input row.
282    Apply(ApplyOp),
283
284    /// Parameter scan: leaf of a correlated inner plan that receives values
285    /// from the outer Apply operator. The column names match `ApplyOp.shared_variables`.
286    ParameterScan(ParameterScanOp),
287
288    // ==================== DDL Operators ====================
289    /// Define a property graph schema (SQL/PGQ DDL).
290    CreatePropertyGraph(CreatePropertyGraphOp),
291
292    // ==================== Multi-Way Join ====================
293    /// Multi-way join using worst-case optimal join (leapfrog).
294    /// Used for cyclic patterns (triangles, cliques) with 3+ relations.
295    MultiWayJoin(MultiWayJoinOp),
296
297    // ==================== Procedure Call Operators ====================
298    /// Invoke a stored procedure (CALL ... YIELD).
299    CallProcedure(CallProcedureOp),
300
301    // ==================== Data Import Operators ====================
302    /// Load data from a file (CSV, JSONL, or Parquet), producing one row per record.
303    LoadData(LoadDataOp),
304}
305
306impl LogicalOperator {
307    /// Returns `true` if this operator or any of its children perform mutations.
308    #[must_use]
309    pub fn has_mutations(&self) -> bool {
310        match self {
311            // Direct mutation operators
312            Self::CreateNode(_)
313            | Self::CreateEdge(_)
314            | Self::DeleteNode(_)
315            | Self::DeleteEdge(_)
316            | Self::SetProperty(_)
317            | Self::AddLabel(_)
318            | Self::RemoveLabel(_)
319            | Self::Merge(_)
320            | Self::MergeRelationship(_)
321            | Self::InsertTriple(_)
322            | Self::DeleteTriple(_)
323            | Self::Modify(_)
324            | Self::ClearGraph(_)
325            | Self::CreateGraph(_)
326            | Self::DropGraph(_)
327            | Self::LoadGraph(_)
328            | Self::CopyGraph(_)
329            | Self::MoveGraph(_)
330            | Self::AddGraph(_)
331            | Self::CreatePropertyGraph(_) => true,
332
333            // Operators with an `input` child
334            Self::Filter(op) => op.input.has_mutations(),
335            Self::Project(op) => op.input.has_mutations(),
336            Self::Aggregate(op) => op.input.has_mutations(),
337            Self::Limit(op) => op.input.has_mutations(),
338            Self::Skip(op) => op.input.has_mutations(),
339            Self::Sort(op) => op.input.has_mutations(),
340            Self::Distinct(op) => op.input.has_mutations(),
341            Self::Unwind(op) => op.input.has_mutations(),
342            Self::Bind(op) => op.input.has_mutations(),
343            Self::MapCollect(op) => op.input.has_mutations(),
344            Self::Return(op) => op.input.has_mutations(),
345            Self::HorizontalAggregate(op) => op.input.has_mutations(),
346            Self::VectorScan(_) | Self::VectorJoin(_) => false,
347
348            // Operators with two children
349            Self::Join(op) => op.left.has_mutations() || op.right.has_mutations(),
350            Self::LeftJoin(op) => op.left.has_mutations() || op.right.has_mutations(),
351            Self::AntiJoin(op) => op.left.has_mutations() || op.right.has_mutations(),
352            Self::Except(op) => op.left.has_mutations() || op.right.has_mutations(),
353            Self::Intersect(op) => op.left.has_mutations() || op.right.has_mutations(),
354            Self::Otherwise(op) => op.left.has_mutations() || op.right.has_mutations(),
355            Self::Union(op) => op.inputs.iter().any(|i| i.has_mutations()),
356            Self::MultiWayJoin(op) => op.inputs.iter().any(|i| i.has_mutations()),
357            Self::Apply(op) => op.input.has_mutations() || op.subplan.has_mutations(),
358
359            // Leaf operators (read-only)
360            Self::NodeScan(_)
361            | Self::EdgeScan(_)
362            | Self::Expand(_)
363            | Self::TripleScan(_)
364            | Self::ShortestPath(_)
365            | Self::Empty
366            | Self::ParameterScan(_)
367            | Self::CallProcedure(_)
368            | Self::LoadData(_) => false,
369        }
370    }
371
372    /// Returns references to the child operators.
373    ///
374    /// Used by [`crate::query::profile::build_profile_tree`] to walk the logical
375    /// plan tree in post-order, matching operators to profiling entries.
376    #[must_use]
377    pub fn children(&self) -> Vec<&LogicalOperator> {
378        match self {
379            // Optional single input
380            Self::NodeScan(op) => op.input.as_deref().into_iter().collect(),
381            Self::EdgeScan(op) => op.input.as_deref().into_iter().collect(),
382            Self::TripleScan(op) => op.input.as_deref().into_iter().collect(),
383            Self::VectorScan(op) => op.input.as_deref().into_iter().collect(),
384            Self::CreateNode(op) => op.input.as_deref().into_iter().collect(),
385            Self::InsertTriple(op) => op.input.as_deref().into_iter().collect(),
386            Self::DeleteTriple(op) => op.input.as_deref().into_iter().collect(),
387
388            // Single required input
389            Self::Expand(op) => vec![&*op.input],
390            Self::Filter(op) => vec![&*op.input],
391            Self::Project(op) => vec![&*op.input],
392            Self::Aggregate(op) => vec![&*op.input],
393            Self::Limit(op) => vec![&*op.input],
394            Self::Skip(op) => vec![&*op.input],
395            Self::Sort(op) => vec![&*op.input],
396            Self::Distinct(op) => vec![&*op.input],
397            Self::Return(op) => vec![&*op.input],
398            Self::Unwind(op) => vec![&*op.input],
399            Self::Bind(op) => vec![&*op.input],
400            Self::MapCollect(op) => vec![&*op.input],
401            Self::ShortestPath(op) => vec![&*op.input],
402            Self::Merge(op) => vec![&*op.input],
403            Self::MergeRelationship(op) => vec![&*op.input],
404            Self::CreateEdge(op) => vec![&*op.input],
405            Self::DeleteNode(op) => vec![&*op.input],
406            Self::DeleteEdge(op) => vec![&*op.input],
407            Self::SetProperty(op) => vec![&*op.input],
408            Self::AddLabel(op) => vec![&*op.input],
409            Self::RemoveLabel(op) => vec![&*op.input],
410            Self::HorizontalAggregate(op) => vec![&*op.input],
411            Self::VectorJoin(op) => vec![&*op.input],
412            Self::Modify(op) => vec![&*op.where_clause],
413
414            // Two children (left + right)
415            Self::Join(op) => vec![&*op.left, &*op.right],
416            Self::LeftJoin(op) => vec![&*op.left, &*op.right],
417            Self::AntiJoin(op) => vec![&*op.left, &*op.right],
418            Self::Except(op) => vec![&*op.left, &*op.right],
419            Self::Intersect(op) => vec![&*op.left, &*op.right],
420            Self::Otherwise(op) => vec![&*op.left, &*op.right],
421
422            // Two children (input + subplan)
423            Self::Apply(op) => vec![&*op.input, &*op.subplan],
424
425            // Vec children
426            Self::Union(op) => op.inputs.iter().collect(),
427            Self::MultiWayJoin(op) => op.inputs.iter().collect(),
428
429            // Leaf operators
430            Self::Empty
431            | Self::ParameterScan(_)
432            | Self::CallProcedure(_)
433            | Self::ClearGraph(_)
434            | Self::CreateGraph(_)
435            | Self::DropGraph(_)
436            | Self::LoadGraph(_)
437            | Self::CopyGraph(_)
438            | Self::MoveGraph(_)
439            | Self::AddGraph(_)
440            | Self::CreatePropertyGraph(_)
441            | Self::LoadData(_) => vec![],
442        }
443    }
444
445    /// Returns a compact display label for this operator, used in PROFILE output.
446    #[must_use]
447    pub fn display_label(&self) -> String {
448        match self {
449            Self::NodeScan(op) => {
450                let label = op.label.as_deref().unwrap_or("*");
451                format!("{}:{}", op.variable, label)
452            }
453            Self::EdgeScan(op) => {
454                let types = if op.edge_types.is_empty() {
455                    "*".to_string()
456                } else {
457                    op.edge_types.join("|")
458                };
459                format!("{}:{}", op.variable, types)
460            }
461            Self::Expand(op) => {
462                let types = if op.edge_types.is_empty() {
463                    "*".to_string()
464                } else {
465                    op.edge_types.join("|")
466                };
467                let dir = match op.direction {
468                    ExpandDirection::Outgoing => "->",
469                    ExpandDirection::Incoming => "<-",
470                    ExpandDirection::Both => "--",
471                };
472                format!(
473                    "({from}){dir}[:{types}]{dir}({to})",
474                    from = op.from_variable,
475                    to = op.to_variable,
476                )
477            }
478            Self::Filter(op) => {
479                let hint = match &op.pushdown_hint {
480                    Some(PushdownHint::IndexLookup { property }) => {
481                        format!(" [index: {property}]")
482                    }
483                    Some(PushdownHint::RangeScan { property }) => {
484                        format!(" [range: {property}]")
485                    }
486                    Some(PushdownHint::LabelFirst) => " [label-first]".to_string(),
487                    None => String::new(),
488                };
489                format!("{}{hint}", fmt_expr(&op.predicate))
490            }
491            Self::Project(op) => {
492                let cols: Vec<String> = op
493                    .projections
494                    .iter()
495                    .map(|p| match &p.alias {
496                        Some(alias) => alias.clone(),
497                        None => fmt_expr(&p.expression),
498                    })
499                    .collect();
500                cols.join(", ")
501            }
502            Self::Join(op) => format!("{:?}", op.join_type),
503            Self::Aggregate(op) => {
504                let groups: Vec<String> = op.group_by.iter().map(fmt_expr).collect();
505                format!("group: [{}]", groups.join(", "))
506            }
507            Self::Limit(op) => format!("{}", op.count),
508            Self::Skip(op) => format!("{}", op.count),
509            Self::Sort(op) => {
510                let keys: Vec<String> = op
511                    .keys
512                    .iter()
513                    .map(|k| {
514                        let dir = match k.order {
515                            SortOrder::Ascending => "ASC",
516                            SortOrder::Descending => "DESC",
517                        };
518                        format!("{} {dir}", fmt_expr(&k.expression))
519                    })
520                    .collect();
521                keys.join(", ")
522            }
523            Self::Distinct(_) => String::new(),
524            Self::Return(op) => {
525                let items: Vec<String> = op
526                    .items
527                    .iter()
528                    .map(|item| match &item.alias {
529                        Some(alias) => alias.clone(),
530                        None => fmt_expr(&item.expression),
531                    })
532                    .collect();
533                items.join(", ")
534            }
535            Self::Union(op) => format!("{} branches", op.inputs.len()),
536            Self::MultiWayJoin(op) => {
537                format!("{} inputs", op.inputs.len())
538            }
539            Self::LeftJoin(_) => String::new(),
540            Self::AntiJoin(_) => String::new(),
541            Self::Unwind(op) => op.variable.clone(),
542            Self::Bind(op) => op.variable.clone(),
543            Self::MapCollect(op) => op.alias.clone(),
544            Self::ShortestPath(op) => {
545                format!("{} -> {}", op.source_var, op.target_var)
546            }
547            Self::Merge(op) => op.variable.clone(),
548            Self::MergeRelationship(op) => op.variable.clone(),
549            Self::CreateNode(op) => {
550                let labels = op.labels.join(":");
551                format!("{}:{labels}", op.variable)
552            }
553            Self::CreateEdge(op) => {
554                format!(
555                    "[{}:{}]",
556                    op.variable.as_deref().unwrap_or("?"),
557                    op.edge_type
558                )
559            }
560            Self::DeleteNode(op) => op.variable.clone(),
561            Self::DeleteEdge(op) => op.variable.clone(),
562            Self::SetProperty(op) => op.variable.clone(),
563            Self::AddLabel(op) => {
564                let labels = op.labels.join(":");
565                format!("{}:{labels}", op.variable)
566            }
567            Self::RemoveLabel(op) => {
568                let labels = op.labels.join(":");
569                format!("{}:{labels}", op.variable)
570            }
571            Self::CallProcedure(op) => op.name.join("."),
572            Self::LoadData(op) => format!("{} AS {}", op.path, op.variable),
573            Self::Apply(_) => String::new(),
574            Self::VectorScan(op) => op.variable.clone(),
575            Self::VectorJoin(op) => op.right_variable.clone(),
576            _ => String::new(),
577        }
578    }
579}
580
581impl LogicalOperator {
582    /// Formats this operator tree as a human-readable plan for EXPLAIN output.
583    pub fn explain_tree(&self) -> String {
584        let mut output = String::new();
585        self.fmt_tree(&mut output, 0);
586        output
587    }
588
589    fn fmt_tree(&self, out: &mut String, depth: usize) {
590        use std::fmt::Write;
591
592        let indent = "  ".repeat(depth);
593        match self {
594            Self::NodeScan(op) => {
595                let label = op.label.as_deref().unwrap_or("*");
596                let _ = writeln!(out, "{indent}NodeScan ({var}:{label})", var = op.variable);
597                if let Some(input) = &op.input {
598                    input.fmt_tree(out, depth + 1);
599                }
600            }
601            Self::EdgeScan(op) => {
602                let types = if op.edge_types.is_empty() {
603                    "*".to_string()
604                } else {
605                    op.edge_types.join("|")
606                };
607                let _ = writeln!(out, "{indent}EdgeScan ({var}:{types})", var = op.variable);
608            }
609            Self::Expand(op) => {
610                let types = if op.edge_types.is_empty() {
611                    "*".to_string()
612                } else {
613                    op.edge_types.join("|")
614                };
615                let dir = match op.direction {
616                    ExpandDirection::Outgoing => "->",
617                    ExpandDirection::Incoming => "<-",
618                    ExpandDirection::Both => "--",
619                };
620                let hops = match (op.min_hops, op.max_hops) {
621                    (1, Some(1)) => String::new(),
622                    (min, Some(max)) if min == max => format!("*{min}"),
623                    (min, Some(max)) => format!("*{min}..{max}"),
624                    (min, None) => format!("*{min}.."),
625                };
626                let _ = writeln!(
627                    out,
628                    "{indent}Expand ({from}){dir}[:{types}{hops}]{dir}({to})",
629                    from = op.from_variable,
630                    to = op.to_variable,
631                );
632                op.input.fmt_tree(out, depth + 1);
633            }
634            Self::Filter(op) => {
635                let hint = match &op.pushdown_hint {
636                    Some(PushdownHint::IndexLookup { property }) => {
637                        format!(" [index: {property}]")
638                    }
639                    Some(PushdownHint::RangeScan { property }) => {
640                        format!(" [range: {property}]")
641                    }
642                    Some(PushdownHint::LabelFirst) => " [label-first]".to_string(),
643                    None => String::new(),
644                };
645                let _ = writeln!(
646                    out,
647                    "{indent}Filter ({expr}){hint}",
648                    expr = fmt_expr(&op.predicate)
649                );
650                op.input.fmt_tree(out, depth + 1);
651            }
652            Self::Project(op) => {
653                let cols: Vec<String> = op
654                    .projections
655                    .iter()
656                    .map(|p| {
657                        let expr = fmt_expr(&p.expression);
658                        match &p.alias {
659                            Some(alias) => format!("{expr} AS {alias}"),
660                            None => expr,
661                        }
662                    })
663                    .collect();
664                let _ = writeln!(out, "{indent}Project ({cols})", cols = cols.join(", "));
665                op.input.fmt_tree(out, depth + 1);
666            }
667            Self::Join(op) => {
668                let _ = writeln!(out, "{indent}Join ({ty:?})", ty = op.join_type);
669                op.left.fmt_tree(out, depth + 1);
670                op.right.fmt_tree(out, depth + 1);
671            }
672            Self::Aggregate(op) => {
673                let groups: Vec<String> = op.group_by.iter().map(fmt_expr).collect();
674                let aggs: Vec<String> = op
675                    .aggregates
676                    .iter()
677                    .map(|a| {
678                        let func = format!("{:?}", a.function).to_lowercase();
679                        match &a.alias {
680                            Some(alias) => format!("{func}(...) AS {alias}"),
681                            None => format!("{func}(...)"),
682                        }
683                    })
684                    .collect();
685                let _ = writeln!(
686                    out,
687                    "{indent}Aggregate (group: [{groups}], aggs: [{aggs}])",
688                    groups = groups.join(", "),
689                    aggs = aggs.join(", "),
690                );
691                op.input.fmt_tree(out, depth + 1);
692            }
693            Self::Limit(op) => {
694                let _ = writeln!(out, "{indent}Limit ({})", op.count);
695                op.input.fmt_tree(out, depth + 1);
696            }
697            Self::Skip(op) => {
698                let _ = writeln!(out, "{indent}Skip ({})", op.count);
699                op.input.fmt_tree(out, depth + 1);
700            }
701            Self::Sort(op) => {
702                let keys: Vec<String> = op
703                    .keys
704                    .iter()
705                    .map(|k| {
706                        let dir = match k.order {
707                            SortOrder::Ascending => "ASC",
708                            SortOrder::Descending => "DESC",
709                        };
710                        format!("{} {dir}", fmt_expr(&k.expression))
711                    })
712                    .collect();
713                let _ = writeln!(out, "{indent}Sort ({keys})", keys = keys.join(", "));
714                op.input.fmt_tree(out, depth + 1);
715            }
716            Self::Distinct(op) => {
717                let _ = writeln!(out, "{indent}Distinct");
718                op.input.fmt_tree(out, depth + 1);
719            }
720            Self::Return(op) => {
721                let items: Vec<String> = op
722                    .items
723                    .iter()
724                    .map(|item| {
725                        let expr = fmt_expr(&item.expression);
726                        match &item.alias {
727                            Some(alias) => format!("{expr} AS {alias}"),
728                            None => expr,
729                        }
730                    })
731                    .collect();
732                let distinct = if op.distinct { " DISTINCT" } else { "" };
733                let _ = writeln!(
734                    out,
735                    "{indent}Return{distinct} ({items})",
736                    items = items.join(", ")
737                );
738                op.input.fmt_tree(out, depth + 1);
739            }
740            Self::Union(op) => {
741                let _ = writeln!(out, "{indent}Union ({n} branches)", n = op.inputs.len());
742                for input in &op.inputs {
743                    input.fmt_tree(out, depth + 1);
744                }
745            }
746            Self::MultiWayJoin(op) => {
747                let vars = op.shared_variables.join(", ");
748                let _ = writeln!(
749                    out,
750                    "{indent}MultiWayJoin ({n} inputs, shared: [{vars}])",
751                    n = op.inputs.len()
752                );
753                for input in &op.inputs {
754                    input.fmt_tree(out, depth + 1);
755                }
756            }
757            Self::LeftJoin(op) => {
758                if let Some(cond) = &op.condition {
759                    let _ = writeln!(out, "{indent}LeftJoin (condition: {cond:?})");
760                } else {
761                    let _ = writeln!(out, "{indent}LeftJoin");
762                }
763                op.left.fmt_tree(out, depth + 1);
764                op.right.fmt_tree(out, depth + 1);
765            }
766            Self::AntiJoin(op) => {
767                let _ = writeln!(out, "{indent}AntiJoin");
768                op.left.fmt_tree(out, depth + 1);
769                op.right.fmt_tree(out, depth + 1);
770            }
771            Self::Unwind(op) => {
772                let _ = writeln!(out, "{indent}Unwind ({var})", var = op.variable);
773                op.input.fmt_tree(out, depth + 1);
774            }
775            Self::Bind(op) => {
776                let _ = writeln!(out, "{indent}Bind ({var})", var = op.variable);
777                op.input.fmt_tree(out, depth + 1);
778            }
779            Self::MapCollect(op) => {
780                let _ = writeln!(
781                    out,
782                    "{indent}MapCollect ({key} -> {val} AS {alias})",
783                    key = op.key_var,
784                    val = op.value_var,
785                    alias = op.alias
786                );
787                op.input.fmt_tree(out, depth + 1);
788            }
789            Self::Apply(op) => {
790                let _ = writeln!(out, "{indent}Apply");
791                op.input.fmt_tree(out, depth + 1);
792                op.subplan.fmt_tree(out, depth + 1);
793            }
794            Self::Except(op) => {
795                let all = if op.all { " ALL" } else { "" };
796                let _ = writeln!(out, "{indent}Except{all}");
797                op.left.fmt_tree(out, depth + 1);
798                op.right.fmt_tree(out, depth + 1);
799            }
800            Self::Intersect(op) => {
801                let all = if op.all { " ALL" } else { "" };
802                let _ = writeln!(out, "{indent}Intersect{all}");
803                op.left.fmt_tree(out, depth + 1);
804                op.right.fmt_tree(out, depth + 1);
805            }
806            Self::Otherwise(op) => {
807                let _ = writeln!(out, "{indent}Otherwise");
808                op.left.fmt_tree(out, depth + 1);
809                op.right.fmt_tree(out, depth + 1);
810            }
811            Self::ShortestPath(op) => {
812                let _ = writeln!(
813                    out,
814                    "{indent}ShortestPath ({from} -> {to})",
815                    from = op.source_var,
816                    to = op.target_var
817                );
818                op.input.fmt_tree(out, depth + 1);
819            }
820            Self::Merge(op) => {
821                let _ = writeln!(out, "{indent}Merge ({var})", var = op.variable);
822                op.input.fmt_tree(out, depth + 1);
823            }
824            Self::MergeRelationship(op) => {
825                let _ = writeln!(out, "{indent}MergeRelationship ({var})", var = op.variable);
826                op.input.fmt_tree(out, depth + 1);
827            }
828            Self::CreateNode(op) => {
829                let labels = op.labels.join(":");
830                let _ = writeln!(
831                    out,
832                    "{indent}CreateNode ({var}:{labels})",
833                    var = op.variable
834                );
835                if let Some(input) = &op.input {
836                    input.fmt_tree(out, depth + 1);
837                }
838            }
839            Self::CreateEdge(op) => {
840                let var = op.variable.as_deref().unwrap_or("?");
841                let _ = writeln!(
842                    out,
843                    "{indent}CreateEdge ({from})-[{var}:{ty}]->({to})",
844                    from = op.from_variable,
845                    ty = op.edge_type,
846                    to = op.to_variable
847                );
848                op.input.fmt_tree(out, depth + 1);
849            }
850            Self::DeleteNode(op) => {
851                let _ = writeln!(out, "{indent}DeleteNode ({var})", var = op.variable);
852                op.input.fmt_tree(out, depth + 1);
853            }
854            Self::DeleteEdge(op) => {
855                let _ = writeln!(out, "{indent}DeleteEdge ({var})", var = op.variable);
856                op.input.fmt_tree(out, depth + 1);
857            }
858            Self::SetProperty(op) => {
859                let props: Vec<String> = op
860                    .properties
861                    .iter()
862                    .map(|(k, _)| format!("{}.{k}", op.variable))
863                    .collect();
864                let _ = writeln!(
865                    out,
866                    "{indent}SetProperty ({props})",
867                    props = props.join(", ")
868                );
869                op.input.fmt_tree(out, depth + 1);
870            }
871            Self::AddLabel(op) => {
872                let labels = op.labels.join(":");
873                let _ = writeln!(out, "{indent}AddLabel ({var}:{labels})", var = op.variable);
874                op.input.fmt_tree(out, depth + 1);
875            }
876            Self::RemoveLabel(op) => {
877                let labels = op.labels.join(":");
878                let _ = writeln!(
879                    out,
880                    "{indent}RemoveLabel ({var}:{labels})",
881                    var = op.variable
882                );
883                op.input.fmt_tree(out, depth + 1);
884            }
885            Self::CallProcedure(op) => {
886                let _ = writeln!(
887                    out,
888                    "{indent}CallProcedure ({name})",
889                    name = op.name.join(".")
890                );
891            }
892            Self::LoadData(op) => {
893                let format_name = match op.format {
894                    LoadDataFormat::Csv => "LoadCsv",
895                    LoadDataFormat::Jsonl => "LoadJsonl",
896                    LoadDataFormat::Parquet => "LoadParquet",
897                    _ => "LoadData",
898                };
899                let headers = if op.with_headers && op.format == LoadDataFormat::Csv {
900                    " WITH HEADERS"
901                } else {
902                    ""
903                };
904                let _ = writeln!(
905                    out,
906                    "{indent}{format_name}{headers} ('{path}' AS {var})",
907                    path = op.path,
908                    var = op.variable,
909                );
910            }
911            Self::TripleScan(op) => {
912                let _ = writeln!(
913                    out,
914                    "{indent}TripleScan ({s} {p} {o})",
915                    s = fmt_triple_component(&op.subject),
916                    p = fmt_triple_component(&op.predicate),
917                    o = fmt_triple_component(&op.object)
918                );
919                if let Some(input) = &op.input {
920                    input.fmt_tree(out, depth + 1);
921                }
922            }
923            Self::Empty => {
924                let _ = writeln!(out, "{indent}Empty");
925            }
926            // Remaining operators: show a simple name
927            _ => {
928                let _ = writeln!(out, "{indent}{:?}", std::mem::discriminant(self));
929            }
930        }
931    }
932}
933
934/// Format a logical expression compactly for EXPLAIN output.
935fn fmt_expr(expr: &LogicalExpression) -> String {
936    match expr {
937        LogicalExpression::Variable(name) => name.clone(),
938        LogicalExpression::Property { variable, property } => format!("{variable}.{property}"),
939        LogicalExpression::Literal(val) => format!("{val}"),
940        LogicalExpression::Binary { left, op, right } => {
941            format!("{} {op:?} {}", fmt_expr(left), fmt_expr(right))
942        }
943        LogicalExpression::Unary { op, operand } => {
944            format!("{op:?} {}", fmt_expr(operand))
945        }
946        LogicalExpression::FunctionCall { name, args, .. } => {
947            let arg_strs: Vec<String> = args.iter().map(fmt_expr).collect();
948            format!("{name}({})", arg_strs.join(", "))
949        }
950        _ => format!("{expr:?}"),
951    }
952}
953
954/// Format a triple component for EXPLAIN output.
955fn fmt_triple_component(comp: &TripleComponent) -> String {
956    match comp {
957        TripleComponent::Variable(name) => format!("?{name}"),
958        TripleComponent::Iri(iri) => format!("<{iri}>"),
959        TripleComponent::Literal(val) => format!("{val}"),
960        TripleComponent::LangLiteral { value, lang } => format!("\"{value}\"@{lang}"),
961        TripleComponent::BlankNode(label) => format!("_:{label}"),
962    }
963}
964
965/// Scan nodes from the graph.
966#[derive(Debug, Clone)]
967pub struct NodeScanOp {
968    /// Variable name to bind the node to.
969    pub variable: String,
970    /// Optional label filter.
971    pub label: Option<String>,
972    /// Child operator (if any, for chained patterns).
973    pub input: Option<Box<LogicalOperator>>,
974}
975
976/// Scan edges from the graph.
977#[derive(Debug, Clone)]
978pub struct EdgeScanOp {
979    /// Variable name to bind the edge to.
980    pub variable: String,
981    /// Edge type filter (empty = match all types).
982    pub edge_types: Vec<String>,
983    /// Child operator (if any).
984    pub input: Option<Box<LogicalOperator>>,
985}
986
987/// Path traversal mode for variable-length expansion.
988#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
989#[non_exhaustive]
990pub enum PathMode {
991    /// Allows repeated nodes and edges (default).
992    #[default]
993    Walk,
994    /// No repeated edges.
995    Trail,
996    /// No repeated nodes except endpoints.
997    Simple,
998    /// No repeated nodes at all.
999    Acyclic,
1000}
1001
1002/// Expand from nodes to their neighbors.
1003#[derive(Debug, Clone)]
1004pub struct ExpandOp {
1005    /// Source node variable.
1006    pub from_variable: String,
1007    /// Target node variable to bind.
1008    pub to_variable: String,
1009    /// Edge variable to bind (optional).
1010    pub edge_variable: Option<String>,
1011    /// Direction of expansion.
1012    pub direction: ExpandDirection,
1013    /// Edge type filter (empty = match all types, multiple = match any).
1014    pub edge_types: Vec<String>,
1015    /// Minimum hops (for variable-length patterns).
1016    pub min_hops: u32,
1017    /// Maximum hops (for variable-length patterns).
1018    pub max_hops: Option<u32>,
1019    /// Input operator.
1020    pub input: Box<LogicalOperator>,
1021    /// Path alias for variable-length patterns (e.g., `p` in `p = (a)-[*1..3]->(b)`).
1022    /// When set, a path length column will be output under this name.
1023    pub path_alias: Option<String>,
1024    /// Path traversal mode (WALK, TRAIL, SIMPLE, ACYCLIC).
1025    pub path_mode: PathMode,
1026}
1027
1028/// Direction for edge expansion.
1029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1030#[non_exhaustive]
1031pub enum ExpandDirection {
1032    /// Follow outgoing edges.
1033    Outgoing,
1034    /// Follow incoming edges.
1035    Incoming,
1036    /// Follow edges in either direction.
1037    Both,
1038}
1039
1040/// Join two inputs.
1041#[derive(Debug, Clone)]
1042pub struct JoinOp {
1043    /// Left input.
1044    pub left: Box<LogicalOperator>,
1045    /// Right input.
1046    pub right: Box<LogicalOperator>,
1047    /// Join type.
1048    pub join_type: JoinType,
1049    /// Join conditions.
1050    pub conditions: Vec<JoinCondition>,
1051}
1052
1053/// Join type.
1054#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1055#[non_exhaustive]
1056pub enum JoinType {
1057    /// Inner join.
1058    Inner,
1059    /// Left outer join.
1060    Left,
1061    /// Right outer join.
1062    Right,
1063    /// Full outer join.
1064    Full,
1065    /// Cross join (Cartesian product).
1066    Cross,
1067    /// Semi join (returns left rows with matching right rows).
1068    Semi,
1069    /// Anti join (returns left rows without matching right rows).
1070    Anti,
1071}
1072
1073/// A join condition.
1074#[derive(Debug, Clone)]
1075pub struct JoinCondition {
1076    /// Left expression.
1077    pub left: LogicalExpression,
1078    /// Right expression.
1079    pub right: LogicalExpression,
1080}
1081
1082/// Multi-way join for worst-case optimal joins (leapfrog).
1083///
1084/// Unlike binary `JoinOp`, this joins 3+ relations simultaneously
1085/// using the leapfrog trie join algorithm. Preferred for cyclic patterns
1086/// (triangles, cliques) where cascading binary joins hit O(N^2).
1087#[derive(Debug, Clone)]
1088pub struct MultiWayJoinOp {
1089    /// Input relations (one per relation in the join).
1090    pub inputs: Vec<LogicalOperator>,
1091    /// All pairwise join conditions.
1092    pub conditions: Vec<JoinCondition>,
1093    /// Variables shared across multiple inputs (intersection keys).
1094    pub shared_variables: Vec<String>,
1095}
1096
1097/// Aggregate with grouping.
1098#[derive(Debug, Clone)]
1099pub struct AggregateOp {
1100    /// Group by expressions.
1101    pub group_by: Vec<LogicalExpression>,
1102    /// Aggregate functions.
1103    pub aggregates: Vec<AggregateExpr>,
1104    /// Input operator.
1105    pub input: Box<LogicalOperator>,
1106    /// HAVING clause filter (applied after aggregation).
1107    pub having: Option<LogicalExpression>,
1108}
1109
1110/// Whether a horizontal aggregate operates on edges or nodes.
1111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112#[non_exhaustive]
1113pub enum EntityKind {
1114    /// Aggregate over edges in a path.
1115    Edge,
1116    /// Aggregate over nodes in a path.
1117    Node,
1118}
1119
1120/// Per-row aggregation over a list-valued column (horizontal aggregation, GE09).
1121///
1122/// For each input row, reads a list of entity IDs from `list_column`, accesses
1123/// `property` on each entity, computes the aggregate, and emits the scalar result.
1124#[derive(Debug, Clone)]
1125pub struct HorizontalAggregateOp {
1126    /// The list column name (e.g., `_path_edges_p`).
1127    pub list_column: String,
1128    /// Whether the list contains edge IDs or node IDs.
1129    pub entity_kind: EntityKind,
1130    /// The aggregate function to apply.
1131    pub function: AggregateFunction,
1132    /// The property to access on each entity.
1133    pub property: String,
1134    /// Output alias for the result column.
1135    pub alias: String,
1136    /// Input operator.
1137    pub input: Box<LogicalOperator>,
1138}
1139
1140/// An aggregate expression.
1141#[derive(Debug, Clone)]
1142pub struct AggregateExpr {
1143    /// Aggregate function.
1144    pub function: AggregateFunction,
1145    /// Expression to aggregate (first/only argument, y for binary set functions).
1146    pub expression: Option<LogicalExpression>,
1147    /// Second expression for binary set functions (x for COVAR, CORR, REGR_*).
1148    pub expression2: Option<LogicalExpression>,
1149    /// Whether to use DISTINCT.
1150    pub distinct: bool,
1151    /// Alias for the result.
1152    pub alias: Option<String>,
1153    /// Percentile parameter for PERCENTILE_DISC/PERCENTILE_CONT (0.0 to 1.0).
1154    pub percentile: Option<f64>,
1155    /// Separator string for GROUP_CONCAT / LISTAGG (defaults to space for GROUP_CONCAT, comma for LISTAGG).
1156    pub separator: Option<String>,
1157}
1158
1159/// Aggregate function.
1160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1161#[non_exhaustive]
1162pub enum AggregateFunction {
1163    /// Count all rows (COUNT(*)).
1164    Count,
1165    /// Count non-null values (COUNT(expr)).
1166    CountNonNull,
1167    /// Sum values.
1168    Sum,
1169    /// Average values.
1170    Avg,
1171    /// Minimum value.
1172    Min,
1173    /// Maximum value.
1174    Max,
1175    /// Collect into list.
1176    Collect,
1177    /// Sample standard deviation (STDEV).
1178    StdDev,
1179    /// Population standard deviation (STDEVP).
1180    StdDevPop,
1181    /// Sample variance (VAR_SAMP / VARIANCE).
1182    Variance,
1183    /// Population variance (VAR_POP).
1184    VariancePop,
1185    /// Discrete percentile (PERCENTILE_DISC).
1186    PercentileDisc,
1187    /// Continuous percentile (PERCENTILE_CONT).
1188    PercentileCont,
1189    /// Concatenate values with separator (GROUP_CONCAT).
1190    GroupConcat,
1191    /// Return an arbitrary value from the group (SAMPLE).
1192    Sample,
1193    /// Sample covariance (COVAR_SAMP(y, x)).
1194    CovarSamp,
1195    /// Population covariance (COVAR_POP(y, x)).
1196    CovarPop,
1197    /// Pearson correlation coefficient (CORR(y, x)).
1198    Corr,
1199    /// Regression slope (REGR_SLOPE(y, x)).
1200    RegrSlope,
1201    /// Regression intercept (REGR_INTERCEPT(y, x)).
1202    RegrIntercept,
1203    /// Coefficient of determination (REGR_R2(y, x)).
1204    RegrR2,
1205    /// Regression count of non-null pairs (REGR_COUNT(y, x)).
1206    RegrCount,
1207    /// Regression sum of squares for x (REGR_SXX(y, x)).
1208    RegrSxx,
1209    /// Regression sum of squares for y (REGR_SYY(y, x)).
1210    RegrSyy,
1211    /// Regression sum of cross-products (REGR_SXY(y, x)).
1212    RegrSxy,
1213    /// Regression average of x (REGR_AVGX(y, x)).
1214    RegrAvgx,
1215    /// Regression average of y (REGR_AVGY(y, x)).
1216    RegrAvgy,
1217}
1218
1219/// Hint about how a filter will be executed at the physical level.
1220///
1221/// Set during EXPLAIN annotation to communicate pushdown decisions.
1222#[derive(Debug, Clone)]
1223#[non_exhaustive]
1224pub enum PushdownHint {
1225    /// Equality predicate resolved via a property index.
1226    IndexLookup {
1227        /// The indexed property name.
1228        property: String,
1229    },
1230    /// Range predicate resolved via a range/btree index.
1231    RangeScan {
1232        /// The indexed property name.
1233        property: String,
1234    },
1235    /// No index available, but label narrows the scan before filtering.
1236    LabelFirst,
1237}
1238
1239/// Filter rows based on a predicate.
1240#[derive(Debug, Clone)]
1241pub struct FilterOp {
1242    /// The filter predicate.
1243    pub predicate: LogicalExpression,
1244    /// Input operator.
1245    pub input: Box<LogicalOperator>,
1246    /// Optional hint about pushdown strategy (populated by EXPLAIN).
1247    pub pushdown_hint: Option<PushdownHint>,
1248}
1249
1250/// Project specific columns.
1251#[derive(Debug, Clone)]
1252pub struct ProjectOp {
1253    /// Columns to project.
1254    pub projections: Vec<Projection>,
1255    /// Input operator.
1256    pub input: Box<LogicalOperator>,
1257    /// When true, all input columns are passed through and the explicit
1258    /// projections are appended as additional output columns. Used by GQL
1259    /// LET clauses which add bindings without replacing the existing scope.
1260    pub pass_through_input: bool,
1261}
1262
1263/// A single projection (column selection or computation).
1264#[derive(Debug, Clone)]
1265pub struct Projection {
1266    /// Expression to compute.
1267    pub expression: LogicalExpression,
1268    /// Alias for the result.
1269    pub alias: Option<String>,
1270}
1271
1272/// Limit the number of results.
1273#[derive(Debug, Clone)]
1274pub struct LimitOp {
1275    /// Maximum number of rows to return (literal or parameter reference).
1276    pub count: CountExpr,
1277    /// Input operator.
1278    pub input: Box<LogicalOperator>,
1279}
1280
1281/// Skip a number of results.
1282#[derive(Debug, Clone)]
1283pub struct SkipOp {
1284    /// Number of rows to skip (literal or parameter reference).
1285    pub count: CountExpr,
1286    /// Input operator.
1287    pub input: Box<LogicalOperator>,
1288}
1289
1290/// Sort results.
1291#[derive(Debug, Clone)]
1292pub struct SortOp {
1293    /// Sort keys.
1294    pub keys: Vec<SortKey>,
1295    /// Input operator.
1296    pub input: Box<LogicalOperator>,
1297}
1298
1299/// A sort key.
1300#[derive(Debug, Clone)]
1301pub struct SortKey {
1302    /// Expression to sort by.
1303    pub expression: LogicalExpression,
1304    /// Sort order.
1305    pub order: SortOrder,
1306    /// Optional null ordering (NULLS FIRST / NULLS LAST).
1307    pub nulls: Option<NullsOrdering>,
1308}
1309
1310/// Sort order.
1311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1312#[non_exhaustive]
1313pub enum SortOrder {
1314    /// Ascending order.
1315    Ascending,
1316    /// Descending order.
1317    Descending,
1318}
1319
1320/// Null ordering for sort operations.
1321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1322#[non_exhaustive]
1323pub enum NullsOrdering {
1324    /// Nulls sort before all non-null values.
1325    First,
1326    /// Nulls sort after all non-null values.
1327    Last,
1328}
1329
1330/// Remove duplicate results.
1331#[derive(Debug, Clone)]
1332pub struct DistinctOp {
1333    /// Input operator.
1334    pub input: Box<LogicalOperator>,
1335    /// Optional columns to use for deduplication.
1336    /// If None, all columns are used.
1337    pub columns: Option<Vec<String>>,
1338}
1339
1340/// Create a new node.
1341#[derive(Debug, Clone)]
1342pub struct CreateNodeOp {
1343    /// Variable name to bind the created node to.
1344    pub variable: String,
1345    /// Labels for the new node.
1346    pub labels: Vec<String>,
1347    /// Properties for the new node.
1348    pub properties: Vec<(String, LogicalExpression)>,
1349    /// Input operator (for chained creates).
1350    pub input: Option<Box<LogicalOperator>>,
1351}
1352
1353/// Create a new edge.
1354#[derive(Debug, Clone)]
1355pub struct CreateEdgeOp {
1356    /// Variable name to bind the created edge to.
1357    pub variable: Option<String>,
1358    /// Source node variable.
1359    pub from_variable: String,
1360    /// Target node variable.
1361    pub to_variable: String,
1362    /// Edge type.
1363    pub edge_type: String,
1364    /// Properties for the new edge.
1365    pub properties: Vec<(String, LogicalExpression)>,
1366    /// Input operator.
1367    pub input: Box<LogicalOperator>,
1368}
1369
1370/// Delete a node.
1371#[derive(Debug, Clone)]
1372pub struct DeleteNodeOp {
1373    /// Variable of the node to delete.
1374    pub variable: String,
1375    /// Whether to detach (delete connected edges) before deleting.
1376    pub detach: bool,
1377    /// Input operator.
1378    pub input: Box<LogicalOperator>,
1379}
1380
1381/// Delete an edge.
1382#[derive(Debug, Clone)]
1383pub struct DeleteEdgeOp {
1384    /// Variable of the edge to delete.
1385    pub variable: String,
1386    /// Input operator.
1387    pub input: Box<LogicalOperator>,
1388}
1389
1390/// Set properties on a node or edge.
1391#[derive(Debug, Clone)]
1392pub struct SetPropertyOp {
1393    /// Variable of the entity to update.
1394    pub variable: String,
1395    /// Properties to set (name -> expression).
1396    pub properties: Vec<(String, LogicalExpression)>,
1397    /// Whether to replace all properties (vs. merge).
1398    pub replace: bool,
1399    /// Whether the target variable is an edge (vs. node).
1400    pub is_edge: bool,
1401    /// Input operator.
1402    pub input: Box<LogicalOperator>,
1403}
1404
1405/// Add labels to a node.
1406#[derive(Debug, Clone)]
1407pub struct AddLabelOp {
1408    /// Variable of the node to update.
1409    pub variable: String,
1410    /// Labels to add.
1411    pub labels: Vec<String>,
1412    /// Input operator.
1413    pub input: Box<LogicalOperator>,
1414}
1415
1416/// Remove labels from a node.
1417#[derive(Debug, Clone)]
1418pub struct RemoveLabelOp {
1419    /// Variable of the node to update.
1420    pub variable: String,
1421    /// Labels to remove.
1422    pub labels: Vec<String>,
1423    /// Input operator.
1424    pub input: Box<LogicalOperator>,
1425}
1426
1427// ==================== RDF/SPARQL Operators ====================
1428
1429/// SPARQL dataset restriction from FROM / FROM NAMED clauses.
1430///
1431/// When present, restricts which graphs are visible to a triple scan:
1432/// - `default_graphs`: IRIs whose union forms the default graph (basic patterns).
1433/// - `named_graphs`: IRIs that enumerate the available named graphs (GRAPH patterns).
1434#[derive(Debug, Clone, Default)]
1435pub struct DatasetRestriction {
1436    /// FROM IRIs: the default graph is the union of these named graphs.
1437    /// Empty means no FROM clause was specified (unrestricted default graph).
1438    pub default_graphs: Vec<String>,
1439    /// FROM NAMED IRIs: only these named graphs are available to GRAPH patterns.
1440    /// Empty means no FROM NAMED clause was specified (all named graphs visible).
1441    pub named_graphs: Vec<String>,
1442}
1443
1444/// Scan RDF triples matching a pattern.
1445#[derive(Debug, Clone)]
1446pub struct TripleScanOp {
1447    /// Subject pattern (variable name or IRI).
1448    pub subject: TripleComponent,
1449    /// Predicate pattern (variable name or IRI).
1450    pub predicate: TripleComponent,
1451    /// Object pattern (variable name, IRI, or literal).
1452    pub object: TripleComponent,
1453    /// Named graph (optional).
1454    pub graph: Option<TripleComponent>,
1455    /// Input operator (for chained patterns).
1456    pub input: Option<Box<LogicalOperator>>,
1457    /// Dataset restriction from SPARQL FROM / FROM NAMED clauses.
1458    pub dataset: Option<DatasetRestriction>,
1459}
1460
1461/// A component of a triple pattern.
1462#[derive(Debug, Clone)]
1463#[non_exhaustive]
1464pub enum TripleComponent {
1465    /// A variable to bind.
1466    Variable(String),
1467    /// A constant IRI.
1468    Iri(String),
1469    /// A constant literal value.
1470    Literal(Value),
1471    /// A language-tagged string literal (RDF `rdf:langString`).
1472    ///
1473    /// Carries the lexical value and the BCP47 language tag separately so that
1474    /// the tag survives the translator to planner to RDF store round-trip.
1475    LangLiteral {
1476        /// The lexical string value.
1477        value: String,
1478        /// BCP47 language tag, e.g. `"fr"`, `"en-GB"`.
1479        lang: String,
1480    },
1481    /// A blank node with a scoped label (used in INSERT DATA).
1482    BlankNode(String),
1483}
1484
1485/// Union of multiple result sets.
1486#[derive(Debug, Clone)]
1487pub struct UnionOp {
1488    /// Inputs to union together.
1489    pub inputs: Vec<LogicalOperator>,
1490}
1491
1492/// Set difference: rows in left that are not in right.
1493#[derive(Debug, Clone)]
1494pub struct ExceptOp {
1495    /// Left input.
1496    pub left: Box<LogicalOperator>,
1497    /// Right input (rows to exclude).
1498    pub right: Box<LogicalOperator>,
1499    /// If true, preserve duplicates (EXCEPT ALL); if false, deduplicate (EXCEPT DISTINCT).
1500    pub all: bool,
1501}
1502
1503/// Set intersection: rows common to both inputs.
1504#[derive(Debug, Clone)]
1505pub struct IntersectOp {
1506    /// Left input.
1507    pub left: Box<LogicalOperator>,
1508    /// Right input.
1509    pub right: Box<LogicalOperator>,
1510    /// If true, preserve duplicates (INTERSECT ALL); if false, deduplicate (INTERSECT DISTINCT).
1511    pub all: bool,
1512}
1513
1514/// Fallback operator: use left result if non-empty, otherwise use right.
1515#[derive(Debug, Clone)]
1516pub struct OtherwiseOp {
1517    /// Primary input (preferred).
1518    pub left: Box<LogicalOperator>,
1519    /// Fallback input (used only if left produces zero rows).
1520    pub right: Box<LogicalOperator>,
1521}
1522
1523/// Apply (lateral join): evaluate a subplan for each row of the outer input.
1524///
1525/// The subplan can reference variables bound by the outer input. Results are
1526/// concatenated (cross-product per row).
1527#[derive(Debug, Clone)]
1528pub struct ApplyOp {
1529    /// Outer input providing rows.
1530    pub input: Box<LogicalOperator>,
1531    /// Subplan to evaluate per outer row.
1532    pub subplan: Box<LogicalOperator>,
1533    /// Variables imported from the outer scope into the inner plan.
1534    /// When non-empty, the planner injects these via `ParameterState`.
1535    pub shared_variables: Vec<String>,
1536    /// When true, uses left-join semantics: outer rows with no matching inner
1537    /// rows are emitted with NULLs for the inner columns (OPTIONAL CALL).
1538    pub optional: bool,
1539}
1540
1541/// Parameter scan: leaf operator for correlated subquery inner plans.
1542///
1543/// Emits a single row containing the values injected from the outer Apply.
1544/// Column names correspond to the outer variables imported via WITH.
1545#[derive(Debug, Clone)]
1546pub struct ParameterScanOp {
1547    /// Column names for the injected parameters.
1548    pub columns: Vec<String>,
1549}
1550
1551/// Left outer join for OPTIONAL patterns.
1552#[derive(Debug, Clone)]
1553pub struct LeftJoinOp {
1554    /// Left (required) input.
1555    pub left: Box<LogicalOperator>,
1556    /// Right (optional) input.
1557    pub right: Box<LogicalOperator>,
1558    /// Optional filter condition.
1559    pub condition: Option<LogicalExpression>,
1560}
1561
1562/// Anti-join for MINUS patterns.
1563#[derive(Debug, Clone)]
1564pub struct AntiJoinOp {
1565    /// Left input (results to keep if no match on right).
1566    pub left: Box<LogicalOperator>,
1567    /// Right input (patterns to exclude).
1568    pub right: Box<LogicalOperator>,
1569}
1570
1571/// Bind a variable to an expression.
1572#[derive(Debug, Clone)]
1573pub struct BindOp {
1574    /// Expression to compute.
1575    pub expression: LogicalExpression,
1576    /// Variable to bind the result to.
1577    pub variable: String,
1578    /// Input operator.
1579    pub input: Box<LogicalOperator>,
1580}
1581
1582/// Unwind a list into individual rows.
1583///
1584/// For each input row, evaluates the expression (which should return a list)
1585/// and emits one row for each element in the list.
1586#[derive(Debug, Clone)]
1587pub struct UnwindOp {
1588    /// The list expression to unwind.
1589    pub expression: LogicalExpression,
1590    /// The variable name for each element.
1591    pub variable: String,
1592    /// Optional variable for 1-based element position (ORDINALITY).
1593    pub ordinality_var: Option<String>,
1594    /// Optional variable for 0-based element position (OFFSET).
1595    pub offset_var: Option<String>,
1596    /// Input operator.
1597    pub input: Box<LogicalOperator>,
1598}
1599
1600/// Collect grouped key-value rows into a single Map value.
1601/// Used for Gremlin `groupCount()` semantics.
1602#[derive(Debug, Clone)]
1603pub struct MapCollectOp {
1604    /// Variable holding the map key.
1605    pub key_var: String,
1606    /// Variable holding the map value.
1607    pub value_var: String,
1608    /// Output variable alias.
1609    pub alias: String,
1610    /// Input operator (typically a grouped aggregate).
1611    pub input: Box<LogicalOperator>,
1612}
1613
1614/// Merge a pattern (match or create).
1615///
1616/// MERGE tries to match a pattern in the graph. If found, returns the existing
1617/// elements (optionally applying ON MATCH SET). If not found, creates the pattern
1618/// (optionally applying ON CREATE SET).
1619#[derive(Debug, Clone)]
1620pub struct MergeOp {
1621    /// The node to merge.
1622    pub variable: String,
1623    /// Labels to match/create.
1624    pub labels: Vec<String>,
1625    /// Properties that must match (used for both matching and creation).
1626    pub match_properties: Vec<(String, LogicalExpression)>,
1627    /// Properties to set on CREATE.
1628    pub on_create: Vec<(String, LogicalExpression)>,
1629    /// Properties to set on MATCH.
1630    pub on_match: Vec<(String, LogicalExpression)>,
1631    /// Input operator.
1632    pub input: Box<LogicalOperator>,
1633}
1634
1635/// Merge a relationship pattern (match or create between two bound nodes).
1636///
1637/// MERGE on a relationship tries to find an existing relationship of the given type
1638/// between the source and target nodes. If found, returns the existing relationship
1639/// (optionally applying ON MATCH SET). If not found, creates it (optionally applying
1640/// ON CREATE SET).
1641#[derive(Debug, Clone)]
1642pub struct MergeRelationshipOp {
1643    /// Variable to bind the relationship to.
1644    pub variable: String,
1645    /// Source node variable (must already be bound).
1646    pub source_variable: String,
1647    /// Target node variable (must already be bound).
1648    pub target_variable: String,
1649    /// Relationship type.
1650    pub edge_type: String,
1651    /// Properties that must match (used for both matching and creation).
1652    pub match_properties: Vec<(String, LogicalExpression)>,
1653    /// Properties to set on CREATE.
1654    pub on_create: Vec<(String, LogicalExpression)>,
1655    /// Properties to set on MATCH.
1656    pub on_match: Vec<(String, LogicalExpression)>,
1657    /// Input operator.
1658    pub input: Box<LogicalOperator>,
1659}
1660
1661/// Find shortest path between two nodes.
1662///
1663/// This operator uses Dijkstra's algorithm to find the shortest path(s)
1664/// between a source node and a target node, optionally filtered by edge type.
1665#[derive(Debug, Clone)]
1666pub struct ShortestPathOp {
1667    /// Input operator providing source/target nodes.
1668    pub input: Box<LogicalOperator>,
1669    /// Variable name for the source node.
1670    pub source_var: String,
1671    /// Variable name for the target node.
1672    pub target_var: String,
1673    /// Edge type filter (empty = match all types, multiple = match any).
1674    pub edge_types: Vec<String>,
1675    /// Direction of edge traversal.
1676    pub direction: ExpandDirection,
1677    /// Variable name to bind the path result.
1678    pub path_alias: String,
1679    /// Whether to find all shortest paths (vs. just one).
1680    pub all_paths: bool,
1681}
1682
1683// ==================== SPARQL Update Operators ====================
1684
1685/// Insert RDF triples.
1686#[derive(Debug, Clone)]
1687pub struct InsertTripleOp {
1688    /// Subject of the triple.
1689    pub subject: TripleComponent,
1690    /// Predicate of the triple.
1691    pub predicate: TripleComponent,
1692    /// Object of the triple.
1693    pub object: TripleComponent,
1694    /// Named graph (optional).
1695    pub graph: Option<String>,
1696    /// Input operator (provides variable bindings).
1697    pub input: Option<Box<LogicalOperator>>,
1698}
1699
1700/// Delete RDF triples.
1701#[derive(Debug, Clone)]
1702pub struct DeleteTripleOp {
1703    /// Subject pattern.
1704    pub subject: TripleComponent,
1705    /// Predicate pattern.
1706    pub predicate: TripleComponent,
1707    /// Object pattern.
1708    pub object: TripleComponent,
1709    /// Named graph (optional).
1710    pub graph: Option<String>,
1711    /// Input operator (provides variable bindings).
1712    pub input: Option<Box<LogicalOperator>>,
1713}
1714
1715/// SPARQL MODIFY operation (DELETE/INSERT WHERE).
1716///
1717/// Per SPARQL 1.1 Update spec, this operator:
1718/// 1. Evaluates the WHERE clause once to get bindings
1719/// 2. Applies DELETE templates using those bindings
1720/// 3. Applies INSERT templates using the SAME bindings
1721///
1722/// This ensures DELETE and INSERT see consistent data.
1723#[derive(Debug, Clone)]
1724pub struct ModifyOp {
1725    /// DELETE triple templates (patterns with variables).
1726    pub delete_templates: Vec<TripleTemplate>,
1727    /// INSERT triple templates (patterns with variables).
1728    pub insert_templates: Vec<TripleTemplate>,
1729    /// WHERE clause that provides variable bindings.
1730    pub where_clause: Box<LogicalOperator>,
1731    /// Named graph context (for WITH clause).
1732    pub graph: Option<String>,
1733}
1734
1735/// A triple template for DELETE/INSERT operations.
1736#[derive(Debug, Clone)]
1737pub struct TripleTemplate {
1738    /// Subject (may be a variable).
1739    pub subject: TripleComponent,
1740    /// Predicate (may be a variable).
1741    pub predicate: TripleComponent,
1742    /// Object (may be a variable or literal).
1743    pub object: TripleComponent,
1744    /// Named graph (optional).
1745    pub graph: Option<String>,
1746}
1747
1748/// Clear all triples from a graph.
1749#[derive(Debug, Clone)]
1750pub struct ClearGraphOp {
1751    /// Target graph (None = default graph, Some("") = all named, Some(iri) = specific graph).
1752    pub graph: Option<String>,
1753    /// Whether to silently ignore errors.
1754    pub silent: bool,
1755}
1756
1757/// Create a new named graph.
1758#[derive(Debug, Clone)]
1759pub struct CreateGraphOp {
1760    /// IRI of the graph to create.
1761    pub graph: String,
1762    /// Whether to silently ignore if graph already exists.
1763    pub silent: bool,
1764}
1765
1766/// Drop (remove) a named graph.
1767#[derive(Debug, Clone)]
1768pub struct DropGraphOp {
1769    /// Target graph (None = default graph).
1770    pub graph: Option<String>,
1771    /// Whether to silently ignore errors.
1772    pub silent: bool,
1773}
1774
1775/// Load data from a URL into a graph.
1776#[derive(Debug, Clone)]
1777pub struct LoadGraphOp {
1778    /// Source URL to load data from.
1779    pub source: String,
1780    /// Destination graph (None = default graph).
1781    pub destination: Option<String>,
1782    /// Whether to silently ignore errors.
1783    pub silent: bool,
1784}
1785
1786/// Copy triples from one graph to another.
1787#[derive(Debug, Clone)]
1788pub struct CopyGraphOp {
1789    /// Source graph.
1790    pub source: Option<String>,
1791    /// Destination graph.
1792    pub destination: Option<String>,
1793    /// Whether to silently ignore errors.
1794    pub silent: bool,
1795}
1796
1797/// Move triples from one graph to another.
1798#[derive(Debug, Clone)]
1799pub struct MoveGraphOp {
1800    /// Source graph.
1801    pub source: Option<String>,
1802    /// Destination graph.
1803    pub destination: Option<String>,
1804    /// Whether to silently ignore errors.
1805    pub silent: bool,
1806}
1807
1808/// Add (merge) triples from one graph to another.
1809#[derive(Debug, Clone)]
1810pub struct AddGraphOp {
1811    /// Source graph.
1812    pub source: Option<String>,
1813    /// Destination graph.
1814    pub destination: Option<String>,
1815    /// Whether to silently ignore errors.
1816    pub silent: bool,
1817}
1818
1819// ==================== Vector Search Operators ====================
1820
1821/// Vector similarity scan operation.
1822///
1823/// Performs approximate nearest neighbor search using a vector index (HNSW)
1824/// or brute-force search for small datasets. Returns nodes/edges whose
1825/// embeddings are similar to the query vector.
1826///
1827/// # Example GQL
1828///
1829/// ```gql
1830/// MATCH (m:Movie)
1831/// WHERE vector_similarity(m.embedding, $query_vector) > 0.8
1832/// RETURN m.title
1833/// ```
1834#[derive(Debug, Clone)]
1835pub struct VectorScanOp {
1836    /// Variable name to bind matching entities to.
1837    pub variable: String,
1838    /// Name of the vector index to use (None = brute-force).
1839    pub index_name: Option<String>,
1840    /// Property containing the vector embedding.
1841    pub property: String,
1842    /// Optional label filter (scan only nodes with this label).
1843    pub label: Option<String>,
1844    /// The query vector expression.
1845    pub query_vector: LogicalExpression,
1846    /// Number of nearest neighbors to return.
1847    pub k: usize,
1848    /// Distance metric (None = use index default, typically cosine).
1849    pub metric: Option<VectorMetric>,
1850    /// Minimum similarity threshold (filters results below this).
1851    pub min_similarity: Option<f32>,
1852    /// Maximum distance threshold (filters results above this).
1853    pub max_distance: Option<f32>,
1854    /// Input operator (for hybrid queries combining graph + vector).
1855    pub input: Option<Box<LogicalOperator>>,
1856}
1857
1858/// Vector distance/similarity metric for vector scan operations.
1859#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1860#[non_exhaustive]
1861pub enum VectorMetric {
1862    /// Cosine similarity (1 - cosine_distance). Best for normalized embeddings.
1863    Cosine,
1864    /// Euclidean (L2) distance. Best when magnitude matters.
1865    Euclidean,
1866    /// Dot product. Best for maximum inner product search.
1867    DotProduct,
1868    /// Manhattan (L1) distance. Less sensitive to outliers.
1869    Manhattan,
1870}
1871
1872/// Join graph patterns with vector similarity search.
1873///
1874/// This operator takes entities from the left input and computes vector
1875/// similarity against a query vector, outputting (entity, distance) pairs.
1876///
1877/// # Use Cases
1878///
1879/// 1. **Hybrid graph + vector queries**: Find similar nodes after graph traversal
1880/// 2. **Aggregated embeddings**: Use AVG(embeddings) as query vector
1881/// 3. **Filtering by similarity**: Join with threshold-based filtering
1882///
1883/// # Example
1884///
1885/// ```gql
1886/// // Find movies similar to what the user liked
1887/// MATCH (u:User {id: $user_id})-[:LIKED]->(liked:Movie)
1888/// WITH avg(liked.embedding) AS user_taste
1889/// VECTOR JOIN (m:Movie) ON m.embedding
1890/// WHERE vector_similarity(m.embedding, user_taste) > 0.7
1891/// RETURN m.title
1892/// ```
1893#[derive(Debug, Clone)]
1894pub struct VectorJoinOp {
1895    /// Input operator providing entities to match against.
1896    pub input: Box<LogicalOperator>,
1897    /// Variable from input to extract vectors from (for entity-to-entity similarity).
1898    /// If None, uses `query_vector` directly.
1899    pub left_vector_variable: Option<String>,
1900    /// Property containing the left vector (used with `left_vector_variable`).
1901    pub left_property: Option<String>,
1902    /// The query vector expression (constant or computed).
1903    pub query_vector: LogicalExpression,
1904    /// Variable name to bind the right-side matching entities.
1905    pub right_variable: String,
1906    /// Property containing the right-side vector embeddings.
1907    pub right_property: String,
1908    /// Optional label filter for right-side entities.
1909    pub right_label: Option<String>,
1910    /// Name of vector index on right side (None = brute-force).
1911    pub index_name: Option<String>,
1912    /// Number of nearest neighbors per left-side entity.
1913    pub k: usize,
1914    /// Distance metric.
1915    pub metric: Option<VectorMetric>,
1916    /// Minimum similarity threshold.
1917    pub min_similarity: Option<f32>,
1918    /// Maximum distance threshold.
1919    pub max_distance: Option<f32>,
1920    /// Variable to bind the distance/similarity score.
1921    pub score_variable: Option<String>,
1922}
1923
1924/// Return results (terminal operator).
1925#[derive(Debug, Clone)]
1926pub struct ReturnOp {
1927    /// Items to return.
1928    pub items: Vec<ReturnItem>,
1929    /// Whether to return distinct results.
1930    pub distinct: bool,
1931    /// Input operator.
1932    pub input: Box<LogicalOperator>,
1933}
1934
1935/// A single return item.
1936#[derive(Debug, Clone)]
1937pub struct ReturnItem {
1938    /// Expression to return.
1939    pub expression: LogicalExpression,
1940    /// Alias for the result column.
1941    pub alias: Option<String>,
1942}
1943
1944/// Define a property graph schema (SQL/PGQ DDL).
1945#[derive(Debug, Clone)]
1946pub struct CreatePropertyGraphOp {
1947    /// Graph name.
1948    pub name: String,
1949    /// Node table schemas (label name + column definitions).
1950    pub node_tables: Vec<PropertyGraphNodeTable>,
1951    /// Edge table schemas (type name + column definitions + references).
1952    pub edge_tables: Vec<PropertyGraphEdgeTable>,
1953}
1954
1955/// A node table in a property graph definition.
1956#[derive(Debug, Clone)]
1957pub struct PropertyGraphNodeTable {
1958    /// Table name (maps to a node label).
1959    pub name: String,
1960    /// Column definitions as (name, type_name) pairs.
1961    pub columns: Vec<(String, String)>,
1962}
1963
1964/// An edge table in a property graph definition.
1965#[derive(Debug, Clone)]
1966pub struct PropertyGraphEdgeTable {
1967    /// Table name (maps to an edge type).
1968    pub name: String,
1969    /// Column definitions as (name, type_name) pairs.
1970    pub columns: Vec<(String, String)>,
1971    /// Source node table name.
1972    pub source_table: String,
1973    /// Target node table name.
1974    pub target_table: String,
1975}
1976
1977// ==================== Procedure Call Types ====================
1978
1979/// A CALL procedure operation.
1980///
1981/// ```text
1982/// CALL grafeo.pagerank({damping: 0.85}) YIELD nodeId, score
1983/// ```
1984#[derive(Debug, Clone)]
1985pub struct CallProcedureOp {
1986    /// Dotted procedure name, e.g. `["grafeo", "pagerank"]`.
1987    pub name: Vec<String>,
1988    /// Argument expressions (constants in Phase 1).
1989    pub arguments: Vec<LogicalExpression>,
1990    /// Optional YIELD clause: which columns to expose + aliases.
1991    pub yield_items: Option<Vec<ProcedureYield>>,
1992}
1993
1994/// A single YIELD item in a procedure call.
1995#[derive(Debug, Clone)]
1996pub struct ProcedureYield {
1997    /// Column name from the procedure result.
1998    pub field_name: String,
1999    /// Optional alias (YIELD score AS rank).
2000    pub alias: Option<String>,
2001}
2002
2003/// Re-export format enum from the physical operator.
2004pub use grafeo_core::execution::operators::LoadDataFormat;
2005
2006/// LOAD DATA operator: reads a file and produces rows.
2007///
2008/// With headers (CSV), each row is bound as a `Value::Map` with column names as keys.
2009/// Without headers (CSV), each row is bound as a `Value::List` of string values.
2010/// JSONL always produces `Value::Map`. Parquet always produces `Value::Map`.
2011#[derive(Debug, Clone)]
2012pub struct LoadDataOp {
2013    /// File format.
2014    pub format: LoadDataFormat,
2015    /// Whether the file has a header row (CSV only, ignored for JSONL/Parquet).
2016    pub with_headers: bool,
2017    /// File path (local filesystem).
2018    pub path: String,
2019    /// Variable name to bind each row to.
2020    pub variable: String,
2021    /// Field separator character (CSV only, default: comma).
2022    pub field_terminator: Option<char>,
2023}
2024
2025/// A logical expression.
2026#[derive(Debug, Clone)]
2027#[non_exhaustive]
2028pub enum LogicalExpression {
2029    /// A literal value.
2030    Literal(Value),
2031
2032    /// A variable reference.
2033    Variable(String),
2034
2035    /// Property access (e.g., n.name).
2036    Property {
2037        /// The variable to access.
2038        variable: String,
2039        /// The property name.
2040        property: String,
2041    },
2042
2043    /// Binary operation.
2044    Binary {
2045        /// Left operand.
2046        left: Box<LogicalExpression>,
2047        /// Operator.
2048        op: BinaryOp,
2049        /// Right operand.
2050        right: Box<LogicalExpression>,
2051    },
2052
2053    /// Unary operation.
2054    Unary {
2055        /// Operator.
2056        op: UnaryOp,
2057        /// Operand.
2058        operand: Box<LogicalExpression>,
2059    },
2060
2061    /// Function call.
2062    FunctionCall {
2063        /// Function name.
2064        name: String,
2065        /// Arguments.
2066        args: Vec<LogicalExpression>,
2067        /// Whether DISTINCT is applied (e.g., COUNT(DISTINCT x)).
2068        distinct: bool,
2069    },
2070
2071    /// List literal.
2072    List(Vec<LogicalExpression>),
2073
2074    /// Map literal (e.g., {name: 'Alix', age: 30}).
2075    Map(Vec<(String, LogicalExpression)>),
2076
2077    /// Index access (e.g., `list[0]`).
2078    IndexAccess {
2079        /// The base expression (typically a list or string).
2080        base: Box<LogicalExpression>,
2081        /// The index expression.
2082        index: Box<LogicalExpression>,
2083    },
2084
2085    /// Slice access (e.g., list[1..3]).
2086    SliceAccess {
2087        /// The base expression (typically a list or string).
2088        base: Box<LogicalExpression>,
2089        /// Start index (None means from beginning).
2090        start: Option<Box<LogicalExpression>>,
2091        /// End index (None means to end).
2092        end: Option<Box<LogicalExpression>>,
2093    },
2094
2095    /// CASE expression.
2096    Case {
2097        /// Test expression (for simple CASE).
2098        operand: Option<Box<LogicalExpression>>,
2099        /// WHEN clauses.
2100        when_clauses: Vec<(LogicalExpression, LogicalExpression)>,
2101        /// ELSE clause.
2102        else_clause: Option<Box<LogicalExpression>>,
2103    },
2104
2105    /// Parameter reference.
2106    Parameter(String),
2107
2108    /// Labels of a node.
2109    Labels(String),
2110
2111    /// Type of an edge.
2112    Type(String),
2113
2114    /// ID of a node or edge.
2115    Id(String),
2116
2117    /// List comprehension: [x IN list WHERE predicate | expression]
2118    ListComprehension {
2119        /// Variable name for each element.
2120        variable: String,
2121        /// The source list expression.
2122        list_expr: Box<LogicalExpression>,
2123        /// Optional filter predicate.
2124        filter_expr: Option<Box<LogicalExpression>>,
2125        /// The mapping expression for each element.
2126        map_expr: Box<LogicalExpression>,
2127    },
2128
2129    /// List predicate: all/any/none/single(x IN list WHERE pred).
2130    ListPredicate {
2131        /// The kind of list predicate.
2132        kind: ListPredicateKind,
2133        /// The iteration variable name.
2134        variable: String,
2135        /// The source list expression.
2136        list_expr: Box<LogicalExpression>,
2137        /// The predicate to test for each element.
2138        predicate: Box<LogicalExpression>,
2139    },
2140
2141    /// EXISTS subquery.
2142    ExistsSubquery(Box<LogicalOperator>),
2143
2144    /// COUNT subquery.
2145    CountSubquery(Box<LogicalOperator>),
2146
2147    /// VALUE subquery: returns scalar value from first row of inner query.
2148    ValueSubquery(Box<LogicalOperator>),
2149
2150    /// Map projection: `node { .prop1, .prop2, key: expr, .* }`.
2151    MapProjection {
2152        /// The base variable name.
2153        base: String,
2154        /// Projection entries (property selectors, literal entries, all-properties).
2155        entries: Vec<MapProjectionEntry>,
2156    },
2157
2158    /// reduce() accumulator: `reduce(acc = init, x IN list | expr)`.
2159    Reduce {
2160        /// Accumulator variable name.
2161        accumulator: String,
2162        /// Initial value for the accumulator.
2163        initial: Box<LogicalExpression>,
2164        /// Iteration variable name.
2165        variable: String,
2166        /// List to iterate over.
2167        list: Box<LogicalExpression>,
2168        /// Body expression evaluated per iteration (references both accumulator and variable).
2169        expression: Box<LogicalExpression>,
2170    },
2171
2172    /// Pattern comprehension: `[(pattern) WHERE pred | expr]`.
2173    ///
2174    /// Executes the inner subplan, evaluates the projection for each row,
2175    /// and collects the results into a list.
2176    PatternComprehension {
2177        /// The subplan produced by translating the pattern (+optional WHERE).
2178        subplan: Box<LogicalOperator>,
2179        /// The projection expression evaluated for each match.
2180        projection: Box<LogicalExpression>,
2181    },
2182}
2183
2184/// An entry in a map projection.
2185#[derive(Debug, Clone)]
2186#[non_exhaustive]
2187pub enum MapProjectionEntry {
2188    /// `.propertyName`: shorthand for `propertyName: base.propertyName`.
2189    PropertySelector(String),
2190    /// `key: expression`: explicit key-value pair.
2191    LiteralEntry(String, LogicalExpression),
2192    /// `.*`: include all properties of the base entity.
2193    AllProperties,
2194}
2195
2196/// The kind of list predicate function.
2197#[derive(Debug, Clone, PartialEq, Eq)]
2198#[non_exhaustive]
2199pub enum ListPredicateKind {
2200    /// all(x IN list WHERE pred): true if pred holds for every element.
2201    All,
2202    /// any(x IN list WHERE pred): true if pred holds for at least one element.
2203    Any,
2204    /// none(x IN list WHERE pred): true if pred holds for no element.
2205    None,
2206    /// single(x IN list WHERE pred): true if pred holds for exactly one element.
2207    Single,
2208}
2209
2210/// Binary operator.
2211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2212#[non_exhaustive]
2213pub enum BinaryOp {
2214    /// Equality comparison (=).
2215    Eq,
2216    /// Inequality comparison (<>).
2217    Ne,
2218    /// Less than (<).
2219    Lt,
2220    /// Less than or equal (<=).
2221    Le,
2222    /// Greater than (>).
2223    Gt,
2224    /// Greater than or equal (>=).
2225    Ge,
2226
2227    /// Logical AND.
2228    And,
2229    /// Logical OR.
2230    Or,
2231    /// Logical XOR.
2232    Xor,
2233
2234    /// Addition (+).
2235    Add,
2236    /// Subtraction (-).
2237    Sub,
2238    /// Multiplication (*).
2239    Mul,
2240    /// Division (/).
2241    Div,
2242    /// Modulo (%).
2243    Mod,
2244
2245    /// String concatenation.
2246    Concat,
2247    /// String starts with.
2248    StartsWith,
2249    /// String ends with.
2250    EndsWith,
2251    /// String contains.
2252    Contains,
2253
2254    /// Collection membership (IN).
2255    In,
2256    /// Pattern matching (LIKE).
2257    Like,
2258    /// Regex matching (=~).
2259    Regex,
2260    /// Power/exponentiation (^).
2261    Pow,
2262}
2263
2264/// Unary operator.
2265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2266#[non_exhaustive]
2267pub enum UnaryOp {
2268    /// Logical NOT.
2269    Not,
2270    /// Numeric negation.
2271    Neg,
2272    /// IS NULL check.
2273    IsNull,
2274    /// IS NOT NULL check.
2275    IsNotNull,
2276}
2277
2278#[cfg(test)]
2279mod tests {
2280    use super::*;
2281
2282    #[test]
2283    fn test_simple_node_scan_plan() {
2284        let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2285            items: vec![ReturnItem {
2286                expression: LogicalExpression::Variable("n".into()),
2287                alias: None,
2288            }],
2289            distinct: false,
2290            input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2291                variable: "n".into(),
2292                label: Some("Person".into()),
2293                input: None,
2294            })),
2295        }));
2296
2297        // Verify structure
2298        if let LogicalOperator::Return(ret) = &plan.root {
2299            assert_eq!(ret.items.len(), 1);
2300            assert!(!ret.distinct);
2301            if let LogicalOperator::NodeScan(scan) = ret.input.as_ref() {
2302                assert_eq!(scan.variable, "n");
2303                assert_eq!(scan.label, Some("Person".into()));
2304            } else {
2305                panic!("Expected NodeScan");
2306            }
2307        } else {
2308            panic!("Expected Return");
2309        }
2310    }
2311
2312    #[test]
2313    fn test_filter_plan() {
2314        let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2315            items: vec![ReturnItem {
2316                expression: LogicalExpression::Property {
2317                    variable: "n".into(),
2318                    property: "name".into(),
2319                },
2320                alias: Some("name".into()),
2321            }],
2322            distinct: false,
2323            input: Box::new(LogicalOperator::Filter(FilterOp {
2324                predicate: LogicalExpression::Binary {
2325                    left: Box::new(LogicalExpression::Property {
2326                        variable: "n".into(),
2327                        property: "age".into(),
2328                    }),
2329                    op: BinaryOp::Gt,
2330                    right: Box::new(LogicalExpression::Literal(Value::Int64(30))),
2331                },
2332                input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2333                    variable: "n".into(),
2334                    label: Some("Person".into()),
2335                    input: None,
2336                })),
2337                pushdown_hint: None,
2338            })),
2339        }));
2340
2341        if let LogicalOperator::Return(ret) = &plan.root {
2342            if let LogicalOperator::Filter(filter) = ret.input.as_ref() {
2343                if let LogicalExpression::Binary { op, .. } = &filter.predicate {
2344                    assert_eq!(*op, BinaryOp::Gt);
2345                } else {
2346                    panic!("Expected Binary expression");
2347                }
2348            } else {
2349                panic!("Expected Filter");
2350            }
2351        } else {
2352            panic!("Expected Return");
2353        }
2354    }
2355}