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 grafeo_common::types::Value;
8
9/// A logical query plan.
10#[derive(Debug, Clone)]
11pub struct LogicalPlan {
12 /// The root operator of the plan.
13 pub root: LogicalOperator,
14}
15
16impl LogicalPlan {
17 /// Creates a new logical plan with the given root operator.
18 pub fn new(root: LogicalOperator) -> Self {
19 Self { root }
20 }
21}
22
23/// A logical operator in the query plan.
24#[derive(Debug, Clone)]
25pub enum LogicalOperator {
26 /// Scan all nodes, optionally filtered by label.
27 NodeScan(NodeScanOp),
28
29 /// Scan all edges, optionally filtered by type.
30 EdgeScan(EdgeScanOp),
31
32 /// Expand from nodes to neighbors via edges.
33 Expand(ExpandOp),
34
35 /// Filter rows based on a predicate.
36 Filter(FilterOp),
37
38 /// Project specific columns.
39 Project(ProjectOp),
40
41 /// Join two inputs.
42 Join(JoinOp),
43
44 /// Aggregate with grouping.
45 Aggregate(AggregateOp),
46
47 /// Limit the number of results.
48 Limit(LimitOp),
49
50 /// Skip a number of results.
51 Skip(SkipOp),
52
53 /// Sort results.
54 Sort(SortOp),
55
56 /// Remove duplicate results.
57 Distinct(DistinctOp),
58
59 /// Create a new node.
60 CreateNode(CreateNodeOp),
61
62 /// Create a new edge.
63 CreateEdge(CreateEdgeOp),
64
65 /// Delete a node.
66 DeleteNode(DeleteNodeOp),
67
68 /// Delete an edge.
69 DeleteEdge(DeleteEdgeOp),
70
71 /// Set properties on a node or edge.
72 SetProperty(SetPropertyOp),
73
74 /// Add labels to a node.
75 AddLabel(AddLabelOp),
76
77 /// Remove labels from a node.
78 RemoveLabel(RemoveLabelOp),
79
80 /// Return results (terminal operator).
81 Return(ReturnOp),
82
83 /// Empty result set.
84 Empty,
85
86 // ==================== RDF/SPARQL Operators ====================
87 /// Scan RDF triples matching a pattern.
88 TripleScan(TripleScanOp),
89
90 /// Union of multiple result sets.
91 Union(UnionOp),
92
93 /// Left outer join for OPTIONAL patterns.
94 LeftJoin(LeftJoinOp),
95
96 /// Anti-join for MINUS patterns.
97 AntiJoin(AntiJoinOp),
98
99 /// Bind a variable to an expression.
100 Bind(BindOp),
101
102 /// Unwind a list into individual rows.
103 Unwind(UnwindOp),
104
105 /// Merge a pattern (match or create).
106 Merge(MergeOp),
107}
108
109/// Scan nodes from the graph.
110#[derive(Debug, Clone)]
111pub struct NodeScanOp {
112 /// Variable name to bind the node to.
113 pub variable: String,
114 /// Optional label filter.
115 pub label: Option<String>,
116 /// Child operator (if any, for chained patterns).
117 pub input: Option<Box<LogicalOperator>>,
118}
119
120/// Scan edges from the graph.
121#[derive(Debug, Clone)]
122pub struct EdgeScanOp {
123 /// Variable name to bind the edge to.
124 pub variable: String,
125 /// Optional edge type filter.
126 pub edge_type: Option<String>,
127 /// Child operator (if any).
128 pub input: Option<Box<LogicalOperator>>,
129}
130
131/// Expand from nodes to their neighbors.
132#[derive(Debug, Clone)]
133pub struct ExpandOp {
134 /// Source node variable.
135 pub from_variable: String,
136 /// Target node variable to bind.
137 pub to_variable: String,
138 /// Edge variable to bind (optional).
139 pub edge_variable: Option<String>,
140 /// Direction of expansion.
141 pub direction: ExpandDirection,
142 /// Optional edge type filter.
143 pub edge_type: Option<String>,
144 /// Minimum hops (for variable-length patterns).
145 pub min_hops: u32,
146 /// Maximum hops (for variable-length patterns).
147 pub max_hops: Option<u32>,
148 /// Input operator.
149 pub input: Box<LogicalOperator>,
150}
151
152/// Direction for edge expansion.
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum ExpandDirection {
155 /// Follow outgoing edges.
156 Outgoing,
157 /// Follow incoming edges.
158 Incoming,
159 /// Follow edges in either direction.
160 Both,
161}
162
163/// Join two inputs.
164#[derive(Debug, Clone)]
165pub struct JoinOp {
166 /// Left input.
167 pub left: Box<LogicalOperator>,
168 /// Right input.
169 pub right: Box<LogicalOperator>,
170 /// Join type.
171 pub join_type: JoinType,
172 /// Join conditions.
173 pub conditions: Vec<JoinCondition>,
174}
175
176/// Join type.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum JoinType {
179 /// Inner join.
180 Inner,
181 /// Left outer join.
182 Left,
183 /// Right outer join.
184 Right,
185 /// Full outer join.
186 Full,
187 /// Cross join (Cartesian product).
188 Cross,
189 /// Semi join (returns left rows with matching right rows).
190 Semi,
191 /// Anti join (returns left rows without matching right rows).
192 Anti,
193}
194
195/// A join condition.
196#[derive(Debug, Clone)]
197pub struct JoinCondition {
198 /// Left expression.
199 pub left: LogicalExpression,
200 /// Right expression.
201 pub right: LogicalExpression,
202}
203
204/// Aggregate with grouping.
205#[derive(Debug, Clone)]
206pub struct AggregateOp {
207 /// Group by expressions.
208 pub group_by: Vec<LogicalExpression>,
209 /// Aggregate functions.
210 pub aggregates: Vec<AggregateExpr>,
211 /// Input operator.
212 pub input: Box<LogicalOperator>,
213}
214
215/// An aggregate expression.
216#[derive(Debug, Clone)]
217pub struct AggregateExpr {
218 /// Aggregate function.
219 pub function: AggregateFunction,
220 /// Expression to aggregate.
221 pub expression: Option<LogicalExpression>,
222 /// Whether to use DISTINCT.
223 pub distinct: bool,
224 /// Alias for the result.
225 pub alias: Option<String>,
226}
227
228/// Aggregate function.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum AggregateFunction {
231 /// Count rows.
232 Count,
233 /// Sum values.
234 Sum,
235 /// Average values.
236 Avg,
237 /// Minimum value.
238 Min,
239 /// Maximum value.
240 Max,
241 /// Collect into list.
242 Collect,
243}
244
245/// Filter rows based on a predicate.
246#[derive(Debug, Clone)]
247pub struct FilterOp {
248 /// The filter predicate.
249 pub predicate: LogicalExpression,
250 /// Input operator.
251 pub input: Box<LogicalOperator>,
252}
253
254/// Project specific columns.
255#[derive(Debug, Clone)]
256pub struct ProjectOp {
257 /// Columns to project.
258 pub projections: Vec<Projection>,
259 /// Input operator.
260 pub input: Box<LogicalOperator>,
261}
262
263/// A single projection (column selection or computation).
264#[derive(Debug, Clone)]
265pub struct Projection {
266 /// Expression to compute.
267 pub expression: LogicalExpression,
268 /// Alias for the result.
269 pub alias: Option<String>,
270}
271
272/// Limit the number of results.
273#[derive(Debug, Clone)]
274pub struct LimitOp {
275 /// Maximum number of rows to return.
276 pub count: usize,
277 /// Input operator.
278 pub input: Box<LogicalOperator>,
279}
280
281/// Skip a number of results.
282#[derive(Debug, Clone)]
283pub struct SkipOp {
284 /// Number of rows to skip.
285 pub count: usize,
286 /// Input operator.
287 pub input: Box<LogicalOperator>,
288}
289
290/// Sort results.
291#[derive(Debug, Clone)]
292pub struct SortOp {
293 /// Sort keys.
294 pub keys: Vec<SortKey>,
295 /// Input operator.
296 pub input: Box<LogicalOperator>,
297}
298
299/// A sort key.
300#[derive(Debug, Clone)]
301pub struct SortKey {
302 /// Expression to sort by.
303 pub expression: LogicalExpression,
304 /// Sort order.
305 pub order: SortOrder,
306}
307
308/// Sort order.
309#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310pub enum SortOrder {
311 /// Ascending order.
312 Ascending,
313 /// Descending order.
314 Descending,
315}
316
317/// Remove duplicate results.
318#[derive(Debug, Clone)]
319pub struct DistinctOp {
320 /// Input operator.
321 pub input: Box<LogicalOperator>,
322}
323
324/// Create a new node.
325#[derive(Debug, Clone)]
326pub struct CreateNodeOp {
327 /// Variable name to bind the created node to.
328 pub variable: String,
329 /// Labels for the new node.
330 pub labels: Vec<String>,
331 /// Properties for the new node.
332 pub properties: Vec<(String, LogicalExpression)>,
333 /// Input operator (for chained creates).
334 pub input: Option<Box<LogicalOperator>>,
335}
336
337/// Create a new edge.
338#[derive(Debug, Clone)]
339pub struct CreateEdgeOp {
340 /// Variable name to bind the created edge to.
341 pub variable: Option<String>,
342 /// Source node variable.
343 pub from_variable: String,
344 /// Target node variable.
345 pub to_variable: String,
346 /// Edge type.
347 pub edge_type: String,
348 /// Properties for the new edge.
349 pub properties: Vec<(String, LogicalExpression)>,
350 /// Input operator.
351 pub input: Box<LogicalOperator>,
352}
353
354/// Delete a node.
355#[derive(Debug, Clone)]
356pub struct DeleteNodeOp {
357 /// Variable of the node to delete.
358 pub variable: String,
359 /// Input operator.
360 pub input: Box<LogicalOperator>,
361}
362
363/// Delete an edge.
364#[derive(Debug, Clone)]
365pub struct DeleteEdgeOp {
366 /// Variable of the edge to delete.
367 pub variable: String,
368 /// Input operator.
369 pub input: Box<LogicalOperator>,
370}
371
372/// Set properties on a node or edge.
373#[derive(Debug, Clone)]
374pub struct SetPropertyOp {
375 /// Variable of the entity to update.
376 pub variable: String,
377 /// Properties to set (name -> expression).
378 pub properties: Vec<(String, LogicalExpression)>,
379 /// Whether to replace all properties (vs. merge).
380 pub replace: bool,
381 /// Input operator.
382 pub input: Box<LogicalOperator>,
383}
384
385/// Add labels to a node.
386#[derive(Debug, Clone)]
387pub struct AddLabelOp {
388 /// Variable of the node to update.
389 pub variable: String,
390 /// Labels to add.
391 pub labels: Vec<String>,
392 /// Input operator.
393 pub input: Box<LogicalOperator>,
394}
395
396/// Remove labels from a node.
397#[derive(Debug, Clone)]
398pub struct RemoveLabelOp {
399 /// Variable of the node to update.
400 pub variable: String,
401 /// Labels to remove.
402 pub labels: Vec<String>,
403 /// Input operator.
404 pub input: Box<LogicalOperator>,
405}
406
407// ==================== RDF/SPARQL Operators ====================
408
409/// Scan RDF triples matching a pattern.
410#[derive(Debug, Clone)]
411pub struct TripleScanOp {
412 /// Subject pattern (variable name or IRI).
413 pub subject: TripleComponent,
414 /// Predicate pattern (variable name or IRI).
415 pub predicate: TripleComponent,
416 /// Object pattern (variable name, IRI, or literal).
417 pub object: TripleComponent,
418 /// Named graph (optional).
419 pub graph: Option<TripleComponent>,
420 /// Input operator (for chained patterns).
421 pub input: Option<Box<LogicalOperator>>,
422}
423
424/// A component of a triple pattern.
425#[derive(Debug, Clone)]
426pub enum TripleComponent {
427 /// A variable to bind.
428 Variable(String),
429 /// A constant IRI.
430 Iri(String),
431 /// A constant literal value.
432 Literal(Value),
433}
434
435/// Union of multiple result sets.
436#[derive(Debug, Clone)]
437pub struct UnionOp {
438 /// Inputs to union together.
439 pub inputs: Vec<LogicalOperator>,
440}
441
442/// Left outer join for OPTIONAL patterns.
443#[derive(Debug, Clone)]
444pub struct LeftJoinOp {
445 /// Left (required) input.
446 pub left: Box<LogicalOperator>,
447 /// Right (optional) input.
448 pub right: Box<LogicalOperator>,
449 /// Optional filter condition.
450 pub condition: Option<LogicalExpression>,
451}
452
453/// Anti-join for MINUS patterns.
454#[derive(Debug, Clone)]
455pub struct AntiJoinOp {
456 /// Left input (results to keep if no match on right).
457 pub left: Box<LogicalOperator>,
458 /// Right input (patterns to exclude).
459 pub right: Box<LogicalOperator>,
460}
461
462/// Bind a variable to an expression.
463#[derive(Debug, Clone)]
464pub struct BindOp {
465 /// Expression to compute.
466 pub expression: LogicalExpression,
467 /// Variable to bind the result to.
468 pub variable: String,
469 /// Input operator.
470 pub input: Box<LogicalOperator>,
471}
472
473/// Unwind a list into individual rows.
474///
475/// For each input row, evaluates the expression (which should return a list)
476/// and emits one row for each element in the list.
477#[derive(Debug, Clone)]
478pub struct UnwindOp {
479 /// The list expression to unwind.
480 pub expression: LogicalExpression,
481 /// The variable name for each element.
482 pub variable: String,
483 /// Input operator.
484 pub input: Box<LogicalOperator>,
485}
486
487/// Merge a pattern (match or create).
488///
489/// MERGE tries to match a pattern in the graph. If found, returns the existing
490/// elements (optionally applying ON MATCH SET). If not found, creates the pattern
491/// (optionally applying ON CREATE SET).
492#[derive(Debug, Clone)]
493pub struct MergeOp {
494 /// The node to merge.
495 pub variable: String,
496 /// Labels to match/create.
497 pub labels: Vec<String>,
498 /// Properties that must match (used for both matching and creation).
499 pub match_properties: Vec<(String, LogicalExpression)>,
500 /// Properties to set on CREATE.
501 pub on_create: Vec<(String, LogicalExpression)>,
502 /// Properties to set on MATCH.
503 pub on_match: Vec<(String, LogicalExpression)>,
504 /// Input operator.
505 pub input: Box<LogicalOperator>,
506}
507
508/// Return results (terminal operator).
509#[derive(Debug, Clone)]
510pub struct ReturnOp {
511 /// Items to return.
512 pub items: Vec<ReturnItem>,
513 /// Whether to return distinct results.
514 pub distinct: bool,
515 /// Input operator.
516 pub input: Box<LogicalOperator>,
517}
518
519/// A single return item.
520#[derive(Debug, Clone)]
521pub struct ReturnItem {
522 /// Expression to return.
523 pub expression: LogicalExpression,
524 /// Alias for the result column.
525 pub alias: Option<String>,
526}
527
528/// A logical expression.
529#[derive(Debug, Clone)]
530pub enum LogicalExpression {
531 /// A literal value.
532 Literal(Value),
533
534 /// A variable reference.
535 Variable(String),
536
537 /// Property access (e.g., n.name).
538 Property {
539 /// The variable to access.
540 variable: String,
541 /// The property name.
542 property: String,
543 },
544
545 /// Binary operation.
546 Binary {
547 /// Left operand.
548 left: Box<LogicalExpression>,
549 /// Operator.
550 op: BinaryOp,
551 /// Right operand.
552 right: Box<LogicalExpression>,
553 },
554
555 /// Unary operation.
556 Unary {
557 /// Operator.
558 op: UnaryOp,
559 /// Operand.
560 operand: Box<LogicalExpression>,
561 },
562
563 /// Function call.
564 FunctionCall {
565 /// Function name.
566 name: String,
567 /// Arguments.
568 args: Vec<LogicalExpression>,
569 },
570
571 /// List literal.
572 List(Vec<LogicalExpression>),
573
574 /// Map literal (e.g., {name: 'Alice', age: 30}).
575 Map(Vec<(String, LogicalExpression)>),
576
577 /// Index access (e.g., `list[0]`).
578 IndexAccess {
579 /// The base expression (typically a list or string).
580 base: Box<LogicalExpression>,
581 /// The index expression.
582 index: Box<LogicalExpression>,
583 },
584
585 /// Slice access (e.g., list[1..3]).
586 SliceAccess {
587 /// The base expression (typically a list or string).
588 base: Box<LogicalExpression>,
589 /// Start index (None means from beginning).
590 start: Option<Box<LogicalExpression>>,
591 /// End index (None means to end).
592 end: Option<Box<LogicalExpression>>,
593 },
594
595 /// CASE expression.
596 Case {
597 /// Test expression (for simple CASE).
598 operand: Option<Box<LogicalExpression>>,
599 /// WHEN clauses.
600 when_clauses: Vec<(LogicalExpression, LogicalExpression)>,
601 /// ELSE clause.
602 else_clause: Option<Box<LogicalExpression>>,
603 },
604
605 /// Parameter reference.
606 Parameter(String),
607
608 /// Labels of a node.
609 Labels(String),
610
611 /// Type of an edge.
612 Type(String),
613
614 /// ID of a node or edge.
615 Id(String),
616
617 /// List comprehension: [x IN list WHERE predicate | expression]
618 ListComprehension {
619 /// Variable name for each element.
620 variable: String,
621 /// The source list expression.
622 list_expr: Box<LogicalExpression>,
623 /// Optional filter predicate.
624 filter_expr: Option<Box<LogicalExpression>>,
625 /// The mapping expression for each element.
626 map_expr: Box<LogicalExpression>,
627 },
628
629 /// EXISTS subquery.
630 ExistsSubquery(Box<LogicalOperator>),
631
632 /// COUNT subquery.
633 CountSubquery(Box<LogicalOperator>),
634}
635
636/// Binary operator.
637#[derive(Debug, Clone, Copy, PartialEq, Eq)]
638pub enum BinaryOp {
639 /// Equality comparison (=).
640 Eq,
641 /// Inequality comparison (<>).
642 Ne,
643 /// Less than (<).
644 Lt,
645 /// Less than or equal (<=).
646 Le,
647 /// Greater than (>).
648 Gt,
649 /// Greater than or equal (>=).
650 Ge,
651
652 /// Logical AND.
653 And,
654 /// Logical OR.
655 Or,
656 /// Logical XOR.
657 Xor,
658
659 /// Addition (+).
660 Add,
661 /// Subtraction (-).
662 Sub,
663 /// Multiplication (*).
664 Mul,
665 /// Division (/).
666 Div,
667 /// Modulo (%).
668 Mod,
669
670 /// String concatenation.
671 Concat,
672 /// String starts with.
673 StartsWith,
674 /// String ends with.
675 EndsWith,
676 /// String contains.
677 Contains,
678
679 /// Collection membership (IN).
680 In,
681 /// Pattern matching (LIKE).
682 Like,
683 /// Regex matching (=~).
684 Regex,
685 /// Power/exponentiation (^).
686 Pow,
687}
688
689/// Unary operator.
690#[derive(Debug, Clone, Copy, PartialEq, Eq)]
691pub enum UnaryOp {
692 /// Logical NOT.
693 Not,
694 /// Numeric negation.
695 Neg,
696 /// IS NULL check.
697 IsNull,
698 /// IS NOT NULL check.
699 IsNotNull,
700}
701
702#[cfg(test)]
703mod tests {
704 use super::*;
705
706 #[test]
707 fn test_simple_node_scan_plan() {
708 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
709 items: vec![ReturnItem {
710 expression: LogicalExpression::Variable("n".into()),
711 alias: None,
712 }],
713 distinct: false,
714 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
715 variable: "n".into(),
716 label: Some("Person".into()),
717 input: None,
718 })),
719 }));
720
721 // Verify structure
722 if let LogicalOperator::Return(ret) = &plan.root {
723 assert_eq!(ret.items.len(), 1);
724 assert!(!ret.distinct);
725 if let LogicalOperator::NodeScan(scan) = ret.input.as_ref() {
726 assert_eq!(scan.variable, "n");
727 assert_eq!(scan.label, Some("Person".into()));
728 } else {
729 panic!("Expected NodeScan");
730 }
731 } else {
732 panic!("Expected Return");
733 }
734 }
735
736 #[test]
737 fn test_filter_plan() {
738 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
739 items: vec![ReturnItem {
740 expression: LogicalExpression::Property {
741 variable: "n".into(),
742 property: "name".into(),
743 },
744 alias: Some("name".into()),
745 }],
746 distinct: false,
747 input: Box::new(LogicalOperator::Filter(FilterOp {
748 predicate: LogicalExpression::Binary {
749 left: Box::new(LogicalExpression::Property {
750 variable: "n".into(),
751 property: "age".into(),
752 }),
753 op: BinaryOp::Gt,
754 right: Box::new(LogicalExpression::Literal(Value::Int64(30))),
755 },
756 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
757 variable: "n".into(),
758 label: Some("Person".into()),
759 input: None,
760 })),
761 })),
762 }));
763
764 if let LogicalOperator::Return(ret) = &plan.root {
765 if let LogicalOperator::Filter(filter) = ret.input.as_ref() {
766 if let LogicalExpression::Binary { op, .. } = &filter.predicate {
767 assert_eq!(*op, BinaryOp::Gt);
768 } else {
769 panic!("Expected Binary expression");
770 }
771 } else {
772 panic!("Expected Filter");
773 }
774 } else {
775 panic!("Expected Return");
776 }
777 }
778}