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