1use super::{
16 access_path::index_seek_from_filter,
17 costing::{
18 estimate_node_cartesian_product_cost, estimate_physical_plan_cost,
19 push_optional_relationship_count_sum_cost_decision,
20 },
21 logical_rewrite::{rewrite_logical_plan, LogicalRewriteOutput},
22 properties::satisfies_ordering,
23 selected_trace::selected_plan_trace,
24 stages::{
25 ACCESS_PATH_SELECTION_STAGE, DIRECT_PHYSICAL_FALLBACK_STAGE, LOGICAL_GROUPING_STAGE,
26 PHYSICAL_SEARCH_STAGE, PLAN_FINALIZATION_STAGE, SELECTED_PLAN_COSTING_STAGE,
27 },
28 LogicalPlanRoot, LoweringReadyLogicalPlanRoot, OptimizationSearchReport, OptimizerCatalog,
29 OptimizerConfig, OptimizerTrace, PhysicalPlan, PhysicalPlanRoot, StageStats,
30};
31use crate::{
32 GroupId, Memo, OptimizerContext, OptimizerSearchDirective, OptimizerSearchDirectiveError,
33 RuleEvent, RuleOutcome, SelectedPlanTrace, StageTrace,
34};
35use hawdb_core::Value;
36use hawdb_cypher::RelationshipDirection;
37use hawdb_plan_cypher::{
38 AggregateFunction, AggregateTarget, GraphExpansionBudget, LogicalPlan, NodeProjectionAccess,
39 Predicate, Projection, ProjectionExpression, SortItem, SortKey,
40};
41use std::collections::{BTreeMap, BTreeSet};
42
43mod access;
44mod ddl;
45mod join;
46mod mutation;
47mod procedure;
48mod simple;
49mod traversal;
50
51#[cfg(test)]
52mod tests;
53
54type GraphMemo = Memo<GroupExpr>;
55
56#[derive(Debug)]
57struct GroupExpr {
58 logical: LogicalPlan,
59 children: Vec<GroupId>,
60}
61
62#[derive(Debug, Default, Clone)]
63pub struct CascadesOptimizer {
64 context: OptimizerContext,
65}
66
67impl CascadesOptimizer {
68 pub fn new(config: OptimizerConfig) -> Self {
69 Self {
70 context: OptimizerContext::from_config(config),
71 }
72 }
73
74 pub fn with_context(context: OptimizerContext) -> Self {
75 Self { context }
76 }
77
78 pub fn context(&self) -> &OptimizerContext {
79 &self.context
80 }
81
82 pub fn refresh_trace_for_physical_plan(
83 &self,
84 trace: &mut OptimizerTrace,
85 plan: &PhysicalPlan,
86 catalog: &OptimizerCatalog,
87 ) {
88 refresh_selected_plan_trace(trace, selected_plan_trace(plan, catalog, &self.context));
89 }
90
91 pub fn optimize(&self, logical: &LogicalPlan) -> PhysicalPlan {
92 self.optimize_root(&LogicalPlanRoot::new(logical.clone()))
93 .into_parts()
94 .0
95 }
96
97 pub fn optimize_with_trace(&self, logical: &LogicalPlan) -> (PhysicalPlan, OptimizerTrace) {
98 self.optimize_root(&LogicalPlanRoot::new(logical.clone()))
99 .into_parts()
100 }
101
102 pub fn optimize_with_catalog(
103 &self,
104 logical: &LogicalPlan,
105 catalog: &OptimizerCatalog,
106 ) -> (PhysicalPlan, OptimizerTrace) {
107 self.optimize_root_with_catalog(&LogicalPlanRoot::new(logical.clone()), catalog)
108 .into_parts()
109 }
110
111 pub fn optimize_root(&self, root: &LogicalPlanRoot) -> PhysicalPlanRoot {
112 self.optimize_root_with_catalog(root, &OptimizerCatalog::optimistic())
113 }
114
115 pub fn optimize_root_with_catalog(
116 &self,
117 root: &LogicalPlanRoot,
118 catalog: &OptimizerCatalog,
119 ) -> PhysicalPlanRoot {
120 self.optimize_lowering_ready_root_with_catalog_and_directive(
121 &root.clone().into_lowering_ready(),
122 catalog,
123 OptimizerSearchDirective::Auto,
124 )
125 .expect("automatic optimizer search cannot reject its directive")
126 }
127
128 pub fn optimize_lowering_ready_root_with_catalog(
129 &self,
130 root: &LoweringReadyLogicalPlanRoot,
131 catalog: &OptimizerCatalog,
132 ) -> PhysicalPlanRoot {
133 self.optimize_lowering_ready_root_with_catalog_and_directive(
134 root,
135 catalog,
136 OptimizerSearchDirective::Auto,
137 )
138 .expect("automatic optimizer search cannot reject its directive")
139 }
140
141 pub fn optimize_root_with_catalog_and_directive(
142 &self,
143 root: &LogicalPlanRoot,
144 catalog: &OptimizerCatalog,
145 directive: OptimizerSearchDirective,
146 ) -> Result<PhysicalPlanRoot, OptimizerSearchDirectiveError> {
147 self.optimize_lowering_ready_root_with_catalog_and_directive(
148 &root.clone().into_lowering_ready(),
149 catalog,
150 directive,
151 )
152 }
153
154 pub fn optimize_lowering_ready_root_with_catalog_and_directive(
155 &self,
156 root: &LoweringReadyLogicalPlanRoot,
157 catalog: &OptimizerCatalog,
158 directive: OptimizerSearchDirective,
159 ) -> Result<PhysicalPlanRoot, OptimizerSearchDirectiveError> {
160 let rewrite = rewrite_logical_plan(root.plan());
161 let logical = rewrite.plan();
162 let required_groups = logical_group_count(logical);
163 let max_groups = self.context.optimizer_config().max_groups;
164 if directive == OptimizerSearchDirective::Memo && required_groups > max_groups {
165 return Err(OptimizerSearchDirectiveError::MemoGroupBudgetExceeded {
166 required_groups,
167 max_groups,
168 });
169 }
170 if directive == OptimizerSearchDirective::DirectFallback
171 || (directive == OptimizerSearchDirective::Auto && required_groups > max_groups)
172 {
173 let mut decisions = Vec::new();
174 let mut stage_events = Vec::new();
175 let plan = lower_logical(
176 logical,
177 LoweringChildren::Direct,
178 catalog,
179 &self.context,
180 &mut decisions,
181 &mut stage_events,
182 );
183 let plan = finalize_physical_plan(plan, &mut decisions, &mut stage_events);
184 let mut report = if directive == OptimizerSearchDirective::DirectFallback {
185 OptimizationSearchReport::forced_direct_fallback(required_groups)
186 } else {
187 OptimizationSearchReport::direct_fallback(required_groups, max_groups)
188 };
189 record_logical_rewrite(&mut report, &rewrite);
190 report.push_stage_event(
191 LOGICAL_GROUPING_STAGE.trace(StageStats::new(1, required_groups)),
192 );
193 for event in stage_events {
194 report.push_stage_event(event);
195 }
196 report.push_stage_event(
197 DIRECT_PHYSICAL_FALLBACK_STAGE.trace(StageStats::new(required_groups, 1)),
198 );
199 report.extend_decisions(decisions);
200 let selected = selected_plan_trace(&plan, catalog, &self.context);
201 report.push_stage_event(SELECTED_PLAN_COSTING_STAGE.trace(StageStats::new(1, 1)));
202 report.record_selected_plan_cost(selected.cost);
203 return Ok(PhysicalPlanRoot::new(plan, report.into_trace(selected)));
204 }
205 let mut memo = GraphMemo::default();
206 let root = insert_logical_group(&mut memo, logical);
207 let mut decisions = Vec::new();
208 let mut stage_events = Vec::new();
209 let plan = best_physical(
210 &memo,
211 root,
212 catalog,
213 &self.context,
214 &mut decisions,
215 &mut stage_events,
216 );
217 let plan = finalize_physical_plan(plan, &mut decisions, &mut stage_events);
218 let mut report = OptimizationSearchReport::memo(memo.group_count());
219 record_logical_rewrite(&mut report, &rewrite);
220 report
221 .push_stage_event(LOGICAL_GROUPING_STAGE.trace(StageStats::new(1, memo.group_count())));
222 let (applied_rules, skipped_rules) = stage_rule_counts(&stage_events);
223 for event in stage_events {
224 report.push_stage_event(event);
225 }
226 report.push_stage_event(PHYSICAL_SEARCH_STAGE.trace(
227 StageStats::new(memo.group_count(), 1).with_rule_counts(applied_rules, skipped_rules),
228 ));
229 report.extend_decisions(decisions);
230 let selected = selected_plan_trace(&plan, catalog, &self.context);
231 report.push_stage_event(SELECTED_PLAN_COSTING_STAGE.trace(StageStats::new(1, 1)));
232 report.record_selected_plan_cost(selected.cost);
233 if directive == OptimizerSearchDirective::Memo {
234 report.push_decision("selected memo search: explicit optimizer search directive");
235 }
236 Ok(PhysicalPlanRoot::new(plan, report.into_trace(selected)))
237 }
238}
239
240fn refresh_selected_plan_trace(trace: &mut OptimizerTrace, selected: SelectedPlanTrace) {
241 let selected_cost = selected.cost.with_cardinality_floor();
242 let selected_cost_breakdown = selected.cost_breakdown.with_cardinality_floor();
243 let cost_detail = format!(
244 "estimated_rows={} cost={}",
245 selected_cost.estimated_rows, selected_cost.cost
246 );
247 let cost_decision = format!("selected physical plan cost: {cost_detail}");
248 if let Some(decision) = trace
249 .decisions
250 .iter_mut()
251 .find(|decision| decision.starts_with("selected physical plan cost: "))
252 {
253 *decision = cost_decision;
254 } else {
255 trace.decisions.push(cost_decision);
256 }
257 let cost_event = RuleEvent::selected("physical plan cost", cost_detail);
258 if let Some(event) = trace.rule_events.iter_mut().find(|event| {
259 event.rule() == "physical plan cost" && event.outcome() == RuleOutcome::Selected
260 }) {
261 *event = cost_event;
262 } else {
263 trace.rule_events.push(cost_event);
264 }
265
266 trace.query_digest = selected.query_digest;
267 trace.selected_plan = selected.explain;
268 trace.selected_plan_fingerprint = selected.fingerprint;
269 trace.selected_plan_cost = selected_cost;
270 trace.selected_plan_cost_breakdown = selected_cost_breakdown;
271 trace.selected_plan_properties = selected.properties;
272 trace.selected_plan_cardinality_estimates = selected.cardinality_estimates;
273 trace.selected_plan_operator_counts = selected.operator_counts;
274 trace.selected_plan_class_counts = selected.class_counts;
275}
276
277pub(super) fn record_logical_rewrite(
278 report: &mut OptimizationSearchReport,
279 rewrite: &LogicalRewriteOutput,
280) {
281 report.push_stage_event(rewrite.trace().clone());
282 if let Some(warning) = rewrite.warning() {
283 report.push_warning(warning);
284 }
285 for event in rewrite.events().iter().cloned() {
286 report.push_rule_event(event);
287 }
288}
289
290fn insert_logical_group(memo: &mut GraphMemo, logical: &LogicalPlan) -> GroupId {
291 let expr = GroupExpr::from_logical(logical, memo);
292 memo.insert_group(expr)
293}
294
295fn best_physical(
296 memo: &GraphMemo,
297 root: GroupId,
298 catalog: &OptimizerCatalog,
299 optimizer_context: &OptimizerContext,
300 decisions: &mut Vec<String>,
301 stage_events: &mut Vec<StageTrace>,
302) -> PhysicalPlan {
303 let group = memo.group(root).expect("memo group id should exist");
304 group
305 .first_expression()
306 .expect("memo group should contain at least one expression")
307 .to_physical(memo, catalog, optimizer_context, decisions, stage_events)
308}
309
310fn select_bounded_sort_plan(
311 items: Vec<SortItem>,
312 offset: usize,
313 limit: usize,
314 input: PhysicalPlan,
315 catalog: &OptimizerCatalog,
316 decisions: &mut Vec<String>,
317) -> PhysicalPlan {
318 if satisfies_ordering(&input, &items, catalog) {
319 decisions.push("elide bounded sort: access path satisfies required ordering".to_string());
320 return PhysicalPlan::LimitExec {
321 offset,
322 limit: Some(limit),
323 input: Box::new(input),
324 };
325 }
326 let top_n = PhysicalPlan::TopNExec {
327 items: items.clone(),
328 offset,
329 limit,
330 input: Box::new(input.clone()),
331 };
332 let sort_limit = PhysicalPlan::LimitExec {
333 offset,
334 limit: Some(limit),
335 input: Box::new(PhysicalPlan::SortExec {
336 items,
337 input: Box::new(input),
338 }),
339 };
340 let top_n_cost = estimate_physical_plan_cost(&top_n, catalog);
341 let sort_limit_cost = estimate_physical_plan_cost(&sort_limit, catalog);
342 if top_n_cost.cost < sort_limit_cost.cost {
343 decisions.push(format!(
344 "choose TopN for bounded sort: offset={offset} limit={limit} top_n_cost={} sort_limit_cost={}",
345 top_n_cost.cost, sort_limit_cost.cost
346 ));
347 top_n
348 } else {
349 decisions.push(format!(
350 "keep Sort + Limit for bounded sort: offset={offset} limit={limit} top_n_cost={} sort_limit_cost={}",
351 top_n_cost.cost, sort_limit_cost.cost
352 ));
353 sort_limit
354 }
355}
356
357fn stage_rule_counts(stage_events: &[StageTrace]) -> (usize, usize) {
358 stage_events
359 .iter()
360 .fold((0, 0), |(applied, skipped), event| {
361 let stats = event.stats();
362 (
363 applied.saturating_add(stats.applied_rules),
364 skipped.saturating_add(stats.skipped_rules),
365 )
366 })
367}
368
369impl GroupExpr {
370 fn from_logical(logical: &LogicalPlan, memo: &mut GraphMemo) -> Self {
371 match logical {
372 LogicalPlan::GraphMatch { input, .. } => Self {
373 logical: logical.clone(),
374 children: input
375 .iter()
376 .map(|input| insert_logical_group(memo, input))
377 .collect(),
378 },
379 LogicalPlan::CreateNodeLabel { .. }
380 | LogicalPlan::CreateRelationshipType { .. }
381 | LogicalPlan::CreateNodeTable { .. }
382 | LogicalPlan::CreateRelationshipTable { .. }
383 | LogicalPlan::CreateProperty { .. }
384 | LogicalPlan::AlterTableState { .. }
385 | LogicalPlan::AlterPropertyState { .. }
386 | LogicalPlan::CreateIndex { .. }
387 | LogicalPlan::CreateCompositeIndex { .. }
388 | LogicalPlan::CreateRangeIndex { .. }
389 | LogicalPlan::CreateFullTextIndex { .. }
390 | LogicalPlan::CreateUniqueConstraint { .. }
391 | LogicalPlan::CreateNodePropertyExistsConstraint { .. }
392 | LogicalPlan::CreateRelationshipUniqueConstraint { .. }
393 | LogicalPlan::CreateRelationshipPropertyExistsConstraint { .. }
394 | LogicalPlan::ProjectGraph { .. }
395 | LogicalPlan::GraphAlgorithm { .. }
396 | LogicalPlan::GraphAlgorithmOutput(_)
397 | LogicalPlan::VectorSeed { .. }
398 | LogicalPlan::TextSeed { .. }
399 | LogicalPlan::GraphSeed { .. }
400 | LogicalPlan::CreateNode { .. }
401 | LogicalPlan::UnwindMutation { .. }
402 | LogicalPlan::MergeNode { .. }
403 | LogicalPlan::MergeRelationship { .. }
404 | LogicalPlan::MergeMatchedRelationship { .. }
405 | LogicalPlan::MergeRelationshipFromMatchedRelationship { .. }
406 | LogicalPlan::MergeRelationshipToMatchedTarget { .. }
407 | LogicalPlan::MergeRelationshipFromMatchedTarget { .. }
408 | LogicalPlan::CreateMatchedRelationship { .. }
409 | LogicalPlan::SetNodeProperty { .. }
410 | LogicalPlan::SetNodeProperties { .. }
411 | LogicalPlan::SetNodePropertiesReturn { .. }
412 | LogicalPlan::SetRelationshipProperty { .. }
413 | LogicalPlan::SetRelationshipProperties { .. }
414 | LogicalPlan::DeleteNode { .. }
415 | LogicalPlan::DeleteRelationship { .. }
416 | LogicalPlan::DeleteRelationshipTargetNodes { .. }
417 | LogicalPlan::CreateRelationship { .. }
418 | LogicalPlan::NodeScan { .. }
419 | LogicalPlan::OptionalRelationshipCountSum { .. }
420 | LogicalPlan::ThreadRepairStats { .. }
421 | LogicalPlan::ShortestPath { .. } => Self {
422 logical: logical.clone(),
423 children: Vec::new(),
424 },
425 LogicalPlan::Limit { limit: Some(0), .. } => Self {
426 logical: logical.clone(),
427 children: Vec::new(),
428 },
429 LogicalPlan::NodeCartesianProduct { left, right } => Self {
430 logical: logical.clone(),
431 children: vec![
432 insert_logical_group(memo, left),
433 insert_logical_group(memo, right),
434 ],
435 },
436 LogicalPlan::Limit {
437 limit: Some(_),
438 input,
439 ..
440 } if matches!(input.as_ref(), LogicalPlan::Sort { .. }) => {
441 let LogicalPlan::Sort { input, .. } = input.as_ref() else {
442 unreachable!("guard requires a sort input");
443 };
444 Self {
445 logical: logical.clone(),
446 children: vec![insert_logical_group(memo, input)],
447 }
448 }
449 LogicalPlan::Expand { input, .. }
450 | LogicalPlan::NodeColumnLookup { input, .. }
451 | LogicalPlan::OptionalDegree { input, .. }
452 | LogicalPlan::Filter { input, .. }
453 | LogicalPlan::Project { input, .. }
454 | LogicalPlan::Aggregate { input, .. }
455 | LogicalPlan::Distinct { input }
456 | LogicalPlan::Sort { input, .. }
457 | LogicalPlan::Limit { input, .. } => Self {
458 logical: logical.clone(),
459 children: vec![insert_logical_group(memo, input)],
460 },
461 }
462 }
463
464 fn to_physical(
465 &self,
466 memo: &GraphMemo,
467 catalog: &OptimizerCatalog,
468 optimizer_context: &OptimizerContext,
469 decisions: &mut Vec<String>,
470 stage_events: &mut Vec<StageTrace>,
471 ) -> PhysicalPlan {
472 lower_logical(
473 &self.logical,
474 LoweringChildren::Memo {
475 memo,
476 children: &self.children,
477 },
478 catalog,
479 optimizer_context,
480 decisions,
481 stage_events,
482 )
483 }
484}
485
486fn push_seed_metadata_filter(
487 input: &mut PhysicalPlan,
488 predicate: &hawdb_plan_cypher::Predicate,
489 decisions: &mut Vec<String>,
490) {
491 if let hawdb_plan_cypher::Predicate::And(predicates) = predicate {
492 for predicate in predicates {
493 push_seed_metadata_filter(input, predicate, decisions);
494 }
495 return;
496 }
497 let hawdb_plan_cypher::Predicate::PropertyEq {
498 variable,
499 property,
500 value,
501 } = predicate
502 else {
503 return;
504 };
505 let Some(filter_field) = seed_metadata_field(property) else {
506 return;
507 };
508 let Some(filter_value) = seed_metadata_value(value) else {
509 return;
510 };
511 let producer = seed_window(input)
512 .map(|(producer, _)| producer)
513 .unwrap_or("retriever");
514 if attach_seed_metadata_filter(input, variable, filter_field, filter_value) {
515 decisions.push(format!(
516 "push descriptor-safe {producer} seed filter {variable}.{property} before candidate generation"
517 ));
518 }
519}
520
521fn attach_seed_metadata_filter(
522 plan: &mut PhysicalPlan,
523 variable: &str,
524 field: &str,
525 value: String,
526) -> bool {
527 match plan {
528 PhysicalPlan::NodeColumnLookupExec {
529 variable: lookup_variable,
530 input,
531 ..
532 } if lookup_variable == variable => attach_metadata_filter_to_seed(input, field, value),
533 PhysicalPlan::AdjacencyExpandExec {
534 source_variable,
535 input,
536 ..
537 } if source_variable == variable => {
538 attach_seed_metadata_filter(input, variable, field, value)
539 }
540 _ => false,
541 }
542}
543
544fn attach_metadata_filter_to_seed(plan: &mut PhysicalPlan, field: &str, value: String) -> bool {
545 if let PhysicalPlan::TextSeedScan {
546 metadata_filters, ..
547 } = plan
548 {
549 if metadata_filters
550 .get(field)
551 .is_some_and(|existing| existing != &value)
552 {
553 return false;
554 }
555 metadata_filters.insert(field.to_string(), value);
556 return true;
557 }
558 let PhysicalPlan::VectorSeedScan {
559 metadata_filters,
560 vector_plan,
561 ..
562 } = plan
563 else {
564 return false;
565 };
566 if metadata_filters
567 .get(field)
568 .is_some_and(|existing| existing != &value)
569 {
570 return false;
571 }
572 metadata_filters.insert(field.to_string(), value);
573 attach_vector_filter_field(vector_plan, field);
574 true
575}
576
577fn attach_vector_filter_field(plan: &mut hawdb_plan_cypher::VectorPhysicalPlan, field: &str) {
578 match plan {
579 hawdb_plan_cypher::VectorPhysicalPlan::Filter { fields } => {
580 if !fields.iter().any(|existing| existing == field) {
581 fields.push(field.to_string());
582 fields.sort();
583 }
584 }
585 hawdb_plan_cypher::VectorPhysicalPlan::VectorCandidateScan { input, .. }
586 | hawdb_plan_cypher::VectorPhysicalPlan::ResidualFilter { input, .. }
587 | hawdb_plan_cypher::VectorPhysicalPlan::RawVectorRerank { input, .. }
588 | hawdb_plan_cypher::VectorPhysicalPlan::TopK { input, .. } => {
589 attach_vector_filter_field(input, field);
590 }
591 }
592}
593
594fn seed_metadata_field(property: &str) -> Option<&str> {
595 match property {
596 "id" => Some("external_id"),
597 "kind" | "external_id" | "source_id" | "space_id" | "unit_type" | "lifecycle_state"
598 | "importance" | "confidence" | "created_at" | "updated_at" | "event_start"
599 | "event_end" | "is_latest" => Some(property),
600 _ => None,
601 }
602}
603
604fn seed_metadata_value(value: &Value) -> Option<String> {
605 match value {
606 Value::String(value) => Some(value.clone()),
607 Value::Bool(value) => Some(value.to_string()),
608 Value::Int(value) => Some(value.to_string()),
609 Value::Float(value) if value.is_finite() => Some(value.to_string()),
610 Value::Uuid(value) => Some(value.to_string()),
611 Value::Null | Value::Float(_) | Value::Binary(_) | Value::List(_) | Value::Map(_) => None,
612 }
613}
614
615fn seed_window(plan: &PhysicalPlan) -> Option<(&'static str, usize)> {
616 match plan {
617 PhysicalPlan::VectorSeedScan { vector_plan, .. } => {
618 vector_plan_top_k(vector_plan).map(|window| ("vector", window))
619 }
620 PhysicalPlan::TextSeedScan { top_k, .. } => Some(("text", *top_k)),
621 PhysicalPlan::GraphSeedScan { top_k, .. } => Some(("graph", *top_k)),
622 PhysicalPlan::NodeColumnLookupExec { input, .. }
623 | PhysicalPlan::AdjacencyExpandExec { input, .. }
624 | PhysicalPlan::AdjacencyExistsExec { input, .. }
625 | PhysicalPlan::FilterExec { input, .. }
626 | PhysicalPlan::ProjectExec { input, .. }
627 | PhysicalPlan::LimitExec { input, .. } => seed_window(input),
628 _ => None,
629 }
630}
631
632fn vector_plan_top_k(plan: &hawdb_plan_cypher::VectorPhysicalPlan) -> Option<usize> {
633 match plan {
634 hawdb_plan_cypher::VectorPhysicalPlan::TopK { limit, .. } => Some(*limit),
635 hawdb_plan_cypher::VectorPhysicalPlan::VectorCandidateScan { input, .. }
636 | hawdb_plan_cypher::VectorPhysicalPlan::ResidualFilter { input, .. }
637 | hawdb_plan_cypher::VectorPhysicalPlan::RawVectorRerank { input, .. } => {
638 vector_plan_top_k(input)
639 }
640 hawdb_plan_cypher::VectorPhysicalPlan::Filter { .. } => None,
641 }
642}
643
644fn logical_group_count(logical: &LogicalPlan) -> usize {
645 match logical {
646 LogicalPlan::GraphMatch { input, .. } => {
647 1 + input.as_deref().map(logical_group_count).unwrap_or(0)
648 }
649 LogicalPlan::Limit { limit: Some(0), .. } => 1,
650 LogicalPlan::Limit {
651 limit: Some(_),
652 input,
653 ..
654 } if matches!(input.as_ref(), LogicalPlan::Sort { .. }) => {
655 let LogicalPlan::Sort { input, .. } = input.as_ref() else {
656 unreachable!("guard requires a sort input");
657 };
658 1 + logical_group_count(input)
660 }
661 LogicalPlan::Expand { input, .. }
662 | LogicalPlan::NodeColumnLookup { input, .. }
663 | LogicalPlan::OptionalDegree { input, .. }
664 | LogicalPlan::Filter { input, .. }
665 | LogicalPlan::Project { input, .. }
666 | LogicalPlan::Aggregate { input, .. }
667 | LogicalPlan::Distinct { input }
668 | LogicalPlan::Sort { input, .. }
669 | LogicalPlan::Limit { input, .. } => 1 + logical_group_count(input),
670 LogicalPlan::OptionalRelationshipCountSum { .. }
671 | LogicalPlan::ThreadRepairStats { .. }
672 | LogicalPlan::ShortestPath { .. } => 1,
673 LogicalPlan::CreateNodeLabel { .. }
674 | LogicalPlan::CreateRelationshipType { .. }
675 | LogicalPlan::CreateNodeTable { .. }
676 | LogicalPlan::CreateRelationshipTable { .. }
677 | LogicalPlan::CreateProperty { .. }
678 | LogicalPlan::AlterTableState { .. }
679 | LogicalPlan::AlterPropertyState { .. }
680 | LogicalPlan::CreateIndex { .. }
681 | LogicalPlan::CreateCompositeIndex { .. }
682 | LogicalPlan::CreateRangeIndex { .. }
683 | LogicalPlan::CreateFullTextIndex { .. }
684 | LogicalPlan::CreateUniqueConstraint { .. }
685 | LogicalPlan::CreateNodePropertyExistsConstraint { .. }
686 | LogicalPlan::CreateRelationshipUniqueConstraint { .. }
687 | LogicalPlan::CreateRelationshipPropertyExistsConstraint { .. }
688 | LogicalPlan::ProjectGraph { .. }
689 | LogicalPlan::GraphAlgorithm { .. }
690 | LogicalPlan::GraphAlgorithmOutput(_)
691 | LogicalPlan::VectorSeed { .. }
692 | LogicalPlan::TextSeed { .. }
693 | LogicalPlan::GraphSeed { .. }
694 | LogicalPlan::CreateNode { .. }
695 | LogicalPlan::UnwindMutation { .. }
696 | LogicalPlan::MergeNode { .. }
697 | LogicalPlan::MergeRelationship { .. }
698 | LogicalPlan::MergeMatchedRelationship { .. }
699 | LogicalPlan::MergeRelationshipFromMatchedRelationship { .. }
700 | LogicalPlan::MergeRelationshipToMatchedTarget { .. }
701 | LogicalPlan::MergeRelationshipFromMatchedTarget { .. }
702 | LogicalPlan::CreateMatchedRelationship { .. }
703 | LogicalPlan::SetNodeProperty { .. }
704 | LogicalPlan::SetNodeProperties { .. }
705 | LogicalPlan::SetNodePropertiesReturn { .. }
706 | LogicalPlan::SetRelationshipProperty { .. }
707 | LogicalPlan::SetRelationshipProperties { .. }
708 | LogicalPlan::DeleteNode { .. }
709 | LogicalPlan::DeleteRelationship { .. }
710 | LogicalPlan::DeleteRelationshipTargetNodes { .. }
711 | LogicalPlan::CreateRelationship { .. }
712 | LogicalPlan::NodeScan { .. } => 1,
713 LogicalPlan::NodeCartesianProduct { left, right } => {
714 1 + logical_group_count(left) + logical_group_count(right)
715 }
716 }
717}
718
719enum LoweringChildren<'a> {
722 Memo {
723 memo: &'a GraphMemo,
724 children: &'a [GroupId],
725 },
726 Direct,
727}
728
729impl LoweringChildren<'_> {
730 fn lower(
731 &self,
732 logical: &LogicalPlan,
733 index: usize,
734 catalog: &OptimizerCatalog,
735 optimizer_context: &OptimizerContext,
736 decisions: &mut Vec<String>,
737 stage_events: &mut Vec<StageTrace>,
738 ) -> PhysicalPlan {
739 match self {
740 Self::Memo { memo, children } => best_physical(
741 memo,
742 children[index],
743 catalog,
744 optimizer_context,
745 decisions,
746 stage_events,
747 ),
748 Self::Direct => lower_logical(
749 logical,
750 Self::Direct,
751 catalog,
752 optimizer_context,
753 decisions,
754 stage_events,
755 ),
756 }
757 }
758}
759
760fn lower_logical(
761 logical: &LogicalPlan,
762 children: LoweringChildren<'_>,
763 catalog: &OptimizerCatalog,
764 optimizer_context: &OptimizerContext,
765 decisions: &mut Vec<String>,
766 stage_events: &mut Vec<StageTrace>,
767) -> PhysicalPlan {
768 if let Some(plan) = select_exact_count_fast_path(logical, decisions, stage_events) {
769 return plan;
770 }
771 if let Some(plan) = simple::lower_simple_logical(logical, optimizer_context) {
772 return plan;
773 }
774 match logical {
775 LogicalPlan::GraphMatch { program, input } => PhysicalPlan::GraphMatchExec {
776 program: program.clone(),
777 input: input.as_ref().map(|input| {
778 Box::new(children.lower(
779 input,
780 0,
781 catalog,
782 optimizer_context,
783 decisions,
784 stage_events,
785 ))
786 }),
787 },
788 LogicalPlan::NodeCartesianProduct { left, right } => {
789 let left = children.lower(left, 0, catalog, optimizer_context, decisions, stage_events);
790 let right = children.lower(
791 right,
792 1,
793 catalog,
794 optimizer_context,
795 decisions,
796 stage_events,
797 );
798 let (left, right) =
799 order_single_row_cartesian_product_children(catalog, decisions, left, right);
800 push_cartesian_product_cost_decision(catalog, decisions, &left, &right);
801 PhysicalPlan::NodeCartesianProductExec {
802 left: Box::new(left),
803 right: Box::new(right),
804 }
805 }
806 LogicalPlan::NodeColumnLookup {
807 variable,
808 label,
809 property,
810 column,
811 optional,
812 node_visibility_predicate,
813 input,
814 } => PhysicalPlan::NodeColumnLookupExec {
815 variable: variable.clone(),
816 label: label.clone(),
817 property: property.clone(),
818 column: column.clone(),
819 optional: *optional,
820 node_visibility_predicate: node_visibility_predicate.clone(),
821 input: Box::new(children.lower(
822 input,
823 0,
824 catalog,
825 optimizer_context,
826 decisions,
827 stage_events,
828 )),
829 },
830 LogicalPlan::Expand {
831 source_variable,
832 source_label,
833 rel_variable,
834 rel_type,
835 rel_properties,
836 direction,
837 target_variable,
838 target_label,
839 min_hops,
840 max_hops,
841 optional,
842 input,
843 } => {
844 push_expand_estimate_decision(
845 catalog,
846 decisions,
847 ExpandEstimateRequest {
848 source_label,
849 rel_type,
850 rel_properties,
851 target_label,
852 min_hops: *min_hops,
853 max_hops: *max_hops,
854 },
855 );
856 let input = children.lower(
857 input,
858 0,
859 catalog,
860 optimizer_context,
861 decisions,
862 stage_events,
863 );
864 let graph_budget = seed_window(&input).map(|(producer, _)| {
865 decisions.push(format!(
866 "admit {producer}-seeded graph expansion using execution memory budgets"
867 ));
868 GraphExpansionBudget {
872 candidate_limit: usize::MAX,
873 payload_byte_limit: usize::MAX,
874 }
875 });
876 PhysicalPlan::AdjacencyExpandExec {
877 source_variable: source_variable.clone(),
878 source_label: source_label.clone(),
879 rel_variable: rel_variable.clone(),
880 rel_type: rel_type.clone(),
881 rel_properties: rel_properties.clone(),
882 direction: *direction,
883 target_variable: target_variable.clone(),
884 target_label: target_label.clone(),
885 min_hops: *min_hops,
886 max_hops: *max_hops,
887 optional: *optional,
888 graph_budget,
889 input: Box::new(input),
890 }
891 }
892 LogicalPlan::OptionalDegree {
893 source_variable,
894 rel_type,
895 rel_properties,
896 direction,
897 target_label,
898 target_properties,
899 alias,
900 input,
901 } => PhysicalPlan::OptionalDegreeExec {
902 source_variable: source_variable.clone(),
903 rel_type: rel_type.clone(),
904 rel_properties: rel_properties.clone(),
905 direction: *direction,
906 target_label: target_label.clone(),
907 target_properties: target_properties.clone(),
908 alias: alias.clone(),
909 input: Box::new(children.lower(
910 input,
911 0,
912 catalog,
913 optimizer_context,
914 decisions,
915 stage_events,
916 )),
917 },
918 LogicalPlan::OptionalRelationshipCountSum {
919 variable,
920 label,
921 properties,
922 legs,
923 output,
924 } => {
925 push_optional_relationship_count_sum_cost_decision(
926 catalog, decisions, label, properties, legs,
927 );
928 PhysicalPlan::OptionalRelationshipCountSumExec {
929 variable: variable.clone(),
930 label: label.clone(),
931 properties: properties.clone(),
932 legs: legs.clone(),
933 output: output.clone(),
934 }
935 }
936 LogicalPlan::Filter { predicate, input } => {
937 if let Some(plan) =
938 index_seek_from_filter(predicate, input, catalog, decisions, stage_events)
939 {
940 plan
941 } else if let Some(plan) = access::source_segment_scan_from_filter(predicate, input) {
942 decisions.push(
943 "choose SourceSegmentScan for storage-prunable Source filter".to_string(),
944 );
945 PhysicalPlan::FilterExec {
946 predicate: predicate.clone(),
947 input: Box::new(plan),
948 }
949 } else if let Predicate::BoundRelationshipExists {
950 source_variable,
951 rel_type,
952 direction,
953 target_variable,
954 } = predicate
955 {
956 decisions.push(
957 "lower bound relationship existence predicate to AdjacencyExistsExec"
958 .to_string(),
959 );
960 PhysicalPlan::AdjacencyExistsExec {
961 source_variable: source_variable.clone(),
962 rel_type: rel_type.clone(),
963 direction: *direction,
964 target_variable: target_variable.clone(),
965 input: Box::new(children.lower(
966 input,
967 0,
968 catalog,
969 optimizer_context,
970 decisions,
971 stage_events,
972 )),
973 }
974 } else {
975 let mut input = children.lower(
976 input,
977 0,
978 catalog,
979 optimizer_context,
980 decisions,
981 stage_events,
982 );
983 push_seed_metadata_filter(&mut input, predicate, decisions);
984 input = join::lower_property_join(input, predicate, decisions);
985 PhysicalPlan::FilterExec {
986 predicate: predicate.clone(),
987 input: Box::new(input),
988 }
989 }
990 }
991 LogicalPlan::Project { items, input } => PhysicalPlan::ProjectExec {
992 items: items.clone(),
993 input: Box::new(children.lower(
994 input,
995 0,
996 catalog,
997 optimizer_context,
998 decisions,
999 stage_events,
1000 )),
1001 },
1002 LogicalPlan::Aggregate {
1003 group_keys,
1004 items,
1005 input,
1006 } => PhysicalPlan::AggregateExec {
1007 group_keys: group_keys.clone(),
1008 items: items.clone(),
1009 input: Box::new(children.lower(
1010 input,
1011 0,
1012 catalog,
1013 optimizer_context,
1014 decisions,
1015 stage_events,
1016 )),
1017 },
1018 LogicalPlan::Distinct { input } => PhysicalPlan::DistinctExec {
1019 input: Box::new(children.lower(
1020 input,
1021 0,
1022 catalog,
1023 optimizer_context,
1024 decisions,
1025 stage_events,
1026 )),
1027 },
1028 LogicalPlan::Sort { items, input } => {
1029 let input = children.lower(
1030 input,
1031 0,
1032 catalog,
1033 optimizer_context,
1034 decisions,
1035 stage_events,
1036 );
1037 if satisfies_ordering(&input, items, catalog) {
1038 decisions
1039 .push("elide SortExec: access path satisfies required ordering".to_string());
1040 input
1041 } else {
1042 PhysicalPlan::SortExec {
1043 items: items.clone(),
1044 input: Box::new(input),
1045 }
1046 }
1047 }
1048 LogicalPlan::Limit {
1049 offset,
1050 limit,
1051 input,
1052 } => {
1053 if *limit == Some(0) {
1054 PhysicalPlan::EmptyExec
1055 } else if let (Some(limit), LogicalPlan::Sort { items, input }) =
1056 (limit, input.as_ref())
1057 {
1058 let input = children.lower(
1059 input,
1060 0,
1061 catalog,
1062 optimizer_context,
1063 decisions,
1064 stage_events,
1065 );
1066 select_bounded_sort_plan(items.clone(), *offset, *limit, input, catalog, decisions)
1067 } else {
1068 PhysicalPlan::LimitExec {
1069 offset: *offset,
1070 limit: *limit,
1071 input: Box::new(children.lower(
1072 input,
1073 0,
1074 catalog,
1075 optimizer_context,
1076 decisions,
1077 stage_events,
1078 )),
1079 }
1080 }
1081 }
1082 _ => unreachable!("leaf logical plans are lowered before child planning"),
1083 }
1084}
1085
1086fn select_exact_count_fast_path(
1087 logical: &LogicalPlan,
1088 decisions: &mut Vec<String>,
1089 stage_events: &mut Vec<StageTrace>,
1090) -> Option<PhysicalPlan> {
1091 if let Some(plan) = select_node_count_fast_path(logical, decisions, stage_events) {
1092 return Some(plan);
1093 }
1094 select_relationship_count_fast_path(logical, decisions, stage_events)
1095}
1096
1097fn select_node_count_fast_path(
1098 logical: &LogicalPlan,
1099 decisions: &mut Vec<String>,
1100 stage_events: &mut Vec<StageTrace>,
1101) -> Option<PhysicalPlan> {
1102 let LogicalPlan::Aggregate {
1103 group_keys,
1104 items,
1105 input,
1106 } = logical
1107 else {
1108 return None;
1109 };
1110 let [item] = items.as_slice() else {
1111 return None;
1112 };
1113 let LogicalPlan::NodeScan { variable, label } = input.as_ref() else {
1114 return None;
1115 };
1116 let exact_node_count = group_keys.is_empty()
1117 && item.function == AggregateFunction::Count
1118 && match &item.target {
1119 AggregateTarget::All => !item.distinct,
1120 AggregateTarget::Variable(target) => target == variable,
1121 AggregateTarget::Property { .. }
1122 | AggregateTarget::Column(_)
1123 | AggregateTarget::ColumnProperty { .. } => false,
1124 };
1125 if !exact_node_count {
1126 return None;
1127 }
1128
1129 decisions.push(format!(
1130 "selected implementation:node_count_fast_path: use exact label count for {label}"
1131 ));
1132 stage_events
1133 .push(ACCESS_PATH_SELECTION_STAGE.trace(StageStats::new(1, 1).with_rule_counts(1, 0)));
1134 Some(PhysicalPlan::NodeCountExec {
1135 label: label.clone(),
1136 output: item.name.clone(),
1137 })
1138}
1139
1140fn select_relationship_count_fast_path(
1141 logical: &LogicalPlan,
1142 decisions: &mut Vec<String>,
1143 stage_events: &mut Vec<StageTrace>,
1144) -> Option<PhysicalPlan> {
1145 let LogicalPlan::Aggregate {
1146 group_keys,
1147 items,
1148 input,
1149 } = logical
1150 else {
1151 return None;
1152 };
1153 let [item] = items.as_slice() else {
1154 return None;
1155 };
1156 let LogicalPlan::Expand {
1157 source_variable,
1158 source_label,
1159 rel_variable,
1160 rel_type,
1161 rel_properties,
1162 direction,
1163 target_label,
1164 min_hops,
1165 max_hops,
1166 optional,
1167 input,
1168 ..
1169 } = input.as_ref()
1170 else {
1171 return None;
1172 };
1173 let LogicalPlan::NodeScan { variable, label } = input.as_ref() else {
1174 return None;
1175 };
1176 let exact_relationship_count = group_keys.is_empty()
1177 && item.function == AggregateFunction::Count
1178 && !item.distinct
1179 && source_variable == variable
1180 && source_label.is_empty()
1181 && label.is_empty()
1182 && target_label.is_empty()
1183 && rel_properties.is_empty()
1184 && *min_hops == 1
1185 && *max_hops == 1
1186 && !optional
1187 && matches!(
1188 direction,
1189 RelationshipDirection::Outgoing | RelationshipDirection::Incoming
1190 )
1191 && match &item.target {
1192 AggregateTarget::All => true,
1193 AggregateTarget::Variable(target) => rel_variable.as_ref() == Some(target),
1194 AggregateTarget::Property { .. }
1195 | AggregateTarget::Column(_)
1196 | AggregateTarget::ColumnProperty { .. } => false,
1197 };
1198 if !exact_relationship_count {
1199 return None;
1200 }
1201
1202 decisions.push(format!(
1203 "selected implementation:relationship_count_fast_path: use exact relationship count for {rel_type}"
1204 ));
1205 stage_events
1206 .push(ACCESS_PATH_SELECTION_STAGE.trace(StageStats::new(1, 1).with_rule_counts(1, 0)));
1207 Some(PhysicalPlan::RelationshipCountExec {
1208 rel_type: rel_type.clone(),
1209 output: item.name.clone(),
1210 })
1211}
1212
1213fn finalize_physical_plan(
1214 plan: PhysicalPlan,
1215 decisions: &mut Vec<String>,
1216 stage_events: &mut Vec<StageTrace>,
1217) -> PhysicalPlan {
1218 let event_count_before = stage_events.len();
1219 let finalized = finalize_required_node_properties(plan, decisions, stage_events);
1220 if !stage_events[event_count_before..]
1221 .iter()
1222 .any(|event| event.name() == PLAN_FINALIZATION_STAGE.name())
1223 {
1224 stage_events.push(PLAN_FINALIZATION_STAGE.trace(StageStats::new(1, 1)));
1225 }
1226 finalized
1227}
1228
1229fn finalize_required_node_properties(
1230 plan: PhysicalPlan,
1231 decisions: &mut Vec<String>,
1232 stage_events: &mut Vec<StageTrace>,
1233) -> PhysicalPlan {
1234 match plan {
1235 PhysicalPlan::ProjectExec { items, input } => {
1236 select_node_projection_scan(&items, &input, decisions, stage_events)
1237 .unwrap_or(PhysicalPlan::ProjectExec { items, input })
1238 }
1239 PhysicalPlan::AggregateExec {
1240 group_keys,
1241 items,
1242 input,
1243 } => {
1244 let input = select_node_aggregate_required_scan(
1245 &group_keys,
1246 &items,
1247 &input,
1248 decisions,
1249 stage_events,
1250 )
1251 .map(Box::new)
1252 .unwrap_or(input);
1253 PhysicalPlan::AggregateExec {
1254 group_keys,
1255 items,
1256 input,
1257 }
1258 }
1259 PhysicalPlan::LimitExec {
1260 offset,
1261 limit,
1262 input,
1263 } => PhysicalPlan::LimitExec {
1264 offset,
1265 limit,
1266 input: Box::new(finalize_required_node_properties(
1267 *input,
1268 decisions,
1269 stage_events,
1270 )),
1271 },
1272 PhysicalPlan::DistinctExec { input } => PhysicalPlan::DistinctExec {
1273 input: Box::new(finalize_required_node_properties(
1274 *input,
1275 decisions,
1276 stage_events,
1277 )),
1278 },
1279 PhysicalPlan::SortExec { items, input }
1280 if sort_items_use_only_projected_columns(&items) =>
1281 {
1282 PhysicalPlan::SortExec {
1283 items,
1284 input: Box::new(finalize_required_node_properties(
1285 *input,
1286 decisions,
1287 stage_events,
1288 )),
1289 }
1290 }
1291 PhysicalPlan::TopNExec {
1292 items,
1293 offset,
1294 limit,
1295 input,
1296 } if sort_items_use_only_projected_columns(&items) => PhysicalPlan::TopNExec {
1297 items,
1298 offset,
1299 limit,
1300 input: Box::new(finalize_required_node_properties(
1301 *input,
1302 decisions,
1303 stage_events,
1304 )),
1305 },
1306 plan => plan,
1307 }
1308}
1309
1310fn sort_items_use_only_projected_columns(items: &[SortItem]) -> bool {
1311 items
1312 .iter()
1313 .all(|item| matches!(item.key, SortKey::Column(_)))
1314}
1315
1316fn select_node_projection_scan(
1317 items: &[Projection],
1318 input: &PhysicalPlan,
1319 decisions: &mut Vec<String>,
1320 stage_events: &mut Vec<StageTrace>,
1321) -> Option<PhysicalPlan> {
1322 let (source, predicate) = match input {
1323 PhysicalPlan::FilterExec { predicate, input } => (input.as_ref(), Some(predicate)),
1324 source => (source, None),
1325 };
1326 let (variable, label, access) = node_projection_access(source)?;
1327 let mut required_properties = BTreeSet::new();
1328 if !items.iter().all(|item| {
1329 collect_projection_properties(&item.expression, variable, &mut required_properties)
1330 }) || predicate.is_some_and(|predicate| {
1331 !collect_predicate_properties(predicate, variable, &mut required_properties)
1332 }) {
1333 return None;
1334 }
1335 collect_access_properties(&access, &mut required_properties);
1336
1337 let required_properties = required_properties.into_iter().collect::<Vec<_>>();
1338 decisions.push(format!(
1339 "selected implementation:node_projection_scan: access={} decode {} required properties for {variable}:{label}",
1340 access.kind_name(),
1341 required_properties.len(),
1342 ));
1343 stage_events.push(PLAN_FINALIZATION_STAGE.trace(StageStats::new(1, 1).with_rule_counts(1, 0)));
1344 Some(PhysicalPlan::NodeProjectionScanExec {
1345 variable: variable.clone(),
1346 label: label.clone(),
1347 access,
1348 required_properties,
1349 predicate: predicate.cloned(),
1350 items: items.to_vec(),
1351 })
1352}
1353
1354fn select_node_aggregate_required_scan(
1355 group_keys: &[Projection],
1356 items: &[hawdb_plan_cypher::Aggregation],
1357 input: &PhysicalPlan,
1358 decisions: &mut Vec<String>,
1359 stage_events: &mut Vec<StageTrace>,
1360) -> Option<PhysicalPlan> {
1361 let (source, predicate) = match input {
1362 PhysicalPlan::FilterExec { predicate, input } => (input.as_ref(), Some(predicate)),
1363 source => (source, None),
1364 };
1365 let (variable, label, access) = node_projection_access(source)?;
1366 let mut required_properties = BTreeSet::new();
1367 if !group_keys.iter().all(|item| {
1368 collect_projection_properties(&item.expression, variable, &mut required_properties)
1369 }) || !items
1370 .iter()
1371 .all(|item| collect_aggregate_properties(item, variable, &mut required_properties))
1372 || predicate.is_some_and(|predicate| {
1373 !collect_predicate_properties(predicate, variable, &mut required_properties)
1374 })
1375 {
1376 return None;
1377 }
1378 collect_access_properties(&access, &mut required_properties);
1379
1380 let required_properties = required_properties.into_iter().collect::<Vec<_>>();
1381 decisions.push(format!(
1382 "selected implementation:node_aggregate_required_scan: access={} decode {} required properties for {variable}:{label}",
1383 access.kind_name(),
1384 required_properties.len(),
1385 ));
1386 stage_events.push(PLAN_FINALIZATION_STAGE.trace(StageStats::new(1, 1).with_rule_counts(1, 0)));
1387 Some(PhysicalPlan::NodeProjectionScanExec {
1388 variable: variable.clone(),
1389 label: label.clone(),
1390 access,
1391 required_properties,
1392 predicate: predicate.cloned(),
1393 items: Vec::new(),
1394 })
1395}
1396
1397fn collect_aggregate_properties(
1398 item: &hawdb_plan_cypher::Aggregation,
1399 variable: &str,
1400 required: &mut BTreeSet<String>,
1401) -> bool {
1402 match (&item.function, &item.target) {
1403 (AggregateFunction::Count, AggregateTarget::All) => true,
1404 (AggregateFunction::Count, AggregateTarget::Variable(current)) => current == variable,
1405 (
1406 AggregateFunction::Count
1407 | AggregateFunction::Min
1408 | AggregateFunction::Max
1409 | AggregateFunction::Avg
1410 | AggregateFunction::Collect,
1411 AggregateTarget::Property {
1412 variable: current,
1413 property,
1414 },
1415 ) if current == variable => {
1416 required.insert(property.clone());
1417 true
1418 }
1419 _ => false,
1420 }
1421}
1422
1423fn node_projection_access(plan: &PhysicalPlan) -> Option<(&String, &String, NodeProjectionAccess)> {
1424 match plan {
1425 PhysicalPlan::SeqNodeScan { variable, label } => {
1426 Some((variable, label, NodeProjectionAccess::LabelScan))
1427 }
1428 PhysicalPlan::IndexNodeSeek {
1429 variable,
1430 label,
1431 property,
1432 value,
1433 } => Some((
1434 variable,
1435 label,
1436 NodeProjectionAccess::PropertyValues {
1437 property: property.clone(),
1438 values: vec![value.clone()],
1439 },
1440 )),
1441 PhysicalPlan::IndexNodeMultiSeek {
1442 variable,
1443 label,
1444 property,
1445 values,
1446 } => Some((
1447 variable,
1448 label,
1449 NodeProjectionAccess::PropertyValues {
1450 property: property.clone(),
1451 values: values.clone(),
1452 },
1453 )),
1454 PhysicalPlan::IndexNodeUnionSeek {
1455 variable,
1456 label,
1457 branches,
1458 } => Some((
1459 variable,
1460 label,
1461 NodeProjectionAccess::PropertyUnion {
1462 branches: branches.clone(),
1463 },
1464 )),
1465 PhysicalPlan::IndexNodeCompositeSeek {
1466 variable,
1467 label,
1468 predicates,
1469 } => Some((
1470 variable,
1471 label,
1472 NodeProjectionAccess::CompositeEquality {
1473 predicates: predicates.clone(),
1474 },
1475 )),
1476 PhysicalPlan::IndexNodeCompositeRangeSeek {
1477 variable,
1478 label,
1479 seek,
1480 } => Some((
1481 variable,
1482 label,
1483 NodeProjectionAccess::CompositeRange { seek: seek.clone() },
1484 )),
1485 PhysicalPlan::IndexNodeRangeSeek {
1486 variable,
1487 label,
1488 property,
1489 lower,
1490 upper,
1491 } => Some((
1492 variable,
1493 label,
1494 NodeProjectionAccess::PropertyRange {
1495 property: property.clone(),
1496 lower: lower.clone(),
1497 upper: upper.clone(),
1498 },
1499 )),
1500 PhysicalPlan::IndexNodeTextSeek {
1501 variable,
1502 label,
1503 property,
1504 query,
1505 } => Some((
1506 variable,
1507 label,
1508 NodeProjectionAccess::FullText {
1509 property: property.clone(),
1510 query: query.clone(),
1511 },
1512 )),
1513 _ => None,
1514 }
1515}
1516
1517fn collect_access_properties(access: &NodeProjectionAccess, required: &mut BTreeSet<String>) {
1518 match access {
1519 NodeProjectionAccess::LabelScan => {}
1520 NodeProjectionAccess::PropertyValues { property, .. }
1521 | NodeProjectionAccess::PropertyRange { property, .. }
1522 | NodeProjectionAccess::FullText { property, .. } => {
1523 required.insert(property.clone());
1524 }
1525 NodeProjectionAccess::CompositeEquality { predicates } => {
1526 required.extend(predicates.iter().map(|(property, _)| property.clone()));
1527 }
1528 NodeProjectionAccess::CompositeRange { seek } => {
1529 required.extend(
1530 seek.equality_prefix
1531 .iter()
1532 .map(|(property, _)| property.clone()),
1533 );
1534 required.insert(seek.range_property.clone());
1535 }
1536 NodeProjectionAccess::PropertyUnion { branches } => {
1537 required.extend(branches.iter().map(|branch| branch.property.clone()));
1538 }
1539 }
1540}
1541
1542fn collect_predicate_properties(
1543 predicate: &Predicate,
1544 variable: &str,
1545 required: &mut BTreeSet<String>,
1546) -> bool {
1547 match predicate {
1548 Predicate::And(predicates) | Predicate::Or(predicates) => predicates
1549 .iter()
1550 .all(|predicate| collect_predicate_properties(predicate, variable, required)),
1551 Predicate::Not(predicate) => collect_predicate_properties(predicate, variable, required),
1552 Predicate::ConstantBool(_) => true,
1553 Predicate::IdEq {
1554 variable: current, ..
1555 }
1556 | Predicate::IdNotEq {
1557 variable: current, ..
1558 }
1559 | Predicate::IdCompare {
1560 variable: current, ..
1561 }
1562 | Predicate::IdIn {
1563 variable: current, ..
1564 } => current == variable,
1565 Predicate::PropertyEq {
1566 variable: current,
1567 property,
1568 ..
1569 }
1570 | Predicate::PropertyNotEq {
1571 variable: current,
1572 property,
1573 ..
1574 }
1575 | Predicate::PropertyCompare {
1576 variable: current,
1577 property,
1578 ..
1579 }
1580 | Predicate::PropertyListContains {
1581 variable: current,
1582 property,
1583 ..
1584 }
1585 | Predicate::PropertyListContainsLower {
1586 variable: current,
1587 property,
1588 ..
1589 }
1590 | Predicate::PropertyContains {
1591 variable: current,
1592 property,
1593 ..
1594 }
1595 | Predicate::PropertyStartsWith {
1596 variable: current,
1597 property,
1598 ..
1599 }
1600 | Predicate::PropertyEndsWith {
1601 variable: current,
1602 property,
1603 ..
1604 }
1605 | Predicate::PropertyRegexMatch {
1606 variable: current,
1607 property,
1608 ..
1609 }
1610 | Predicate::PropertyIsNull {
1611 variable: current,
1612 property,
1613 }
1614 | Predicate::PropertyIsNotNull {
1615 variable: current,
1616 property,
1617 }
1618 | Predicate::PropertyIn {
1619 variable: current,
1620 property,
1621 ..
1622 } => {
1623 if current != variable {
1624 return false;
1625 }
1626 required.insert(property.clone());
1627 true
1628 }
1629 Predicate::ExpressionEq { expression, value }
1630 | Predicate::ExpressionNotEq { expression, value }
1631 | Predicate::ExpressionContains { expression, value } => {
1632 collect_projection_properties(expression, variable, required)
1633 && collect_projection_properties(value, variable, required)
1634 }
1635 Predicate::ExpressionIn { expression, .. } => {
1636 collect_projection_properties(expression, variable, required)
1637 }
1638 Predicate::ExpressionCompare {
1639 expression, value, ..
1640 } => {
1641 collect_projection_properties(expression, variable, required)
1642 && collect_projection_properties(value, variable, required)
1643 }
1644 Predicate::RelationshipExists { .. } | Predicate::BoundRelationshipExists { .. } => false,
1645 }
1646}
1647
1648fn collect_projection_properties(
1649 expression: &ProjectionExpression,
1650 variable: &str,
1651 required: &mut BTreeSet<String>,
1652) -> bool {
1653 match expression {
1654 ProjectionExpression::Case { .. }
1655 | ProjectionExpression::Binary { .. }
1656 | ProjectionExpression::Not(_)
1657 | ProjectionExpression::IsNull { .. } => expression
1658 .all_children(|child| collect_projection_properties(child, variable, required)),
1659 ProjectionExpression::Variable { .. }
1660 | ProjectionExpression::RelationshipType { .. }
1661 | ProjectionExpression::Column(_)
1662 | ProjectionExpression::ColumnProperty { .. }
1663 | ProjectionExpression::ColumnDefaultIfNullOrEq { .. }
1664 | ProjectionExpression::ColumnValueDefaultIfNull { .. }
1665 | ProjectionExpression::ColumnValueCasePropertyNotNullOrEq { .. }
1666 | ProjectionExpression::CaseColumnSearchRank(_) => false,
1667 ProjectionExpression::Id { variable: current } => current == variable,
1668 ProjectionExpression::Literal(_) => true,
1669 ProjectionExpression::Property {
1670 variable: current,
1671 property,
1672 }
1673 | ProjectionExpression::DatePart {
1674 variable: current,
1675 property,
1676 ..
1677 }
1678 | ProjectionExpression::DefaultIfNullOrEq {
1679 variable: current,
1680 property,
1681 ..
1682 }
1683 | ProjectionExpression::DefaultIfNull {
1684 variable: current,
1685 property,
1686 ..
1687 }
1688 | ProjectionExpression::CasePropertyNotNullOrEq {
1689 variable: current,
1690 property,
1691 ..
1692 }
1693 | ProjectionExpression::CasePropertyEqualsRank {
1694 variable: current,
1695 property,
1696 ..
1697 }
1698 | ProjectionExpression::CaseLowerPropertyDefault {
1699 variable: current,
1700 property,
1701 ..
1702 } => {
1703 if current != variable {
1704 return false;
1705 }
1706 required.insert(property.clone());
1707 true
1708 }
1709 ProjectionExpression::Coalesce(expressions) => expressions
1710 .iter()
1711 .all(|expression| collect_projection_properties(expression, variable, required)),
1712 ProjectionExpression::Left { expression, .. }
1713 | ProjectionExpression::Lower(expression)
1714 | ProjectionExpression::JsonValue { expression, .. } => {
1715 collect_projection_properties(expression, variable, required)
1716 }
1717 ProjectionExpression::CaseCoalesceDifferenceFloorZero {
1718 variable: current,
1719 terms,
1720 } => {
1721 if current != variable {
1722 return false;
1723 }
1724 required.extend(terms.iter().map(|term| term.property.clone()));
1725 true
1726 }
1727 ProjectionExpression::CaseEntitySearchRank(rank) => {
1728 if rank.variable != variable {
1729 return false;
1730 }
1731 required.insert(rank.name_property.clone());
1732 required.insert(rank.aliases_property.clone());
1733 true
1734 }
1735 }
1736}
1737
1738struct ExpandEstimateRequest<'a> {
1739 source_label: &'a str,
1740 rel_type: &'a str,
1741 rel_properties: &'a BTreeMap<String, Value>,
1742 target_label: &'a str,
1743 min_hops: usize,
1744 max_hops: usize,
1745}
1746
1747fn push_expand_estimate_decision(
1748 catalog: &OptimizerCatalog,
1749 decisions: &mut Vec<String>,
1750 request: ExpandEstimateRequest<'_>,
1751) {
1752 let estimate = catalog.estimate_expand_rows(
1753 request.source_label,
1754 request.rel_type,
1755 request.rel_properties,
1756 request.target_label,
1757 request.min_hops,
1758 request.max_hops,
1759 );
1760 let path_count = estimate
1761 .path_count
1762 .map(|count| count.to_string())
1763 .unwrap_or_else(|| "unknown".to_string());
1764 let hop_rows = estimate
1765 .hop_estimates
1766 .iter()
1767 .map(|estimate| {
1768 format!(
1769 "{}:{}:{}",
1770 estimate.hop,
1771 if estimate.exact { "exact" } else { "fallback" },
1772 estimate.rows
1773 )
1774 })
1775 .collect::<Vec<_>>()
1776 .join(",");
1777 decisions.push(format!(
1778 "estimate AdjacencyExpand for {}-[:{}*{}..{}]->{}: path_count={path_count} rel_count={} rel_type_sources={} average_fanout={} rel_property_distinct_product={} hop_rows=[{}] estimated_rows={}",
1779 request.source_label,
1780 request.rel_type,
1781 request.min_hops,
1782 request.max_hops,
1783 request.target_label,
1784 estimate.rel_count,
1785 estimate.source_count,
1786 estimate.average_fanout,
1787 estimate.property_distinct_product,
1788 hop_rows,
1789 estimate.estimated_rows
1790 ));
1791}
1792
1793fn order_single_row_cartesian_product_children(
1794 catalog: &OptimizerCatalog,
1795 decisions: &mut Vec<String>,
1796 left: PhysicalPlan,
1797 right: PhysicalPlan,
1798) -> (PhysicalPlan, PhysicalPlan) {
1799 let left_cost = estimate_physical_plan_cost(&left, catalog);
1800 let right_cost = estimate_physical_plan_cost(&right, catalog);
1801 if !cartesian_product_leaves_are_single_row(catalog, &left)
1802 || !cartesian_product_leaves_are_single_row(catalog, &right)
1803 {
1804 decisions.push(format!(
1805 "keep NodeCartesianProduct input order: left_rows={} right_rows={} reason=non_single_row_input",
1806 left_cost.estimated_rows, right_cost.estimated_rows
1807 ));
1808 return (left, right);
1809 }
1810
1811 let mut leaves = Vec::new();
1812 collect_cartesian_product_leaves(left, &mut leaves);
1813 collect_cartesian_product_leaves(right, &mut leaves);
1814
1815 let mut keyed_leaves = leaves
1816 .into_iter()
1817 .map(|plan| {
1818 let cost = estimate_physical_plan_cost(&plan, catalog);
1819 let fingerprint = plan.instance_fingerprint();
1820 (cost, fingerprint, plan)
1821 })
1822 .collect::<Vec<_>>();
1823
1824 let original_order = keyed_leaves
1825 .iter()
1826 .map(|(cost, fingerprint, _)| format!("{}:{}", cost.cost, fingerprint))
1827 .collect::<Vec<_>>()
1828 .join("|");
1829 keyed_leaves.sort_by(
1830 |(left_cost, left_fingerprint, _), (right_cost, right_fingerprint, _)| {
1831 (left_cost.cost, left_fingerprint).cmp(&(right_cost.cost, right_fingerprint))
1832 },
1833 );
1834 let ordered = keyed_leaves
1835 .iter()
1836 .map(|(cost, fingerprint, _)| format!("{}:{}", cost.cost, fingerprint))
1837 .collect::<Vec<_>>()
1838 .join("|");
1839 if original_order == ordered {
1840 decisions.push(format!(
1841 "keep NodeCartesianProduct single-row input order: inputs={} order={ordered}",
1842 keyed_leaves.len()
1843 ));
1844 } else {
1845 decisions.push(format!(
1846 "order NodeCartesianProduct single-row inputs: inputs={} original={original_order} ordered={ordered}",
1847 keyed_leaves.len()
1848 ));
1849 }
1850
1851 let mut ordered_plans = keyed_leaves.into_iter().map(|(_, _, plan)| plan);
1852 let left = ordered_plans
1853 .next()
1854 .expect("cartesian product has left input");
1855 let right = rebuild_cartesian_product(ordered_plans);
1856 (left, right)
1857}
1858
1859fn cartesian_product_leaves_are_single_row(
1860 catalog: &OptimizerCatalog,
1861 plan: &PhysicalPlan,
1862) -> bool {
1863 match plan {
1864 PhysicalPlan::NodeCartesianProductExec { left, right } => {
1865 cartesian_product_leaves_are_single_row(catalog, left)
1866 && cartesian_product_leaves_are_single_row(catalog, right)
1867 }
1868 plan => estimate_physical_plan_cost(plan, catalog).estimated_rows == 1,
1869 }
1870}
1871
1872fn collect_cartesian_product_leaves(plan: PhysicalPlan, leaves: &mut Vec<PhysicalPlan>) {
1873 match plan {
1874 PhysicalPlan::NodeCartesianProductExec { left, right } => {
1875 collect_cartesian_product_leaves(*left, leaves);
1876 collect_cartesian_product_leaves(*right, leaves);
1877 }
1878 plan => leaves.push(plan),
1879 }
1880}
1881
1882fn rebuild_cartesian_product(plans: impl IntoIterator<Item = PhysicalPlan>) -> PhysicalPlan {
1883 let mut plans = plans.into_iter();
1884 let first = plans
1885 .next()
1886 .expect("cartesian product rebuild requires at least one input");
1887 plans.fold(first, |left, right| {
1888 PhysicalPlan::NodeCartesianProductExec {
1889 left: Box::new(left),
1890 right: Box::new(right),
1891 }
1892 })
1893}
1894
1895fn push_cartesian_product_cost_decision(
1896 catalog: &OptimizerCatalog,
1897 decisions: &mut Vec<String>,
1898 left: &PhysicalPlan,
1899 right: &PhysicalPlan,
1900) {
1901 let left_cost = estimate_physical_plan_cost(left, catalog);
1902 let right_cost = estimate_physical_plan_cost(right, catalog);
1903 let cost = estimate_node_cartesian_product_cost(left_cost, right_cost);
1904 decisions.push(format!(
1905 "estimate NodeCartesianProduct: left_rows={} right_rows={} output_rows={} left_cost={} right_cost={} cost={}",
1906 left_cost.estimated_rows,
1907 right_cost.estimated_rows,
1908 cost.estimated_rows,
1909 left_cost.cost,
1910 right_cost.cost,
1911 cost.cost
1912 ));
1913}