1use crate::join_order::{build_join_tree, build_wcoj_intersect, flatten_join_plan};
10use crate::logical_operator::*;
11use akar_binder::bound_statement::*;
12use akar_common::error::PlannerError;
13use akar_common::types::{Value, extract_f64_list};
14use akar_parser::ast::Expression;
15use std::collections::HashSet;
16
17fn eval_standalone_expr(expr: &Expression) -> Option<Value> {
21 match expr {
22 Expression::Constant(c) => match c {
23 akar_parser::ast::Constant::String(s) => Some(Value::String(s.clone())),
24 akar_parser::ast::Constant::Integer(i) => Some(Value::Int64(*i)),
25 akar_parser::ast::Constant::Float(f) => Some(Value::Double(*f)),
26 akar_parser::ast::Constant::Bool(b) => Some(Value::Bool(*b)),
27 akar_parser::ast::Constant::Null => Some(Value::Null),
28 },
29 Expression::List(items) => Some(Value::List(items.iter().filter_map(eval_standalone_expr).collect())),
30 _ => None,
31 }
32}
33
34pub fn projection_covers_sort_key(projected: &[BoundExpression], key: &Expression) -> bool {
42 for be in projected {
43 if let Some(alias) = &be.alias {
44 if sort_key_matches_name(key, alias) {
45 return true;
46 }
47 }
48 if &be.expression == key {
49 return true;
50 }
51 }
52 if let Expression::PropertyAccess(obj, _) = key {
53 if let Expression::Variable(var) = &**obj {
54 let bare = Expression::Variable(var.clone());
55 if projected.iter().any(|be| be.expression == bare) {
56 return true;
57 }
58 }
59 }
60 false
61}
62
63fn sort_key_matches_name(key: &Expression, name: &str) -> bool {
65 match key {
66 Expression::Variable(v) => v == name,
67 Expression::PropertyAccess(obj, prop) => {
68 if let Expression::Variable(var) = &**obj {
69 format!("{var}.{prop}") == name
70 } else {
71 false
72 }
73 }
74 _ => false,
75 }
76}
77
78fn combine_filter_expr(prev: Option<BoundExpression>, next: BoundExpression) -> BoundExpression {
84 let Some(prev) = prev else { return next };
85 BoundExpression {
86 expression: Expression::BinaryOp(
87 akar_parser::ast::BinaryOp::And,
88 Box::new(prev.expression),
89 Box::new(next.expression),
90 ),
91 resolved_type: akar_common::types::LogicalTypeID::Bool,
92 is_constant: prev.is_constant && next.is_constant,
93 alias: None,
94 }
95}
96
97pub struct QueryPlanner;
99
100impl QueryPlanner {
101 pub fn new() -> Self {
102 Self
103 }
104
105 pub fn plan(&self, statement: BoundStatement) -> Result<Vec<LogicalOperator>, PlannerError> {
106 match statement {
107 BoundStatement::BoundQuery(query) => self.plan_query(query),
108 BoundStatement::BoundCopyFrom(c) => self.plan_copy_from(c),
109 BoundStatement::BoundUnion(u) => self.plan_union(u),
110 BoundStatement::BoundMerge(m) => self.plan_merge(m),
111 BoundStatement::BoundExplain(e) => self.plan_explain(e),
112 BoundStatement::BoundCreateNodeTable(t) => self.plan_create_node_table(t),
113 BoundStatement::BoundCreateRelTable(t) => self.plan_create_rel_table(t),
114 BoundStatement::BoundDropTable(t) => self.plan_drop_table(t),
115 BoundStatement::BoundAlterTable(a) => self.plan_alter_table(a),
116 BoundStatement::BoundCreateIndex(idx) => self.plan_create_index(idx),
117 BoundStatement::BoundDropIndex(idx) => self.plan_drop_index(idx),
118 BoundStatement::BoundCreateVectorIndex(idx) => self.plan_create_vector_index(idx),
119 BoundStatement::BoundCreateSequence(s) => self.plan_create_sequence(s),
120 BoundStatement::BoundDropSequence(s) => self.plan_drop_sequence(s),
121 BoundStatement::BoundCreateDml(c) => self.plan_create_dml(c),
122 BoundStatement::BoundExportDatabase(e) => self.plan_export_database(e),
123 BoundStatement::BoundImportDatabase(i) => self.plan_import_database(i),
124 BoundStatement::BoundCreateFtsIndex(c) => {
125 Ok(vec![LogicalOperator::CreateFtsIndex(LogicalCreateFtsIndex {
126 index_name: c.index_name,
127 table_name: c.table_name,
128 column_name: c.column_name,
129 if_not_exists: c.if_not_exists,
130 docs_table: c.docs_table,
131 terms_table: c.terms_table,
132 posting_table: c.posting_table,
133 cardinality: 1,
134 })])
135 }
136 BoundStatement::BoundStandaloneCall(c) => self.plan_standalone_call(c),
137 _ => Ok(Vec::new()),
138 }
139 }
140
141 fn plan_explain(&self, e: BoundExplain) -> Result<Vec<LogicalOperator>, PlannerError> {
146 let inner_plan = self.plan(*e.inner)?;
147 let inner_op = if inner_plan.is_empty() {
149 return Err("Cannot EXPLAIN an empty plan".into());
150 } else if inner_plan.len() == 1 {
151 inner_plan.into_iter().next().unwrap()
152 } else {
153 LogicalOperator::Projection(LogicalProjection {
155 expressions: Vec::new(),
156 children: inner_plan,
157 cardinality: 0,
158 })
159 };
160
161 Ok(vec![LogicalOperator::Explain(LogicalExplain {
162 inner: Box::new(inner_op),
163 explain_type: e.explain_type,
164 cardinality: 1,
165 })])
166 }
167
168 fn plan_copy_from(&self, c: BoundCopyFrom) -> Result<Vec<LogicalOperator>, PlannerError> {
169 Ok(vec![LogicalOperator::CopyFrom(LogicalCopyFrom {
170 table_name: c.table_name,
171 table_id: c.table_id,
172 file_path: c.file_path,
173 options: c.options,
174 cardinality: 0,
175 })])
176 }
177
178 fn plan_standalone_call(&self, c: BoundStandaloneCall) -> Result<Vec<LogicalOperator>, PlannerError> {
179 if c.function_name.eq_ignore_ascii_case("vector_similarity_scan") {
180 return self.plan_vector_similarity_scan_call(c.args);
181 }
182 Ok(vec![LogicalOperator::StandaloneCall(LogicalStandaloneCall {
183 function_name: c.function_name,
184 args: c.args,
185 cardinality: 1,
186 })])
187 }
188
189 fn plan_vector_similarity_scan_call(&self, args: Vec<Expression>) -> Result<Vec<LogicalOperator>, PlannerError> {
194 if args.len() < 4 {
195 return Err(
196 "vector_similarity_scan requires 4 arguments: table_name, column_name, query_vector, top_k".into(),
197 );
198 }
199 let table_name = match eval_standalone_expr(&args[0]) {
200 Some(Value::String(s)) => s,
201 _ => return Err("First argument to vector_similarity_scan must be a table name string".into()),
202 };
203 let column_name = match eval_standalone_expr(&args[1]) {
204 Some(Value::String(s)) => s,
205 _ => return Err("Second argument to vector_similarity_scan must be a column name string".into()),
206 };
207 let query_vector = match eval_standalone_expr(&args[2]) {
208 Some(v) => extract_f64_list(&v).map_err(|_| {
209 format!("Third argument to vector_similarity_scan must be a list of numbers, got: {v:?}")
210 })?,
211 None => return Err("Third argument to vector_similarity_scan must be a list of numbers".into()),
212 };
213 let top_k = match eval_standalone_expr(&args[3]) {
214 Some(Value::Int64(k)) if k > 0 => k as u64,
215 _ => return Err("Fourth argument to vector_similarity_scan must be a positive integer".into()),
216 };
217 Ok(vec![LogicalOperator::VectorSimilarityScan(
218 LogicalVectorSimilarityScan {
219 table_name,
220 column_name,
221 query_vector,
222 top_k,
223 alias: None,
224 cardinality: top_k,
225 },
226 )])
227 }
228
229 fn plan_create_node_table(&self, t: BoundCreateNodeTable) -> Result<Vec<LogicalOperator>, PlannerError> {
232 Ok(vec![LogicalOperator::CreateNodeTable(LogicalCreateNodeTable {
233 name: t.name,
234 columns: t.columns,
235 primary_key: t.primary_key,
236 if_not_exists: t.if_not_exists,
237 cardinality: 1,
238 })])
239 }
240
241 fn plan_create_rel_table(&self, t: BoundCreateRelTable) -> Result<Vec<LogicalOperator>, PlannerError> {
242 Ok(vec![LogicalOperator::CreateRelTable(LogicalCreateRelTable {
243 name: t.name,
244 from: t.from,
245 to: t.to,
246 columns: t.columns,
247 if_not_exists: t.if_not_exists,
248 cardinality: 1,
249 })])
250 }
251
252 fn plan_drop_table(&self, t: BoundDropTable) -> Result<Vec<LogicalOperator>, PlannerError> {
253 Ok(vec![LogicalOperator::DropTable(LogicalDropTable {
254 name: t.name,
255 cardinality: 1,
256 })])
257 }
258
259 fn plan_alter_table(&self, a: BoundAlterTable) -> Result<Vec<LogicalOperator>, PlannerError> {
260 Ok(vec![LogicalOperator::AlterTable(LogicalAlterTable {
261 table_name: a.table_name,
262 action: a.action,
263 cardinality: 1,
264 })])
265 }
266
267 fn plan_create_index(&self, idx: BoundCreateIndex) -> Result<Vec<LogicalOperator>, PlannerError> {
268 Ok(vec![LogicalOperator::CreateIndex(LogicalCreateIndex {
269 index_type: idx.index_type,
270 index_name: idx.index_name,
271 table_name: idx.table_name,
272 column_name: idx.column_name,
273 cardinality: 1,
274 })])
275 }
276
277 fn plan_drop_index(&self, idx: BoundDropIndex) -> Result<Vec<LogicalOperator>, PlannerError> {
278 Ok(vec![LogicalOperator::DropIndex(LogicalDropIndex {
279 index_name: idx.index_name,
280 table_name: idx.table_name,
281 cardinality: 1,
282 })])
283 }
284
285 fn plan_create_vector_index(&self, idx: BoundCreateVectorIndex) -> Result<Vec<LogicalOperator>, PlannerError> {
286 Ok(vec![LogicalOperator::CreateVectorIndex(LogicalCreateVectorIndex {
287 index_name: idx.index_name,
288 table_name: idx.table_name,
289 column_name: idx.column_name,
290 metric: idx.metric,
291 dimensions: idx.dimensions,
292 cardinality: 1,
293 })])
294 }
295
296 fn plan_create_sequence(&self, s: BoundCreateSequence) -> Result<Vec<LogicalOperator>, PlannerError> {
297 Ok(vec![LogicalOperator::CreateSequence(LogicalCreateSequence {
298 name: s.name,
299 if_not_exists: s.if_not_exists,
300 or_replace: s.or_replace,
301 start_with: s.start_with,
302 increment: s.increment,
303 min_value: s.min_value,
304 max_value: s.max_value,
305 cycle: s.cycle,
306 cardinality: 1,
307 })])
308 }
309
310 fn plan_drop_sequence(&self, s: BoundDropSequence) -> Result<Vec<LogicalOperator>, PlannerError> {
311 Ok(vec![LogicalOperator::DropSequence(LogicalDropSequence {
312 name: s.name,
313 if_exists: s.if_exists,
314 cardinality: 1,
315 })])
316 }
317
318 fn plan_create_dml(&self, c: BoundCreateDml) -> Result<Vec<LogicalOperator>, PlannerError> {
319 let first_node = c.patterns.iter().find_map(|p| p.node.clone());
320 let (table_name, table_id, properties) = match first_node {
321 Some(n) => (n.table_name, n.table_id, n.properties),
322 None => (String::new(), 0, Vec::new()),
323 };
324 Ok(vec![LogicalOperator::CreateDml(LogicalCreateDml {
325 table_name,
326 table_id,
327 properties,
328 cardinality: 1,
329 })])
330 }
331
332 fn plan_export_database(&self, e: BoundExportDatabase) -> Result<Vec<LogicalOperator>, PlannerError> {
333 Ok(vec![LogicalOperator::ExportDatabase(LogicalExportDatabase {
334 file_path: e.file_path,
335 file_type: e.file_type,
336 schema_only: e.schema_only,
337 options: e.options,
338 cardinality: 1,
339 })])
340 }
341
342 fn plan_import_database(&self, i: BoundImportDatabase) -> Result<Vec<LogicalOperator>, PlannerError> {
343 Ok(vec![LogicalOperator::ImportDatabase(LogicalImportDatabase {
344 file_path: i.file_path,
345 query: i.query,
346 index_query: i.index_query,
347 cardinality: 1,
348 })])
349 }
350
351 fn plan_merge(&self, m: BoundMerge) -> Result<Vec<LogicalOperator>, PlannerError> {
356 Ok(self.build_merge_operators(&m))
357 }
358
359 fn build_merge_operators(&self, m: &BoundMerge) -> Vec<LogicalOperator> {
363 let on_match: Vec<LogicalSet> = m
364 .on_match
365 .iter()
366 .map(|item| LogicalSet {
367 table_name: item.table_name.clone(),
368 table_id: item.table_id,
369 is_node: item.is_node,
370 items: vec![SetItem {
371 column_name: item.column_name.clone(),
372 column_idx: item.column_idx,
373 value: item.value.clone(),
374 }],
375 emit_count: false,
376 cardinality: 0,
377 })
378 .collect();
379
380 let on_create: Vec<LogicalSet> = m
381 .on_create
382 .iter()
383 .map(|item| LogicalSet {
384 table_name: item.table_name.clone(),
385 table_id: item.table_id,
386 is_node: item.is_node,
387 items: vec![SetItem {
388 column_name: item.column_name.clone(),
389 column_idx: item.column_idx,
390 value: item.value.clone(),
391 }],
392 emit_count: false,
393 cardinality: 0,
394 })
395 .collect();
396
397 if let Some(edge) = m.patterns.iter().find_map(|p| p.edge.clone()) {
398 return vec![LogicalOperator::MergeRel(LogicalMergeRel {
399 rel_table_name: edge.table_name,
400 rel_table_id: edge.table_id,
401 edge_var: edge.variable.unwrap_or_default(),
402 src_node_var: edge.src_var,
403 dst_node_var: edge.dst_var,
404 properties: edge.properties,
405 on_match,
406 on_create,
407 cardinality: 0,
408 })];
409 }
410
411 vec![LogicalOperator::Merge(LogicalMerge {
412 table_name: m.table_name.clone(),
413 table_id: m.table_id,
414 properties: m.properties.clone(),
415 on_match,
416 on_create,
417 cardinality: 0,
418 })]
419 }
420
421 fn plan_union(&self, u: BoundUnion) -> Result<Vec<LogicalOperator>, PlannerError> {
427 let left_plan = self.plan_query(*u.left)?;
428 let right_plan = self.plan_query(*u.right)?;
429
430 let left_op = if left_plan.len() == 1 {
431 left_plan.into_iter().next().unwrap()
432 } else {
433 LogicalOperator::Projection(LogicalProjection {
435 expressions: Vec::new(),
436 children: left_plan,
437 cardinality: 0,
438 })
439 };
440
441 let right_op = if right_plan.len() == 1 {
442 right_plan.into_iter().next().unwrap()
443 } else {
444 LogicalOperator::Projection(LogicalProjection {
445 expressions: Vec::new(),
446 children: right_plan,
447 cardinality: 0,
448 })
449 };
450
451 Ok(vec![LogicalOperator::Union(LogicalUnion {
452 left: Box::new(left_op),
453 right: Box::new(right_op),
454 all: u.all,
455 cardinality: 0,
456 })])
457 }
458
459 pub fn plan_query(&self, query: BoundQuery) -> Result<Vec<LogicalOperator>, PlannerError> {
460 let mut scan_ops: Vec<LogicalOperator> = Vec::new();
461 let mut filter_expr: Option<BoundExpression> = None;
462 let mut projection: Option<LogicalProjection> = None;
463 let mut distinct = false;
464 let mut delete_exprs: Vec<LogicalOperator> = Vec::new();
465 let mut extend_ops: Vec<LogicalOperator> = Vec::new();
466 let mut order_by: Option<Vec<BoundOrderByItem>> = None;
468 let mut limit: Option<u64> = None;
469 let mut skip: Option<u64> = None;
470 let mut skip_next_node = false;
472 let mut available_vars: HashSet<String> = HashSet::new();
476 let has_return = query.clauses.iter().any(|c| matches!(c, BoundClause::BoundReturn(_)));
481
482 for clause in query.clauses {
483 match clause {
484 BoundClause::BoundMatch(mut m) => {
485 let mut fts_to_assign = m.fts_query.as_ref().map(|fq| LogicalFtsScan {
486 index_name: fq.index_name.clone(),
487 query_string: fq.query_string.clone(),
488 docs_table: fq.docs_table.clone(),
489 terms_table: fq.terms_table.clone(),
490 posting_table: fq.posting_table.clone(),
491 table_name: fq.table_name.clone(),
492 column_name: fq.column_name.clone(),
493 cardinality: 0,
494 });
495 let patterns: Vec<BoundPattern> = std::mem::take(&mut m.patterns);
496
497 if let Some((wcoj_op, wcoj_trailing)) = build_wcoj_intersect(&patterns) {
501 scan_ops.push(wcoj_op);
502 extend_ops.extend(wcoj_trailing);
503 } else {
504 let mut patterns_iter = patterns.into_iter().peekable();
505 let _skip_next_node = false;
506 while let Some(pattern) = patterns_iter.next() {
507 let skip_current_node_scan = skip_next_node;
509 skip_next_node = false;
510
511 if let Some(ref edge) = pattern.edge {
513 let is_var_length = edge.lower_bound.is_some() || edge.upper_bound.is_some();
514 if is_var_length {
515 let lb = edge.lower_bound.unwrap_or(0);
516 let ub = edge.upper_bound.unwrap_or(1);
517 let direction = match edge.direction {
518 akar_parser::ast::EdgeDirection::LeftToRight => {
519 akar_common::enums::ExtendDirection::Fwd
520 }
521 akar_parser::ast::EdgeDirection::RightToLeft => {
522 akar_common::enums::ExtendDirection::Bwd
523 }
524 akar_parser::ast::EdgeDirection::Both => {
525 akar_common::enums::ExtendDirection::Both
526 }
527 };
528
529 let node_var = &pattern.node_variable;
531 let var_len_src_bound =
532 node_var.as_ref().is_some_and(|v| available_vars.contains(v));
533 if !skip_current_node_scan && !var_len_src_bound {
534 if let Some(label) = pattern.node_label {
535 scan_ops.push(LogicalOperator::ScanNode(LogicalScanNode {
536 table_name: label,
537 table_id: pattern.node_table_id.unwrap_or(0),
538 alias: node_var.clone(),
539 columns: Vec::new(),
540 cardinality: 0,
541 fts_query: fts_to_assign.take(),
542 predicate: None,
543 }));
544 if let Some(v) = node_var {
545 available_vars.insert(v.clone());
546 }
547 }
548 }
549
550 let rel_table_ids = edge.rel_table_id.map_or(vec![], |id| vec![id]);
552 let rel_labels = edge.label.as_ref().map_or(vec![], |l| vec![l.clone()]);
553 let target_var = patterns_iter
554 .peek()
555 .and_then(|p| p.node_variable.clone())
556 .unwrap_or_default();
557 scan_ops.push(LogicalOperator::RecursiveExtend(LogicalRecursiveExtend {
558 source_var: node_var.clone().unwrap_or_default(),
559 source_table_id: pattern.node_table_id.unwrap_or(0),
560 edge_var: edge.variable.clone(),
561 target_var: target_var.clone(),
562 rel_table_ids,
563 rel_labels,
564 lower_bound: lb,
565 upper_bound: ub,
566 direction,
567 semantic: akar_common::enums::PathSemantic::Walk,
568 weight_property: None,
569 cost_output_name: None,
570 cardinality: 0,
571 }));
572 if !target_var.is_empty() {
574 available_vars.insert(target_var);
575 }
576 skip_next_node = true;
578 continue;
579 }
580
581 let src_node_var = pattern.node_variable.clone();
584 let src_already_bound =
585 src_node_var.as_ref().is_some_and(|v| available_vars.contains(v));
586 if !skip_current_node_scan && !src_already_bound {
587 if let Some(label) = &pattern.node_label {
588 scan_ops.push(LogicalOperator::ScanNode(LogicalScanNode {
589 table_name: label.clone(),
590 table_id: pattern.node_table_id.unwrap_or(0),
591 alias: src_node_var.clone(),
592 columns: Vec::new(),
593 cardinality: 0,
594 fts_query: fts_to_assign.take(),
595 predicate: None,
596 }));
597 if let Some(v) = &src_node_var {
598 available_vars.insert(v.clone());
599 }
600 }
601 }
602
603 if let Some(rel_label) = &edge.label {
605 let dest_pattern = patterns_iter.peek();
606 let dst_var =
607 dest_pattern.and_then(|p| p.node_variable.clone()).unwrap_or_default();
608 let dst_table_name =
609 dest_pattern.and_then(|p| p.node_label.clone()).unwrap_or_default();
610 let dst_table_id = dest_pattern.and_then(|p| p.node_table_id).unwrap_or(0);
611
612 extend_ops.push(LogicalOperator::Extend(LogicalExtend {
613 rel_table_name: rel_label.clone(),
614 rel_table_id: edge.rel_table_id.unwrap_or(0),
615 rel_var: edge.variable.clone().unwrap_or_default(),
616 bound_node_var: src_node_var.unwrap_or_default(),
617 direction: edge.direction.clone(),
618 dst_node_var: dst_var.clone(),
619 dst_table_name,
620 dst_table_id,
621 cardinality: 0,
622 }));
623 if !dst_var.is_empty() {
626 available_vars.insert(dst_var);
627 }
628
629 skip_next_node = true;
631 continue;
632 }
633 }
634
635 if !skip_current_node_scan {
637 if let Some(label) = pattern.node_label {
638 let var = pattern.node_variable.clone();
639 if !var.as_ref().is_some_and(|v| available_vars.contains(v)) {
640 scan_ops.push(LogicalOperator::ScanNode(LogicalScanNode {
641 table_name: label,
642 table_id: pattern.node_table_id.unwrap_or(0),
643 alias: var.clone(),
644 columns: Vec::new(),
645 cardinality: 0,
646 fts_query: fts_to_assign.take(),
647 predicate: None,
648 }));
649 if let Some(v) = var {
650 available_vars.insert(v);
651 }
652 }
653 }
654 }
655 }
656 } }
658
659 BoundClause::BoundWhere(w) => {
660 if !delete_exprs.is_empty() {
661 delete_exprs.push(LogicalOperator::Filter(LogicalFilter {
665 expression: w.expression.expression,
666 children: Vec::new(),
667 cardinality: 0,
668 }));
669 } else {
670 filter_expr = Some(combine_filter_expr(filter_expr.take(), w.expression));
673 }
674 }
675 BoundClause::BoundReturn(r) => {
676 distinct = r.distinct;
677 order_by = r.order_by;
678 limit = r.limit;
679 skip = r.skip;
680 projection = Some(LogicalProjection {
681 expressions: r.expressions,
682 children: Vec::new(),
683 cardinality: 0,
684 });
685 }
686 BoundClause::BoundWith(r) => {
687 delete_exprs.push(LogicalOperator::Projection(LogicalProjection {
688 expressions: r.expressions,
689 children: Vec::new(),
690 cardinality: 0,
691 }));
692 }
693 BoundClause::BoundOptionalMatch(om) => {
694 let probe_edge_shape = om.patterns.len() == 2
704 && om.patterns[0].edge.as_ref().is_some_and(|e| {
705 e.variable.is_some()
706 && e.label.as_ref().is_some_and(|l| !l.is_empty())
707 && e.properties.is_empty()
708 && e.lower_bound.is_none()
709 && e.upper_bound.is_none()
710 })
711 && om.patterns[1].edge.is_none();
712
713 let both_endpoints_bound = probe_edge_shape
714 && om.patterns.iter().all(|p| {
715 p.node_label.is_none()
716 && p.properties.is_empty()
717 && p.node_variable.as_ref().is_some_and(|v| available_vars.contains(v))
718 });
719
720 let anon_dst_fanout = probe_edge_shape
730 && om.patterns[0].node_label.is_none()
731 && om.patterns[0].properties.is_empty()
732 && om.patterns[0]
733 .node_variable
734 .as_ref()
735 .is_some_and(|v| available_vars.contains(v))
736 && om.patterns[1].node_variable.is_none()
737 && om.patterns[1].properties.is_empty()
738 && om.patterns[1].node_label.is_some();
739
740 let edge_probe = both_endpoints_bound || anon_dst_fanout;
741
742 let mut left_pipeline: Vec<LogicalOperator> = Vec::new();
744 if !scan_ops.is_empty() {
745 if scan_ops.len() == 1 {
746 left_pipeline.push(scan_ops.into_iter().next().unwrap());
747 } else {
748 let join_plan = build_join_tree(scan_ops, filter_expr.as_ref());
749 let flattened = flatten_join_plan(&join_plan);
750 left_pipeline.extend(flattened);
751 }
752 }
753 if let Some(expr) = filter_expr.take() {
754 left_pipeline.push(LogicalOperator::Filter(LogicalFilter {
755 expression: expr.expression,
756 children: Vec::new(),
757 cardinality: 0,
758 }));
759 }
760 if let Some(proj) = projection.take() {
761 left_pipeline.push(LogicalOperator::Projection(proj));
762 }
763 if edge_probe {
764 let edge = om.patterns[0].edge.as_ref().unwrap();
765 let src_var = om.patterns[0].node_variable.clone().unwrap();
766 let dst_var = om.patterns[1].node_variable.clone().unwrap_or_default();
770 delete_exprs.push(LogicalOperator::OptionalExtend(LogicalOptionalExtend {
771 children: left_pipeline,
772 rel_table_name: edge.label.clone().unwrap(),
773 rel_table_id: edge.rel_table_id.unwrap_or(0),
774 rel_var: edge.variable.clone().unwrap(),
775 src_node_var: src_var,
776 dst_node_var: dst_var,
777 direction: edge.direction.clone(),
778 cardinality: 0,
779 }));
780 } else {
781 let left_op = if left_pipeline.len() == 1 {
782 left_pipeline.into_iter().next().unwrap()
783 } else if left_pipeline.is_empty() {
784 LogicalOperator::ScanNode(LogicalScanNode {
786 table_name: String::new(),
787 table_id: 0,
788 alias: None,
789 columns: Vec::new(),
790 cardinality: 0,
791 fts_query: None,
792 predicate: None,
793 })
794 } else {
795 LogicalOperator::Projection(LogicalProjection {
796 expressions: Vec::new(),
797 children: left_pipeline,
798 cardinality: 0,
799 })
800 };
801
802 let mut right_ops: Vec<LogicalOperator> = Vec::new();
804 for pattern in &om.patterns {
805 if let Some(label) = &pattern.node_label {
806 right_ops.push(LogicalOperator::ScanNode(LogicalScanNode {
807 table_name: label.clone(),
808 table_id: pattern.node_table_id.unwrap_or(0),
809 alias: pattern.node_variable.clone(),
810 columns: Vec::new(),
811 cardinality: 0,
812 fts_query: None,
813 predicate: None,
814 }));
815 }
816 if let Some(edge) = &pattern.edge
817 && let Some(rel_label) = &edge.label
818 {
819 right_ops.push(LogicalOperator::ScanRel(LogicalScanRel {
820 table_name: rel_label.clone(),
821 table_id: edge.rel_table_id.unwrap_or(0),
822 direction: edge.direction.clone(),
823 cardinality: 0,
824 }));
825 }
826 }
827 let mut inline_exprs: Vec<Expression> = Vec::new();
833 for pattern in &om.patterns {
834 if let Some(node_var) = &pattern.node_variable {
835 for (key, val_expr) in &pattern.properties {
836 inline_exprs.push(Expression::BinaryOp(
837 akar_parser::ast::BinaryOp::Equal,
838 Box::new(Expression::PropertyAccess(
839 Box::new(Expression::Variable(node_var.clone())),
840 key.clone(),
841 )),
842 Box::new(val_expr.clone()),
843 ));
844 }
845 }
846 if let Some(edge) = &pattern.edge
847 && let Some(edge_var) = &edge.variable
848 {
849 for (key, val_expr) in &edge.properties {
850 inline_exprs.push(Expression::BinaryOp(
851 akar_parser::ast::BinaryOp::Equal,
852 Box::new(Expression::PropertyAccess(
853 Box::new(Expression::Variable(edge_var.clone())),
854 key.clone(),
855 )),
856 Box::new(val_expr.clone()),
857 ));
858 }
859 }
860 }
861 if !inline_exprs.is_empty() {
862 let combined = inline_exprs.into_iter().reduce(|acc, e| {
863 Expression::BinaryOp(akar_parser::ast::BinaryOp::And, Box::new(acc), Box::new(e))
864 });
865 right_ops.push(LogicalOperator::Filter(LogicalFilter {
866 expression: combined.unwrap(),
867 children: Vec::new(),
868 cardinality: 0,
869 }));
870 }
871 let right_op = if right_ops.len() == 1 {
872 right_ops.into_iter().next().unwrap()
873 } else if right_ops.is_empty() {
874 LogicalOperator::ScanNode(LogicalScanNode {
875 table_name: String::new(),
876 table_id: 0,
877 alias: None,
878 columns: Vec::new(),
879 cardinality: 0,
880 fts_query: None,
881 predicate: None,
882 })
883 } else {
884 LogicalOperator::Projection(LogicalProjection {
885 expressions: Vec::new(),
886 children: right_ops,
887 cardinality: 0,
888 })
889 };
890
891 delete_exprs.push(LogicalOperator::OptionalMatch(LogicalOptionalMatch {
896 left: Box::new(left_op),
897 right: Box::new(right_op),
898 cardinality: 0,
899 }));
900 }
901 scan_ops = Vec::new();
903 filter_expr = None;
904 projection = None;
905 distinct = false;
906 }
907 BoundClause::BoundDelete(d) => {
908 for item in &d.items {
909 delete_exprs.push(LogicalOperator::Delete(LogicalDelete {
910 table_name: item.table_name.clone(),
911 table_id: item.table_id,
912 primary_key_column: item.primary_key_column.clone(),
913 is_node: item.is_node,
914 detach: d.detach,
915 cardinality: 0,
916 }));
917 }
918 }
919 BoundClause::BoundUnwind(u) => {
920 scan_ops.push(LogicalOperator::Unwind(LogicalUnwind {
927 expression: u.expression.clone(),
928 variable: u.variable.clone(),
929 cardinality: 0,
930 }));
931 if !u.variable.is_empty() {
932 available_vars.insert(u.variable.clone());
933 }
934 }
935 BoundClause::BoundSet(s) => {
936 let mut groups: Vec<(String, u64, bool, Vec<SetItem>)> = Vec::new();
943 for item in &s.items {
944 let key = (item.table_name.clone(), item.table_id, item.is_node);
945 match groups
946 .iter_mut()
947 .find(|(n, id, n2, _)| *n == key.0 && *id == key.1 && *n2 == key.2)
948 {
949 Some((_, _, _, items)) => items.push(SetItem {
950 column_name: item.column_name.clone(),
951 column_idx: item.column_idx,
952 value: item.value.clone(),
953 }),
954 None => groups.push((
955 key.0,
956 key.1,
957 key.2,
958 vec![SetItem {
959 column_name: item.column_name.clone(),
960 column_idx: item.column_idx,
961 value: item.value.clone(),
962 }],
963 )),
964 }
965 }
966 for (table_name, table_id, is_node, items) in groups {
967 delete_exprs.push(LogicalOperator::Set(LogicalSet {
968 table_name,
969 table_id,
970 is_node,
971 items,
972 emit_count: !has_return,
973 cardinality: 0,
974 }));
975 }
976 }
977 BoundClause::BoundCreate(c) => {
978 let mut patterns_iter = c.patterns.into_iter().peekable();
979 while let Some(pattern) = patterns_iter.next() {
980 let node_var = pattern.node_variable.clone().unwrap_or_default();
981
982 if c.new_variables.iter().any(|v| v.name == node_var) {
983 delete_exprs.push(LogicalOperator::CreateNode(LogicalCreateNode {
984 table_name: pattern.node_label.clone().unwrap_or_default(),
985 table_id: pattern.node_table_id.unwrap_or(0),
986 out_var_name: node_var.clone(),
987 properties: pattern.properties.clone(),
988 cardinality: 0,
989 }));
990 }
991
992 if let Some(edge) = pattern.edge {
993 let dest_var = patterns_iter
994 .peek()
995 .and_then(|p| p.node_variable.clone())
996 .unwrap_or_default();
997 let (src_node_name, dst_node_name) = match edge.direction {
998 akar_parser::ast::EdgeDirection::RightToLeft => (dest_var, node_var.clone()),
999 _ => (node_var.clone(), dest_var),
1000 };
1001
1002 delete_exprs.push(LogicalOperator::CreateRel(LogicalCreateRel {
1003 table_name: edge.label.clone().unwrap_or_default(),
1004 table_id: edge.rel_table_id.unwrap_or(0),
1005 src_node_name,
1006 dst_node_name,
1007 out_var_name: edge.variable.clone().unwrap_or_default(),
1008 properties: edge.properties.clone(),
1009 cardinality: 0,
1010 }));
1011 }
1012 }
1013 }
1014 BoundClause::BoundForeach(f) => {
1015 let mut sub_plans = Vec::new();
1017 for sub_stmt in &f.sub_statements {
1018 let plan = self.plan(sub_stmt.clone())?;
1019 sub_plans.push(plan);
1020 }
1021 delete_exprs.push(LogicalOperator::Foreach(LogicalForeach {
1022 variable: f.variable.clone(),
1023 expression: f.expression.clone(),
1024 sub_plans,
1025 cardinality: 0,
1026 }));
1027 }
1028 BoundClause::BoundMerge(m) => {
1029 delete_exprs.extend(self.build_merge_operators(&m));
1030 }
1031 }
1032 }
1033
1034 let delete_ops: Vec<LogicalOperator> = std::mem::take(&mut delete_exprs);
1036
1037 let mut result: Vec<LogicalOperator> = Vec::new();
1039
1040 if scan_ops.is_empty() {
1041 result.extend(delete_ops);
1046 append_return_tail(&mut result, projection, order_by, distinct, limit, skip);
1047 return Ok(result);
1048 }
1049
1050 if scan_ops.len() == 1 {
1051 result.push(scan_ops.into_iter().next().unwrap());
1053 } else {
1054 let join_plan = build_join_tree(scan_ops, filter_expr.as_ref());
1056 let flattened = flatten_join_plan(&join_plan);
1057 result.extend(flattened);
1058 }
1059
1060 result.extend(std::mem::take(&mut extend_ops));
1062
1063 if let Some(expr) = filter_expr {
1065 result.push(LogicalOperator::Filter(LogicalFilter {
1066 expression: expr.expression,
1067 children: Vec::new(),
1068 cardinality: 0,
1069 }));
1070 }
1071
1072 result.extend(delete_ops);
1076
1077 append_return_tail(&mut result, projection, order_by, distinct, limit, skip);
1079
1080 Ok(result)
1081 }
1082}
1083
1084fn append_return_tail(
1093 result: &mut Vec<LogicalOperator>,
1094 projection: Option<LogicalProjection>,
1095 mut order_by: Option<Vec<BoundOrderByItem>>,
1096 distinct: bool,
1097 limit: Option<u64>,
1098 skip: Option<u64>,
1099) {
1100 if let Some(proj) = projection {
1101 let group_by = if distinct {
1102 Some(
1103 proj.expressions
1104 .iter()
1105 .map(|be| be.expression.clone())
1106 .collect::<Vec<Expression>>(),
1107 )
1108 } else {
1109 None
1110 };
1111
1112 let order_by_below_projection = match &order_by {
1120 Some(items) => {
1121 group_by.is_none()
1122 && !items
1123 .iter()
1124 .all(|item| projection_covers_sort_key(&proj.expressions, &item.expression.expression))
1125 }
1126 None => false,
1127 };
1128
1129 if order_by_below_projection {
1130 if let Some(items) = order_by.take() {
1131 let sort_keys: Vec<(Expression, bool)> = items
1132 .iter()
1133 .map(|item| (item.expression.expression.clone(), item.ascending))
1134 .collect();
1135 result.push(LogicalOperator::OrderBy(LogicalOrderBy {
1136 sort_keys,
1137 children: Vec::new(),
1138 cardinality: 0,
1139 }));
1140 }
1141 }
1142
1143 result.push(LogicalOperator::Projection(proj));
1144 if let Some(gb) = group_by {
1146 result.push(LogicalOperator::Aggregate(LogicalAggregate {
1147 group_by: gb,
1148 aggregates: Vec::new(),
1149 children: Vec::new(),
1150 cardinality: 0,
1151 }));
1152 }
1153 }
1154
1155 if let Some(items) = order_by {
1157 let sort_keys: Vec<(Expression, bool)> = items
1158 .iter()
1159 .map(|item| (item.expression.expression.clone(), item.ascending))
1160 .collect();
1161 result.push(LogicalOperator::OrderBy(LogicalOrderBy {
1162 sort_keys,
1163 children: Vec::new(),
1164 cardinality: 0,
1165 }));
1166 }
1167
1168 if limit.is_some() || skip.is_some() {
1170 result.push(LogicalOperator::Limit(LogicalLimit {
1171 limit: limit.unwrap_or(u64::MAX),
1172 offset: skip.unwrap_or(0),
1173 children: Vec::new(),
1174 cardinality: 0,
1175 }));
1176 }
1177}
1178
1179impl Default for QueryPlanner {
1180 fn default() -> Self {
1181 Self::new()
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use super::*;
1188 use akar_binder::Binder;
1189 use akar_catalog::{Catalog, CatalogColumn};
1190 use akar_common::types::LogicalTypeID;
1191 use akar_parser::parse;
1192 use std::sync::Arc;
1193
1194 fn setup_binder() -> Binder {
1195 let mut catalog = Catalog::new();
1196 catalog.create_node_table(
1197 "Person".into(),
1198 vec![
1199 CatalogColumn {
1200 compression: akar_common::enums::CompressionType::Uncompressed,
1201 name: "name".into(),
1202 logical_type: LogicalTypeID::String,
1203 is_primary_key: true,
1204 default_value: None,
1205 },
1206 CatalogColumn {
1207 compression: akar_common::enums::CompressionType::Uncompressed,
1208 name: "age".into(),
1209 logical_type: LogicalTypeID::Int64,
1210 is_primary_key: false,
1211 default_value: None,
1212 },
1213 ],
1214 );
1215 catalog.create_rel_table(
1216 "Knows".into(),
1217 0,
1218 0,
1219 vec![CatalogColumn {
1220 compression: akar_common::enums::CompressionType::Uncompressed,
1221 name: "since".into(),
1222 logical_type: LogicalTypeID::Int64,
1223 is_primary_key: false,
1224 default_value: None,
1225 }],
1226 );
1227 Binder::new(Arc::new(std::sync::Mutex::new(catalog)))
1228 }
1229
1230 #[test]
1231 fn test_plan_match_return() {
1232 let binder = setup_binder();
1233 let sql = "MATCH (a:Person) RETURN a.name";
1234 let stmt = parse(sql).unwrap();
1235 let bound = binder.bind(stmt).unwrap();
1236 let planner = QueryPlanner::new();
1237 let plan = planner.plan(bound).unwrap();
1238 assert!(!plan.is_empty());
1239
1240 let scan_count = plan
1242 .iter()
1243 .filter(|op| matches!(op, LogicalOperator::ScanNode(_)))
1244 .count();
1245 let proj_count = plan
1246 .iter()
1247 .filter(|op| matches!(op, LogicalOperator::Projection(_)))
1248 .count();
1249 assert_eq!(scan_count, 1);
1250 assert_eq!(proj_count, 1);
1251 }
1252
1253 #[test]
1254 fn test_plan_optional_match_keeps_order_by_and_limit() {
1255 let binder = setup_binder();
1259 let sql = "MATCH (a:Person) OPTIONAL MATCH (a)-[r:Knows]-(:Person) \
1260 RETURN a.name AS id, COUNT(r) AS c ORDER BY id LIMIT 5";
1261 let stmt = parse(sql).unwrap();
1262 let bound = binder.bind(stmt).unwrap();
1263 let planner = QueryPlanner::new();
1264 let plan = planner.plan(bound).unwrap();
1265 assert!(
1266 plan.iter().any(|op| matches!(op, LogicalOperator::OrderBy(_))),
1267 "planner must emit OrderBy above the RETURN projection, got: {plan:?}"
1268 );
1269 assert!(
1270 plan.iter().any(|op| matches!(op, LogicalOperator::Limit(_))),
1271 "planner must emit Limit above the RETURN projection, got: {plan:?}"
1272 );
1273 }
1274
1275 #[test]
1276 fn test_plan_return_only_projection() {
1277 let binder = setup_binder();
1278 let sql = "RETURN 1";
1279 let stmt = parse(sql).unwrap();
1280 let bound = binder.bind(stmt).unwrap();
1281 let planner = QueryPlanner::new();
1282 let plan = planner.plan(bound).unwrap();
1283
1284 assert_eq!(plan.len(), 1);
1285 assert!(matches!(plan[0], LogicalOperator::Projection(_)));
1286 }
1287
1288 #[test]
1289 fn test_plan_unwind_then_projection_without_scan() {
1290 let binder = setup_binder();
1291 let sql = "UNWIND [1, 2, 3] AS x RETURN x";
1292 let stmt = parse(sql).unwrap();
1293 let bound = binder.bind(stmt).unwrap();
1294 let planner = QueryPlanner::new();
1295 let plan = planner.plan(bound).unwrap();
1296
1297 assert_eq!(plan.len(), 2);
1298 assert!(matches!(plan[0], LogicalOperator::Unwind(_)));
1299 assert!(matches!(plan[1], LogicalOperator::Projection(_)));
1300 }
1301
1302 #[test]
1303 fn test_plan_match_where_return() {
1304 let binder = setup_binder();
1305 let sql = "MATCH (a:Person) WHERE a.age > 25 RETURN a.name";
1306 let stmt = parse(sql).unwrap();
1307 let bound = binder.bind(stmt).unwrap();
1308 let planner = QueryPlanner::new();
1309 let plan = planner.plan(bound).unwrap();
1310 assert!(!plan.is_empty());
1311
1312 let scan_count = plan
1313 .iter()
1314 .filter(|op| matches!(op, LogicalOperator::ScanNode(_)))
1315 .count();
1316 let filter_count = plan
1317 .iter()
1318 .filter(|op| matches!(op, LogicalOperator::Filter(_)))
1319 .count();
1320 let proj_count = plan
1321 .iter()
1322 .filter(|op| matches!(op, LogicalOperator::Projection(_)))
1323 .count();
1324 assert_eq!(scan_count, 1);
1325 assert_eq!(filter_count, 1);
1326 assert_eq!(proj_count, 1);
1327 }
1328
1329 #[test]
1330 fn test_plan_ddl_empty() {
1331 let binder = Binder::new(Arc::new(std::sync::Mutex::new(Catalog::new())));
1332 let sql = "CREATE NODE TABLE City(name STRING, PRIMARY KEY (name))";
1333 let stmt = parse(sql).unwrap();
1334 let bound = binder.bind(stmt).unwrap();
1335 let planner = QueryPlanner::new();
1336 let plan = planner.plan(bound).unwrap();
1337 assert!(!plan.is_empty()); match &plan[0] {
1339 LogicalOperator::CreateNodeTable(ct) => {
1340 assert_eq!(ct.name, "City");
1341 }
1342 _ => panic!("Expected CreateNodeTable"),
1343 }
1344 }
1345
1346 #[test]
1347 fn test_plan_scan_node_fields() {
1348 let binder = setup_binder();
1349 let sql = "MATCH (a:Person) RETURN a";
1350 let stmt = parse(sql).unwrap();
1351 let bound = binder.bind(stmt).unwrap();
1352 let planner = QueryPlanner::new();
1353 let plan = planner.plan(bound).unwrap();
1354
1355 match &plan[0] {
1356 LogicalOperator::ScanNode(s) => {
1357 assert_eq!(s.table_name, "Person");
1358 assert_eq!(s.alias, Some("a".into()));
1359 }
1360 _ => panic!("Expected ScanNode"),
1361 }
1362 }
1363
1364 #[test]
1365 fn test_plan_rel_pattern() {
1366 let binder = setup_binder();
1367 let sql = "MATCH (a:Person)-[r:Knows]->(b:Person) RETURN a, b";
1368 let stmt = parse(sql).unwrap();
1369 let bound = binder.bind(stmt).unwrap();
1370 let planner = QueryPlanner::new();
1371 let plan = planner.plan(bound).unwrap();
1372 assert!(plan.iter().any(|op| matches!(op, LogicalOperator::Extend(_))));
1374 assert!(plan.iter().any(|op| matches!(op, LogicalOperator::ScanNode(_))));
1376 assert!(!plan.iter().any(|op| matches!(op, LogicalOperator::ScanRel(_))));
1378 }
1379
1380 #[test]
1381 fn test_plan_order() {
1382 let binder = setup_binder();
1383 let sql = "MATCH (a:Person) WHERE a.age > 25 RETURN a.name";
1384 let stmt = parse(sql).unwrap();
1385 let bound = binder.bind(stmt).unwrap();
1386 let planner = QueryPlanner::new();
1387 let plan = planner.plan(bound).unwrap();
1388
1389 let positions: Vec<&str> = plan
1391 .iter()
1392 .map(|op| match op {
1393 LogicalOperator::ScanNode(_) => "scan",
1394 LogicalOperator::Filter(_) => "filter",
1395 LogicalOperator::Projection(_) => "proj",
1396 _ => "other",
1397 })
1398 .collect();
1399
1400 let scan_pos = positions.iter().position(|&p| p == "scan").unwrap();
1401 let filter_pos = positions.iter().position(|&p| p == "filter").unwrap();
1402 let proj_pos = positions.iter().position(|&p| p == "proj").unwrap();
1403
1404 assert!(scan_pos < filter_pos);
1405 assert!(filter_pos < proj_pos);
1406 }
1407}