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