1use crate::planning::plan::core::nodes::access::index_scan::IndexScanNode;
10use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
11use crate::planning::plan::logical::logical_nodes::access::{
12 IndexHint, LogicalScanEdgesNode, LogicalScanVerticesNode,
13};
14use crate::planning::plan::logical::LogicalNodeEnum;
15use crate::planning::plan::PlanNodeEnum;
16
17pub struct PhysicalMapper;
23
24impl PhysicalMapper {
25 pub fn map(logical: LogicalNodeEnum) -> PlanNodeEnum {
30 let hints = collect_index_hints(&logical);
34 let physical = crate::planning::physical_planner::convert_logical_to_physical(logical);
35 apply_collected_hints(physical, &hints)
36 }
37
38 pub(crate) fn needs_physical_mapping(logical: &LogicalNodeEnum) -> bool {
44 if matches!(logical, LogicalNodeEnum::Flatten(_)) {
45 return true;
46 }
47 if let LogicalNodeEnum::ScanVertices(scan) = logical {
48 if scan.index_hint.is_some() {
49 return true;
50 }
51 }
52 if let LogicalNodeEnum::ScanEdges(scan) = logical {
53 if scan.index_hint.is_some() {
54 return true;
55 }
56 }
57 if let LogicalNodeEnum::GetNeighbors(node) = logical {
58 if node.index_hint.is_some() {
59 return true;
60 }
61 }
62 logical_children(logical)
63 .iter()
64 .any(|child| Self::needs_physical_mapping(child))
65 }
66
67 pub fn merge_physical_hints(
75 mapped: PlanNodeEnum,
76 physical: PlanNodeEnum,
77 ) -> (PlanNodeEnum, Vec<String>) {
78 let mut notes = Vec::new();
79 let mut mapped = mapped;
80 merge_inner(&mut mapped, physical, &mut notes);
81 (mapped, notes)
82 }
83
84 pub(crate) fn collect_flatten_positions(node: &LogicalNodeEnum, out: &mut Vec<u32>) {
86 if let LogicalNodeEnum::Flatten(flatten) = node {
87 out.push(flatten.group_pos);
88 }
89 for child in logical_children(node) {
90 Self::collect_flatten_positions(child, out);
91 }
92 }
93}
94
95struct IndexHintTarget {
101 path: Vec<usize>,
102 scan: HintedScan,
103}
104
105enum HintedScan {
107 Vertices(LogicalScanVerticesNode),
108 Edges(LogicalScanEdgesNode),
109}
110
111fn collect_index_hints(logical: &LogicalNodeEnum) -> Vec<IndexHintTarget> {
113 fn walk(node: &LogicalNodeEnum, path: &mut Vec<usize>, out: &mut Vec<IndexHintTarget>) {
114 match node {
115 LogicalNodeEnum::ScanVertices(scan) if scan.index_hint.is_some() => {
116 out.push(IndexHintTarget {
117 path: path.clone(),
118 scan: HintedScan::Vertices(scan.clone()),
119 });
120 }
121 LogicalNodeEnum::ScanEdges(scan) if scan.index_hint.is_some() => {
122 out.push(IndexHintTarget {
123 path: path.clone(),
124 scan: HintedScan::Edges(scan.clone()),
125 });
126 }
127 _ => {}
128 }
129 for (index, child) in logical_children(node).iter().enumerate() {
130 path.push(index);
131 walk(child, path, out);
132 path.pop();
133 }
134 }
135
136 let mut out = Vec::new();
137 walk(logical, &mut Vec::new(), &mut out);
138 out
139}
140
141fn apply_collected_hints(mut physical: PlanNodeEnum, hints: &[IndexHintTarget]) -> PlanNodeEnum {
147 for hint in hints {
148 let Some(target) = descend_to_path(&mut physical, &hint.path) else {
149 continue;
150 };
151 let replacement = match (&hint.scan, &*target) {
152 (HintedScan::Vertices(scan), PlanNodeEnum::ScanVertices(_)) => scan
153 .index_hint
154 .as_ref()
155 .map(|hint| index_scan_from_hint(scan, hint)),
156 (HintedScan::Edges(scan), PlanNodeEnum::ScanEdges(_)) => scan
157 .index_hint
158 .as_ref()
159 .map(|hint| index_scan_from_edge_hint(scan, hint)),
160 _ => None,
161 };
162 if let Some(node) = replacement {
163 *target = node;
164 }
165 }
166 physical
167}
168
169fn descend_to_path<'a>(
174 mut node: &'a mut PlanNodeEnum,
175 path: &[usize],
176) -> Option<&'a mut PlanNodeEnum> {
177 use crate::optimizer::cost::child_accessor::ChildAccessor;
178
179 for &index in path {
180 node = node.get_child_mut(index)?;
181 }
182 Some(node)
183}
184
185fn index_scan_from_hint(scan: &LogicalScanVerticesNode, hint: &IndexHint) -> PlanNodeEnum {
191 let mut node = IndexScanNode::new_with_str(
192 scan.space_id,
193 hint.tag_id,
194 hint.index_id,
195 &hint.index_name,
196 &hint.schema_name,
197 &hint.scan_type,
198 );
199 if let Some(expression) = &scan.expression {
200 node.set_filter(expression.clone());
201 }
202 node.set_return_columns(scan.projected_properties.clone());
203 if let Some(limit) = scan.limit {
204 node.set_limit(limit);
205 }
206 if let Some(output_var) = &scan.output_var {
207 node.set_output_var(output_var.clone());
208 }
209 node.set_col_names(scan.col_names.clone());
210 node.set_column_types(scan.column_types.clone());
211 PlanNodeEnum::IndexScan(node)
212}
213
214fn index_scan_from_edge_hint(scan: &LogicalScanEdgesNode, hint: &IndexHint) -> PlanNodeEnum {
219 let mut node = IndexScanNode::new_with_str(
220 scan.space_id,
221 hint.tag_id,
222 hint.index_id,
223 &hint.index_name,
224 &hint.schema_name,
225 &hint.scan_type,
226 );
227 if let Some(expression) = &scan.expression {
228 node.set_filter(expression.clone());
229 }
230 node.set_return_columns(scan.projected_properties.clone());
231 if let Some(limit) = scan.limit {
232 node.set_limit(limit);
233 }
234 if let Some(output_var) = &scan.output_var {
235 node.set_output_var(output_var.clone());
236 }
237 node.set_col_names(scan.col_names.clone());
238 node.set_column_types(scan.column_types.clone());
239 PlanNodeEnum::IndexScan(node)
240}
241
242fn merge_inner(mapped: &mut PlanNodeEnum, physical: PlanNodeEnum, notes: &mut Vec<String>) {
248 if let PlanNodeEnum::Flatten(flatten) = mapped {
252 let placeholder = PlanNodeEnum::Start(crate::planning::plan::core::nodes::StartNode::new());
253 let mut child = std::mem::replace(flatten.input_mut(), placeholder);
254 merge_inner(&mut child, physical, notes);
255 flatten.set_input(child);
256 return;
257 }
258 if let PlanNodeEnum::IndexScan(_) = &physical {
261 if matches!(
262 &*mapped,
263 PlanNodeEnum::ScanVertices(_) | PlanNodeEnum::IndexScan(_)
264 ) {
265 *mapped = physical;
266 return;
267 }
268 }
269 if let PlanNodeEnum::IndexScan(_) = &*mapped {
272 if matches!(
273 &physical,
274 PlanNodeEnum::ScanVertices(_) | PlanNodeEnum::ScanEdges(_)
275 ) {
276 return;
277 }
278 }
279 if let PlanNodeEnum::TopN(_) = &physical {
281 if matches!(
282 &*mapped,
283 PlanNodeEnum::TopN(_) | PlanNodeEnum::Sort(_) | PlanNodeEnum::Limit(_)
284 ) {
285 *mapped = physical;
286 return;
287 }
288 }
289 if std::mem::discriminant(&*mapped) == std::mem::discriminant(&physical) {
290 use crate::optimizer::cost::child_accessor::ChildAccessor;
291
292 if mapped.child_count() != physical.child_count() {
295 notes.push(format!(
296 "PhysicalMapping: child count diverged (mapped {} vs physical {}); kept physical subtree",
297 mapped.type_name(),
298 physical.type_name()
299 ));
300 *mapped = physical;
301 return;
302 }
303 let mut physical = physical;
304 let mut mapped_children = mapped.take_children();
305 let physical_children = physical.take_children();
306 let mut new_children = Vec::with_capacity(mapped_children.len());
307 for (mapped_child, physical_child) in mapped_children.drain(..).zip(physical_children) {
308 let mut merged_child = mapped_child;
309 merge_inner(&mut merged_child, physical_child, notes);
310 new_children.push(merged_child);
311 }
312 match physical.set_children(new_children) {
313 Ok(()) => {
314 *mapped = physical;
315 }
316 Err(message) => {
317 notes.push(format!(
318 "PhysicalMapping: rebuild failed for {} ({message}); kept physical subtree",
319 physical.type_name()
320 ));
321 *mapped = physical;
322 }
323 }
324 return;
325 }
326 notes.push(format!(
327 "PhysicalMapping: structure diverged (mapped {} vs physical {}); kept physical subtree",
328 mapped.type_name(),
329 physical.type_name()
330 ));
331 *mapped = physical;
332}
333
334pub(crate) fn logical_children(
335 node: &crate::planning::plan::logical::LogicalNodeEnum,
336) -> Vec<&crate::planning::plan::logical::LogicalNodeEnum> {
337 use crate::planning::plan::logical::LogicalNodeEnum;
338 match node {
339 LogicalNodeEnum::Flatten(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
340 LogicalNodeEnum::Project(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
341 LogicalNodeEnum::Filter(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
342 LogicalNodeEnum::Sort(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
343 LogicalNodeEnum::Limit(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
344 LogicalNodeEnum::Skip(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
345 LogicalNodeEnum::TopN(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
346 LogicalNodeEnum::Sample(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
347 LogicalNodeEnum::Dedup(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
348 LogicalNodeEnum::Aggregate(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
349 LogicalNodeEnum::Window(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
350 LogicalNodeEnum::Traverse(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
351 LogicalNodeEnum::Assign(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
352 LogicalNodeEnum::PipeDeleteVertices(n) => {
353 n.input.as_deref().map(|c| vec![c]).unwrap_or_default()
354 }
355 LogicalNodeEnum::PipeDeleteEdges(n) => {
356 n.input.as_deref().map(|c| vec![c]).unwrap_or_default()
357 }
358 LogicalNodeEnum::DataCollect(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
359 LogicalNodeEnum::Materialize(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
360 LogicalNodeEnum::RollUpApply(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
361 LogicalNodeEnum::Unwind(n) => n.input.as_deref().map(|c| vec![c]).unwrap_or_default(),
362 LogicalNodeEnum::Select(n) => {
363 let mut v = Vec::new();
364 if let Some(b) = n.if_branch() {
365 v.push(b);
366 }
367 if let Some(b) = n.else_branch() {
368 v.push(b);
369 }
370 v
371 }
372 LogicalNodeEnum::Loop(n) => n.body().map(|b| vec![b]).unwrap_or_default(),
373 LogicalNodeEnum::InnerJoin(n) => vec![&n.left, &n.right],
374 LogicalNodeEnum::LeftJoin(n) => vec![&n.left, &n.right],
375 LogicalNodeEnum::RightJoin(n) => vec![&n.left, &n.right],
376 LogicalNodeEnum::CrossJoin(n) => vec![&n.left, &n.right],
377 LogicalNodeEnum::FullOuterJoin(n) => vec![&n.left, &n.right],
378 LogicalNodeEnum::SemiJoin(n) => vec![&n.left, &n.right],
379 LogicalNodeEnum::PatternApply(n) => vec![&n.left, &n.right],
380 LogicalNodeEnum::CorrelatedApply(n) => vec![&n.left, &n.right],
381 LogicalNodeEnum::Apply(n) => vec![n.left_input(), n.right_input()],
382 LogicalNodeEnum::BiExpand(n) => vec![&n.left, &n.right],
383 LogicalNodeEnum::BiTraverse(n) => vec![&n.left, &n.right],
384 LogicalNodeEnum::MultiShortestPath(n) => vec![&n.left, &n.right],
385 LogicalNodeEnum::BFSShortest(n) => vec![&n.left, &n.right],
386 LogicalNodeEnum::AllPaths(n) => vec![&n.left, &n.right],
387 LogicalNodeEnum::ShortestPath(n) => vec![&n.left, &n.right],
388 LogicalNodeEnum::Expand(n) => n.deps.iter().collect(),
389 LogicalNodeEnum::ExpandAll(n) => n.deps.iter().collect(),
390 LogicalNodeEnum::AppendVertices(n) => n.deps.iter().collect(),
391 LogicalNodeEnum::GetVertices(n) => n.deps.iter().collect(),
392 LogicalNodeEnum::GetNeighbors(n) => n.deps.iter().collect(),
393 LogicalNodeEnum::Union(n) => n.deps.iter().collect(),
394 LogicalNodeEnum::Minus(n) => n.deps.iter().collect(),
395 LogicalNodeEnum::Intersect(n) => n.deps.iter().collect(),
396 LogicalNodeEnum::WcoIntersect(n) => n.deps.iter().collect(),
397 LogicalNodeEnum::Start(_)
398 | LogicalNodeEnum::ScanVertices(_)
399 | LogicalNodeEnum::ScanEdges(_)
400 | LogicalNodeEnum::GetEdges(_)
401 | LogicalNodeEnum::Argument(_)
402 | LogicalNodeEnum::PassThrough(_)
403 | LogicalNodeEnum::BeginTransaction(_)
404 | LogicalNodeEnum::Commit(_)
405 | LogicalNodeEnum::Rollback(_)
406 | LogicalNodeEnum::InsertVertices(_)
407 | LogicalNodeEnum::InsertEdges(_)
408 | LogicalNodeEnum::Update(_)
409 | LogicalNodeEnum::DeleteVertices(_)
410 | LogicalNodeEnum::DeleteEdges(_)
411 | LogicalNodeEnum::DeleteIndex(_)
412 | LogicalNodeEnum::CopyFrom(_)
413 | LogicalNodeEnum::CopyTo(_)
414 | LogicalNodeEnum::FulltextSearch(_)
415 | LogicalNodeEnum::FulltextLookup(_)
416 | LogicalNodeEnum::MatchFulltext(_) => vec![],
417 #[cfg(feature = "vector")]
418 LogicalNodeEnum::VectorSearch(_)
419 | LogicalNodeEnum::VectorLookup(_)
420 | LogicalNodeEnum::VectorMatch(_) => vec![],
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::planning::plan::core::nodes::access::graph_scan_node::ScanVerticesNode;
428 use crate::planning::plan::core::nodes::access::index_scan::IndexScanNode;
429 use crate::planning::plan::core::nodes::base::plan_node_traits::SingleInputNode;
430 use crate::planning::plan::core::nodes::operation::filter_node::FilterNode;
431 use crate::planning::plan::core::nodes::operation::flatten_node::FlattenNode;
432 use crate::planning::plan::logical::logical_nodes::flatten::LogicalFlattenNode;
433 use crate::planning::plan::logical::logical_nodes::operation::LogicalFilterNode;
434 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
435 use linkrs_core::types::expr::Expression;
436 use linkrs_core::types::expr::{ContextualExpression, ExpressionMeta};
437 use linkrs_core::types::operators::BinaryOperator;
438 use linkrs_core::value::Value;
439 use std::sync::Arc;
440
441 fn hinted_scan() -> LogicalNodeEnum {
442 LogicalNodeEnum::ScanVertices(LogicalScanVerticesNode {
443 id: 1,
444 space_id: 1,
445 space_name: "test".to_string(),
446 tag: Some("person".to_string()),
447 expression: None,
448 limit: None,
449 projected_properties: vec![],
450 index_hint: Some(IndexHint::new(
451 "idx_name".to_string(),
452 "person".to_string(),
453 7,
454 9,
455 "RANGE".to_string(),
456 )),
457 estimated_cardinality: Some(42),
458 output_var: Some("n".to_string()),
459 col_names: vec!["n".to_string()],
460 column_types: vec![],
461 })
462 }
463
464 fn filter_over(logical_input: LogicalNodeEnum) -> LogicalNodeEnum {
465 let context = Arc::new(ExpressionAnalysisContext::new());
466 let expression = Expression::Binary {
467 left: Box::new(Expression::Property {
468 object: Box::new(Expression::Variable("n".to_string())),
469 property: "name".to_string(),
470 }),
471 op: BinaryOperator::Equal,
472 right: Box::new(Expression::Literal(Value::String("alice".into()))),
473 };
474 let id = context.register_expression(ExpressionMeta::new(expression));
475 LogicalNodeEnum::Filter(LogicalFilterNode {
476 id: 2,
477 input: Some(Box::new(logical_input)),
478 condition: ContextualExpression::new(id, context),
479 output_var: None,
480 col_names: vec![],
481 column_types: vec![],
482 })
483 }
484
485 #[test]
486 fn map_hinted_scan_becomes_index_scan() {
487 let mapped = PhysicalMapper::map(filter_over(hinted_scan()));
488 let filter = match &mapped {
489 PlanNodeEnum::Filter(f) => f,
490 other => panic!("expected physical filter, got {}", other.type_name()),
491 };
492 match filter.input() {
493 PlanNodeEnum::IndexScan(scan) => {
494 assert_eq!(scan.index_name(), "idx_name");
495 assert_eq!(scan.tag_id(), 7);
496 assert_eq!(scan.index_id(), 9);
497 }
498 other => panic!("expected index scan, got {}", other.type_name()),
499 }
500 }
501
502 #[test]
503 fn map_flatten_preserves_group_column_mapping() {
504 let mut flatten = LogicalFlattenNode::new(2, hinted_scan());
505 flatten.set_group_columns(vec!["b".to_string()]);
506 flatten.set_expected_groups(3);
507 let mapped = PhysicalMapper::map(LogicalNodeEnum::Flatten(flatten));
508 match &mapped {
509 PlanNodeEnum::Flatten(f) => {
510 assert_eq!(f.group_pos(), 2);
511 assert_eq!(f.group_columns(), &["b".to_string()]);
512 assert_eq!(f.expected_groups(), Some(3));
513 }
514 other => panic!("expected physical flatten, got {}", other.type_name()),
515 }
516 }
517
518 #[test]
519 fn map_hinted_scan_under_flatten_becomes_index_scan() {
520 let mapped = PhysicalMapper::map(LogicalNodeEnum::Flatten(LogicalFlattenNode::new(
521 0,
522 hinted_scan(),
523 )));
524 let flatten = match &mapped {
525 PlanNodeEnum::Flatten(f) => f,
526 other => panic!("expected physical flatten, got {}", other.type_name()),
527 };
528 match flatten.input() {
529 PlanNodeEnum::IndexScan(scan) => {
530 assert_eq!(scan.index_name(), "idx_name");
531 }
532 other => panic!("expected index scan, got {}", other.type_name()),
533 }
534 }
535
536 #[test]
537 fn merge_preserves_flatten_and_physical_index_scan() {
538 let mapped = PhysicalMapper::map(LogicalNodeEnum::Flatten(LogicalFlattenNode::new(
539 0,
540 hinted_scan(),
541 )));
542 let mut physical_scan = IndexScanNode::new(
543 1,
544 7,
545 9,
546 "idx_name".to_string(),
547 "person".to_string(),
548 crate::planning::plan::core::nodes::access::index_scan::ScanType::Range,
549 );
550 physical_scan.set_col_names(vec!["n".to_string()]);
551 let physical = PlanNodeEnum::IndexScan(physical_scan);
552 let (merged, notes) = PhysicalMapper::merge_physical_hints(mapped, physical);
553 assert!(notes.is_empty());
554 let flatten = match &merged {
555 PlanNodeEnum::Flatten(f) => f,
556 other => panic!("expected flatten, got {}", other.type_name()),
557 };
558 assert!(matches!(flatten.input(), PlanNodeEnum::IndexScan(_)));
559 }
560
561 #[test]
562 fn merge_divergence_keeps_physical_with_note() {
563 let mapped = PhysicalMapper::map(hinted_scan());
564 let mut scan = ScanVerticesNode::new(1, "test");
565 scan.set_col_names(vec!["n".to_string()]);
566 let context = Arc::new(ExpressionAnalysisContext::new());
567 let id =
568 context.register_expression(ExpressionMeta::new(Expression::Literal(Value::Int(1))));
569 let physical = PlanNodeEnum::Filter(
570 FilterNode::new(
571 PlanNodeEnum::ScanVertices(scan),
572 ContextualExpression::new(id, context),
573 )
574 .expect("filter"),
575 );
576 let (merged, notes) = PhysicalMapper::merge_physical_hints(mapped, physical);
577 assert!(matches!(merged, PlanNodeEnum::Filter(_)));
578 assert_eq!(notes.len(), 1);
579 assert!(notes[0].contains("diverged"));
580 }
581
582 #[test]
583 fn unmapped_tree_needs_no_mapping() {
584 let mut scan = hinted_scan();
585 if let LogicalNodeEnum::ScanVertices(s) = &mut scan {
586 s.index_hint = None;
587 s.estimated_cardinality = None;
588 }
589 assert!(!PhysicalMapper::needs_physical_mapping(&scan));
590 assert!(PhysicalMapper::needs_physical_mapping(&hinted_scan()));
591 }
592
593 #[test]
594 fn flatten_take_set_roundtrip() {
595 let mut scan = ScanVerticesNode::new(1, "test");
596 scan.set_col_names(vec!["n".to_string()]);
597 let child = PlanNodeEnum::ScanVertices(scan);
598 let flatten = FlattenNode::new(child, 0).expect("flatten");
599 let mut node = PlanNodeEnum::Flatten(flatten);
600 let taken = node.take_children();
601 assert_eq!(taken.len(), 1);
602 node.set_children(vec![PlanNodeEnum::ScanVertices({
603 let mut s = ScanVerticesNode::new(1, "test");
604 s.set_col_names(vec!["n".to_string()]);
605 s
606 })])
607 .expect("set");
608 assert!(matches!(node, PlanNodeEnum::Flatten(_)));
609 }
610}