1use crate::binder::BoundStatement;
12use crate::parser::ast::Stmt;
13use crate::planning::plan::core::next_node_id;
14use crate::planning::plan::core::nodes::traversal::path_algorithms::PathUniqueness;
15use crate::planning::plan::core::nodes::traversal::{AllPathsNode, ShortestPathNode};
16use crate::planning::plan::core::PlanNode;
17use crate::planning::plan::logical::logical_nodes::access::LogicalStartNode;
18use crate::planning::plan::logical::logical_nodes::algorithm::{
19 LogicalAllPathsNode, LogicalShortestPathNode,
20};
21use crate::planning::plan::logical::LogicalNodeEnum;
22use crate::planning::plan::SubPlan;
23use crate::planning::planner::{Planner, PlannerError, ValidatedStatement};
24use crate::planning::statements::plan_combiner::wrap_logical_filter;
25use crate::QueryContext;
26use linkrs_core::types::VertexId;
27use linkrs_core::Value;
28use std::sync::Arc;
29
30pub use crate::planning::plan::core::nodes::{
31 ArgumentNode, DedupNode, ExpandAllNode, FilterNode, GetNeighborsNode, ProjectNode, StartNode,
32};
33pub use crate::planning::plan::core::PlanNodeEnum;
34
35#[derive(Debug, Clone)]
38pub struct PathPlanner {}
39
40impl PathPlanner {
41 pub fn new() -> Self {
43 Self {}
44 }
45}
46
47impl Planner for PathPlanner {
48 fn transform(
49 &mut self,
50 validated: &ValidatedStatement,
51 qctx: Arc<QueryContext>,
52 ) -> Result<SubPlan, PlannerError> {
53 let find_path_stmt = match validated.stmt() {
54 Stmt::FindPath(find_path_stmt) => find_path_stmt,
55 _ => {
56 return Err(PlannerError::InvalidOperation(
57 "PathPlanner requires the FindPath statement.".to_string(),
58 ));
59 }
60 };
61
62 let space_id = qctx.space_id().ok_or_else(|| {
63 PlannerError::InvalidOperation(
64 "No graph space selected, please execute USE <space> first".to_string(),
65 )
66 })?;
67
68 let start_node = StartNode::new();
73 let start_node_enum = PlanNodeEnum::Start(start_node);
74
75 let edge_types = self.get_edge_types_from_stmt(find_path_stmt);
76 let max_steps = self.get_max_steps_from_stmt(find_path_stmt);
77
78 let start_vertex_ids = self.extract_vertex_ids_from_exprs(&find_path_stmt.from.vertices);
79 let end_vertex_ids = self.extract_vertex_ids_from_expr(&find_path_stmt.to);
80
81 let (mut root_node, mut logical_root) = if self.is_shortest_path_stmt(find_path_stmt) {
82 if find_path_stmt.with_loop || find_path_stmt.with_trail || find_path_stmt.with_walk {
86 log::debug!(
87 "FIND SHORTEST PATH ignores WITH LOOP/TRAIL/WALK: \
88 revisiting a vertex or edge cannot yield a shorter path"
89 );
90 }
91 let root = self.build_shortest_path_plan(PathPlanParams {
92 left_input: start_node_enum.clone(),
93 space_id,
94 edge_types: edge_types.clone(),
95 max_steps,
96 start_vertex_ids: start_vertex_ids.clone(),
97 end_vertex_ids: end_vertex_ids.clone(),
98 weight_expression: find_path_stmt.weight_expression.clone(),
99 })?;
100 let logical = Self::shortest_path_logical(
101 space_id,
102 edge_types,
103 max_steps,
104 start_vertex_ids,
105 end_vertex_ids,
106 find_path_stmt.weight_expression.clone(),
107 );
108 (root, logical)
109 } else {
110 let root = self.build_all_paths_plan(
111 PathPlanParams {
112 left_input: start_node_enum.clone(),
113 space_id,
114 edge_types: edge_types.clone(),
115 max_steps,
116 start_vertex_ids: start_vertex_ids.clone(),
117 end_vertex_ids: end_vertex_ids.clone(),
118 weight_expression: None,
120 },
121 find_path_stmt,
122 )?;
123 let logical = Self::all_paths_logical(
124 space_id,
125 edge_types,
126 max_steps,
127 find_path_stmt,
128 start_vertex_ids,
129 end_vertex_ids,
130 );
131 (root, logical)
132 };
133
134 if let Some(where_expr) = find_path_stmt.where_clause.clone() {
136 let filter_node =
137 FilterNode::new(root_node.clone(), where_expr.clone()).map_err(|e| {
138 PlannerError::PlanGenerationFailed(format!(
139 "Failed to create FilterNode: {}",
140 e
141 ))
142 })?;
143 let col_names = filter_node.col_names().to_vec();
144 logical_root = wrap_logical_filter(logical_root, where_expr.clone(), col_names);
145 root_node = PlanNodeEnum::Filter(filter_node);
146 }
147
148 let sub_plan = SubPlan {
149 root: Some(root_node),
150 tail: Some(start_node_enum),
151 logical_root: Some(logical_root),
152 };
153
154 Ok(sub_plan)
155 }
156
157 fn plan_bound(
158 &mut self,
159 ctx: &crate::planning::context::PlanContext<'_>,
160 ) -> Result<SubPlan, PlannerError> {
161 let bound = ctx.bound;
162 let qctx = ctx.qctx.clone();
163 let metadata = ctx.metadata;
164 let validated = ctx.validated;
165 let _ = (&bound, &qctx, &metadata, &validated);
166 let find_path = match bound {
167 BoundStatement::FindPath(fp) => fp,
168 _ => {
169 return Err(PlannerError::InvalidOperation(
170 "PathPlanner requires the FindPath statement.".to_string(),
171 ));
172 }
173 };
174
175 let space_id = qctx.space_id().ok_or_else(|| {
176 PlannerError::InvalidOperation(
177 "No graph space selected, please execute USE <space> first".to_string(),
178 )
179 })?;
180
181 let start_node = StartNode::new();
186 let start_node_enum = PlanNodeEnum::Start(start_node);
187
188 let edge_types = find_path
189 .over
190 .as_ref()
191 .map(|(types, _)| types.clone())
192 .unwrap_or_default();
193
194 let max_steps = find_path.max_steps.unwrap_or(10);
195
196 let start_vertex_ids: Vec<Value> = find_path
197 .from
198 .iter()
199 .filter_map(|expr| match expr {
200 crate::binder::bound::BoundExpression::Literal(v, _) => Some(v.clone()),
201 _ => None,
202 })
203 .collect();
204
205 let end_vertex_ids: Vec<Value> = match &find_path.to {
206 crate::binder::bound::BoundExpression::Literal(v, _) => vec![v.clone()],
207 _ => vec![],
208 };
209
210 let (mut root_node, mut logical_root) = if find_path.shortest {
211 if find_path.with_loop || find_path.with_trail || find_path.with_walk {
215 log::debug!(
216 "FIND SHORTEST PATH ignores WITH LOOP/TRAIL/WALK: \
217 revisiting a vertex or edge cannot yield a shorter path"
218 );
219 }
220 let root = self.build_shortest_path_plan(PathPlanParams {
221 left_input: start_node_enum.clone(),
222 space_id,
223 edge_types: edge_types.clone(),
224 max_steps,
225 start_vertex_ids: start_vertex_ids.clone(),
226 end_vertex_ids: end_vertex_ids.clone(),
227 weight_expression: find_path.weight_expression.clone(),
228 })?;
229 let logical = Self::shortest_path_logical(
230 space_id,
231 edge_types,
232 max_steps,
233 start_vertex_ids,
234 end_vertex_ids,
235 find_path.weight_expression.clone(),
236 );
237 (root, logical)
238 } else {
239 let direction = find_path
240 .over
241 .as_ref()
242 .map(|(_, dir)| *dir)
243 .unwrap_or(linkrs_core::EdgeDirection::Out);
244
245 let path_uniqueness = PathUniqueness::from_flags(
249 find_path.with_loop,
250 find_path.with_trail,
251 find_path.with_walk,
252 );
253
254 let right_node = StartNode::new();
255 let right_node_enum = PlanNodeEnum::Start(right_node);
256
257 let mut all_paths_node = AllPathsNode::new(
258 start_node_enum.clone(),
259 right_node_enum,
260 space_id,
261 max_steps,
262 edge_types.clone(),
263 1,
264 max_steps,
265 path_uniqueness,
266 );
267 all_paths_node.set_direction(direction);
268 let start_vids: Vec<VertexId> = start_vertex_ids
269 .iter()
270 .filter_map(|v| VertexId::try_from(v).ok())
271 .collect();
272 let end_vids: Vec<VertexId> = end_vertex_ids
273 .iter()
274 .filter_map(|v| VertexId::try_from(v).ok())
275 .collect();
276 all_paths_node.set_start_vertex_ids(start_vids.clone());
277 all_paths_node.set_end_vertex_ids(end_vids.clone());
278
279 let logical = Self::all_paths_logical_from_parts(
280 space_id,
281 edge_types,
282 max_steps,
283 path_uniqueness,
284 direction,
285 start_vids,
286 end_vids,
287 );
288 (all_paths_node.into_enum(), logical)
289 };
290
291 if let Some(where_clause) = &find_path.where_clause {
293 let expr_ctx = Arc::new(
294 linkrs_core::types::expr::expression_context::ExpressionAnalysisContext::new(),
295 );
296 let condition = crate::binder::expr_converter::bound_expr_to_contextual(
297 &where_clause.condition,
298 &expr_ctx,
299 )
300 .map_err(PlannerError::PlanGenerationFailed)?;
301 let filter_node =
302 FilterNode::new(root_node.clone(), condition.clone()).map_err(|e| {
303 PlannerError::PlanGenerationFailed(format!(
304 "Failed to create FilterNode: {}",
305 e
306 ))
307 })?;
308 let col_names = filter_node.col_names().to_vec();
309 logical_root = wrap_logical_filter(logical_root, condition, col_names);
310 root_node = PlanNodeEnum::Filter(filter_node);
311 }
312
313 let sub_plan = SubPlan {
314 root: Some(root_node),
315 tail: Some(start_node_enum),
316 logical_root: Some(logical_root),
317 };
318
319 Ok(sub_plan)
320 }
321
322 fn match_planner(&self, stmt: &Stmt) -> bool {
323 matches!(stmt, Stmt::FindPath(_))
324 }
325}
326
327struct PathPlanParams {
329 left_input: PlanNodeEnum,
330 space_id: u64,
331 edge_types: Vec<String>,
332 max_steps: usize,
333 start_vertex_ids: Vec<Value>,
334 end_vertex_ids: Vec<Value>,
335 weight_expression: Option<String>,
336}
337
338impl PathPlanner {
339 fn build_shortest_path_plan(
340 &self,
341 params: PathPlanParams,
342 ) -> Result<PlanNodeEnum, PlannerError> {
343 let right_node = StartNode::new();
344 let right_node_enum = PlanNodeEnum::Start(right_node);
345
346 let mut shortest_path_node = ShortestPathNode::new(
347 params.left_input,
348 right_node_enum,
349 params.space_id,
350 params.edge_types,
351 params.max_steps,
352 );
353 shortest_path_node.set_start_vertex_ids(params.start_vertex_ids);
354 shortest_path_node.set_end_vertex_ids(params.end_vertex_ids);
355 if let Some(weight) = params.weight_expression {
356 shortest_path_node.set_weight_expression(weight);
357 }
358
359 Ok(shortest_path_node.into_enum())
360 }
361
362 fn build_all_paths_plan(
363 &self,
364 params: PathPlanParams,
365 stmt: &crate::parser::ast::FindPathStmt,
366 ) -> Result<PlanNodeEnum, PlannerError> {
367 let right_node = StartNode::new();
368 let right_node_enum = PlanNodeEnum::Start(right_node);
369
370 let path_uniqueness =
374 crate::planning::plan::core::nodes::traversal::path_algorithms::PathUniqueness::from_flags(
375 stmt.with_loop, stmt.with_trail, stmt.with_walk,
376 );
377 let direction = stmt
378 .over
379 .as_ref()
380 .map(|over| over.direction)
381 .unwrap_or(linkrs_core::EdgeDirection::Out);
382
383 let mut all_paths_node = AllPathsNode::new(
384 params.left_input,
385 right_node_enum,
386 params.space_id,
387 params.max_steps,
388 params.edge_types,
389 1,
390 params.max_steps,
391 path_uniqueness,
392 );
393 all_paths_node.set_direction(direction);
394 let start_vids: Vec<VertexId> = params
395 .start_vertex_ids
396 .iter()
397 .filter_map(|v| VertexId::try_from(v).ok())
398 .collect();
399 let end_vids: Vec<VertexId> = params
400 .end_vertex_ids
401 .iter()
402 .filter_map(|v| VertexId::try_from(v).ok())
403 .collect();
404 all_paths_node.set_start_vertex_ids(start_vids);
405 all_paths_node.set_end_vertex_ids(end_vids);
406
407 Ok(all_paths_node.into_enum())
408 }
409
410 fn logical_start_leaf() -> LogicalNodeEnum {
411 LogicalNodeEnum::Start(LogicalStartNode::new())
412 }
413
414 fn shortest_path_logical(
415 space_id: u64,
416 edge_types: Vec<String>,
417 max_steps: usize,
418 start_vertex_ids: Vec<Value>,
419 end_vertex_ids: Vec<Value>,
420 weight_expression: Option<String>,
421 ) -> LogicalNodeEnum {
422 let left = Self::logical_start_leaf();
423 let right = Self::logical_start_leaf();
424 LogicalNodeEnum::ShortestPath(LogicalShortestPathNode {
425 id: next_node_id(),
426 left: Box::new(left),
427 right: Box::new(right),
428 space_id,
429 edge_types,
430 max_step: max_steps,
431 weight_expression,
432 heuristic_expression: None,
433 no_reverse: false,
434 start_vertex_ids,
435 end_vertex_ids,
436 output_var: None,
437 col_names: vec!["path".to_string()],
438 column_types: vec![],
439 })
440 }
441
442 fn all_paths_logical(
443 space_id: u64,
444 edge_types: Vec<String>,
445 max_steps: usize,
446 stmt: &crate::parser::ast::FindPathStmt,
447 start_vertex_ids: Vec<Value>,
448 end_vertex_ids: Vec<Value>,
449 ) -> LogicalNodeEnum {
450 let path_uniqueness =
451 crate::planning::plan::core::nodes::traversal::path_algorithms::PathUniqueness::from_flags(
452 stmt.with_loop, stmt.with_trail, stmt.with_walk,
453 );
454 let direction = stmt
455 .over
456 .as_ref()
457 .map(|over| over.direction)
458 .unwrap_or(linkrs_core::EdgeDirection::Out);
459 let start_vids: Vec<VertexId> = start_vertex_ids
460 .iter()
461 .filter_map(|v| VertexId::try_from(v).ok())
462 .collect();
463 let end_vids: Vec<VertexId> = end_vertex_ids
464 .iter()
465 .filter_map(|v| VertexId::try_from(v).ok())
466 .collect();
467 Self::all_paths_logical_from_parts(
468 space_id,
469 edge_types,
470 max_steps,
471 path_uniqueness,
472 direction,
473 start_vids,
474 end_vids,
475 )
476 }
477
478 fn all_paths_logical_from_parts(
479 space_id: u64,
480 edge_types: Vec<String>,
481 max_steps: usize,
482 path_uniqueness: crate::planning::plan::core::nodes::traversal::path_algorithms::PathUniqueness,
483 direction: linkrs_core::EdgeDirection,
484 start_vertex_ids: Vec<VertexId>,
485 end_vertex_ids: Vec<VertexId>,
486 ) -> LogicalNodeEnum {
487 let left = Self::logical_start_leaf();
488 let right = Self::logical_start_leaf();
489 LogicalNodeEnum::AllPaths(LogicalAllPathsNode {
490 id: next_node_id(),
491 left: Box::new(left),
492 right: Box::new(right),
493 space_id,
494 steps: max_steps,
495 edge_types,
496 min_hop: 1,
497 max_hop: max_steps,
498 path_uniqueness,
499 direction,
500 has_step_limit: true,
501 limit: -1,
502 offset: 0,
503 filter: None,
504 start_vertex_ids,
505 end_vertex_ids,
506 output_var: None,
507 col_names: vec!["path".to_string()],
508 column_types: vec![],
509 })
510 }
511
512 fn is_shortest_path_stmt(&self, stmt: &crate::parser::ast::FindPathStmt) -> bool {
513 stmt.shortest
514 }
515
516 fn get_edge_types_from_stmt(&self, stmt: &crate::parser::ast::FindPathStmt) -> Vec<String> {
517 stmt.over
518 .as_ref()
519 .map(|over| over.edge_types.clone())
520 .unwrap_or_default()
521 }
522
523 fn get_max_steps_from_stmt(&self, stmt: &crate::parser::ast::FindPathStmt) -> usize {
524 stmt.max_steps.unwrap_or(10)
525 }
526
527 fn extract_vertex_ids_from_exprs(
528 &self,
529 exprs: &[linkrs_core::types::ContextualExpression],
530 ) -> Vec<Value> {
531 let mut ids = Vec::new();
532 for expr in exprs {
533 if let Some(meta) = expr.expression() {
534 if let Some(value) = meta.as_literal() {
535 ids.push(value.clone());
536 }
537 }
538 }
539 ids
540 }
541
542 fn extract_vertex_ids_from_expr(
543 &self,
544 expr: &linkrs_core::types::ContextualExpression,
545 ) -> Vec<Value> {
546 self.extract_vertex_ids_from_exprs(std::slice::from_ref(expr))
547 }
548}
549
550impl Default for PathPlanner {
551 fn default() -> Self {
552 Self::new()
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::binder::validation::{ValidatedStatement, ValidationInfo};
560 use crate::parser::ast::stmt::Ast;
561 use crate::parser::ast::{FindPathStmt, FromClause, OverClause, Span, Stmt};
562 use linkrs_core::types::expr::contextual::ContextualExpression;
563 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
564 use linkrs_core::types::expr::{Expression, ExpressionMeta};
565
566 fn test_query_context() -> Arc<QueryContext> {
567 let rctx = Arc::new(crate::QueryRequestContext::new("FIND PATH".to_string()));
568 let space_info = linkrs_core::types::SpaceInfo {
569 space_id: 1,
570 space_name: "test".to_string(),
571 ..Default::default()
572 };
573 Arc::new(
574 crate::QueryContext::builder(rctx)
575 .with_space_info(space_info)
576 .build(),
577 )
578 }
579
580 fn find_path_stmt(shortest: bool) -> FindPathStmt {
581 find_path_stmt_with_modifiers(shortest, false, false, false, None)
582 }
583
584 fn find_path_stmt_with_modifiers(
585 shortest: bool,
586 with_loop: bool,
587 with_trail: bool,
588 with_walk: bool,
589 weight_expression: Option<&str>,
590 ) -> FindPathStmt {
591 let ctx = Arc::new(ExpressionAnalysisContext::new());
592 let id =
593 ctx.register_expression(ExpressionMeta::new(Expression::Variable("dst".to_string())));
594 FindPathStmt {
595 span: Span::default(),
596 from: FromClause {
597 span: Span::default(),
598 vertices: vec![],
599 },
600 to: ContextualExpression::new(id, ctx.clone()),
601 over: Some(OverClause {
602 span: Span::default(),
603 edge_types: vec!["knows".to_string()],
604 direction: linkrs_core::EdgeDirection::Out,
605 }),
606 where_clause: None,
607 shortest,
608 max_steps: Some(3),
609 limit: None,
610 skip: None,
611 yield_clause: None,
612 weight_expression: weight_expression.map(str::to_string),
613 heuristic_expression: None,
614 with_loop,
615 with_trail,
616 with_walk,
617 }
618 }
619
620 fn validated(stmt: Stmt) -> ValidatedStatement {
621 let ctx = Arc::new(ExpressionAnalysisContext::new());
622 ValidatedStatement::new(Arc::new(Ast::new(stmt, ctx)), ValidationInfo::new())
623 }
624
625 #[test]
626 fn shortest_path_plan_carries_native_logical() {
627 let stmt = Stmt::FindPath(find_path_stmt(true));
628 let plan = PathPlanner::new()
629 .transform(&validated(stmt), test_query_context())
630 .expect("label-less FIND PATH plans without a vertex label");
631 assert!(
632 plan.logical_root.is_some(),
633 "shortest path plan must carry its native logical root"
634 );
635 }
636
637 #[test]
638 fn all_paths_plan_carries_native_logical_with_direction() {
639 let stmt = Stmt::FindPath(find_path_stmt(false));
640 let plan = PathPlanner::new()
641 .transform(&validated(stmt), test_query_context())
642 .expect("label-less FIND PATH plans without a vertex label");
643 assert!(
644 plan.logical_root.is_some(),
645 "all-paths plan must carry its native logical root"
646 );
647 }
648
649 #[test]
650 fn native_logical_converts_back_to_same_physical_shape() {
651 let stmt = Stmt::FindPath(find_path_stmt(false));
652 let plan = PathPlanner::new()
653 .transform(&validated(stmt), test_query_context())
654 .expect("label-less FIND PATH plans without a vertex label");
655 assert!(
656 plan.root.is_some() && plan.logical_root.is_some(),
657 "plan must carry both physical and logical shapes"
658 );
659 }
660
661 #[test]
662 fn shortest_path_plan_carries_weight_expression() {
663 let stmt = Stmt::FindPath(find_path_stmt_with_modifiers(
664 true,
665 false,
666 false,
667 false,
668 Some("cost"),
669 ));
670 let plan = PathPlanner::new()
671 .transform(&validated(stmt), test_query_context())
672 .expect("weighted shortest path plans");
673 match plan.root.as_ref().expect("physical root") {
674 PlanNodeEnum::ShortestPath(node) => {
675 assert_eq!(node.weight_expression().as_deref(), Some("cost"));
676 }
677 other => panic!("expected ShortestPath root, got {:?}", other),
678 }
679 match plan.logical_root.as_ref().expect("logical root") {
680 LogicalNodeEnum::ShortestPath(node) => {
681 assert_eq!(node.weight_expression.as_deref(), Some("cost"));
682 }
683 other => panic!("expected ShortestPath logical root, got {:?}", other),
684 }
685 }
686
687 #[test]
688 fn shortest_path_plan_defaults_weight_to_none() {
689 let stmt = Stmt::FindPath(find_path_stmt(true));
690 let plan = PathPlanner::new()
691 .transform(&validated(stmt), test_query_context())
692 .expect("plain shortest path plans");
693 match plan.root.as_ref().expect("physical root") {
694 PlanNodeEnum::ShortestPath(node) => {
695 assert!(node.weight_expression().is_none());
696 }
697 other => panic!("expected ShortestPath root, got {:?}", other),
698 }
699 }
700
701 #[test]
702 fn all_paths_plan_maps_repeat_flags_to_uniqueness() {
703 let cases = [
704 (false, false, false, PathUniqueness::VertexUnique),
705 (true, false, false, PathUniqueness::AllowSelfLoop),
706 (false, true, false, PathUniqueness::EdgeUnique),
707 (false, false, true, PathUniqueness::Free),
708 (true, true, false, PathUniqueness::EdgeUnique),
710 (true, false, true, PathUniqueness::Free),
711 ];
712 for (with_loop, with_trail, with_walk, expected) in cases {
713 let stmt = Stmt::FindPath(find_path_stmt_with_modifiers(
714 false, with_loop, with_trail, with_walk, None,
715 ));
716 let plan = PathPlanner::new()
717 .transform(&validated(stmt), test_query_context())
718 .expect("all path plans");
719 match plan.root.as_ref().expect("physical root") {
720 PlanNodeEnum::AllPaths(node) => {
721 assert_eq!(
722 node.path_uniqueness(),
723 expected,
724 "loop={with_loop} trail={with_trail} walk={with_walk} physical policy mismatch"
725 );
726 }
727 other => panic!("expected AllPaths root, got {:?}", other),
728 }
729 match plan.logical_root.as_ref().expect("logical root") {
730 LogicalNodeEnum::AllPaths(node) => {
731 assert_eq!(
732 node.path_uniqueness, expected,
733 "loop={with_loop} trail={with_trail} walk={with_walk} logical policy mismatch"
734 );
735 }
736 other => panic!("expected AllPaths logical root, got {:?}", other),
737 }
738 }
739 }
740
741 #[test]
742 fn shortest_path_ignores_repeat_flags() {
743 let stmt = Stmt::FindPath(find_path_stmt_with_modifiers(true, true, true, true, None));
746 let plan = PathPlanner::new()
747 .transform(&validated(stmt), test_query_context())
748 .expect("modifiers do not break shortest path planning");
749 assert!(
750 matches!(plan.root.as_ref(), Some(PlanNodeEnum::ShortestPath(_))),
751 "expected ShortestPath root, got {:?}",
752 plan.root
753 );
754 }
755}