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