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