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