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