Skip to main content

hawdb_plan/physical/
metadata.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Physical optimizer operator metadata.
16
17use std::collections::BTreeMap;
18use std::str::FromStr;
19
20/// A plan-local physical operator identity assigned in pre-order.
21///
22/// The identity is stable for one physical plan shape and lets planning and
23/// execution diagnostics correlate repeated operators without relying on
24/// operator names or process-local addresses.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct PhysicalOperatorId(usize);
27
28impl PhysicalOperatorId {
29    /// Creates an identity from its pre-order position in the physical plan.
30    pub const fn from_ordinal(ordinal: usize) -> Self {
31        Self(ordinal)
32    }
33
34    /// Returns the operator's zero-based pre-order position.
35    pub const fn ordinal(self) -> usize {
36        self.0
37    }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub enum PhysicalPlanKind {
42    CreateNodeLabel,
43    CreateRelationshipType,
44    CreateNodeTable,
45    CreateRelationshipTable,
46    CreateProperty,
47    AlterTableState,
48    AlterPropertyState,
49    CreateIndex,
50    CreateCompositeIndex,
51    CreateRangeIndex,
52    CreateFullTextIndex,
53    CreateUniqueConstraint,
54    CreateNodePropertyExistsConstraint,
55    CreateRelationshipUniqueConstraint,
56    CreateRelationshipPropertyExistsConstraint,
57    ProjectGraph,
58    GraphAlgorithm,
59    VectorSeedScan,
60    CreateNode,
61    UnwindMutationExec,
62    MergeNode,
63    MergeRelationship,
64    MergeMatchedRelationship,
65    MergeRelationshipFromMatchedRelationship,
66    MergeRelationshipToMatchedTarget,
67    MergeRelationshipFromMatchedTarget,
68    CreateMatchedRelationship,
69    SetNodeProperty,
70    SetNodeProperties,
71    SetNodePropertiesReturn,
72    SetRelationshipProperty,
73    SetRelationshipProperties,
74    DeleteNode,
75    DeleteRelationship,
76    DeleteRelationshipTargetNodes,
77    CreateRelationship,
78    GraphMatchExec,
79    EmptyExec,
80    SeqNodeScan,
81    NodeProjectionScanExec,
82    SourceSegmentScan,
83    NodeCartesianProductExec,
84    HashJoinExec,
85    NodeColumnLookupExec,
86    IndexNodeSeek,
87    IndexNodeMultiSeek,
88    IndexNodeUnionSeek,
89    IndexNodeCompositeSeek,
90    IndexNodeCompositeRangeSeek,
91    IndexNodeRangeSeek,
92    IndexNodeTextSeek,
93    AdjacencyExpandExec,
94    AdjacencyExistsExec,
95    OptionalDegreeExec,
96    OptionalRelationshipCountSumExec,
97    NodeCountExec,
98    RelationshipCountExec,
99    ThreadRepairStatsExec,
100    ShortestPathExec,
101    FilterExec,
102    ProjectExec,
103    AggregateExec,
104    DistinctExec,
105    SortExec,
106    TopNExec,
107    LimitExec,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
111pub enum PhysicalPlanClass {
112    Schema,
113    Mutation,
114    Access,
115    Traversal,
116    Relational,
117    Procedure,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
121pub enum PlanChildren<'a, P> {
122    None,
123    Unary(&'a P),
124    Binary(&'a P, &'a P),
125}
126
127pub trait PhysicalPlanNode {
128    fn kind(&self) -> PhysicalPlanKind;
129
130    fn children(&self) -> PlanChildren<'_, Self>
131    where
132        Self: Sized;
133
134    fn class(&self) -> PhysicalPlanClass {
135        self.kind().class()
136    }
137}
138
139impl PhysicalPlanKind {
140    pub fn all() -> &'static [Self] {
141        const ALL: &[PhysicalPlanKind] = &[
142            PhysicalPlanKind::CreateNodeLabel,
143            PhysicalPlanKind::CreateRelationshipType,
144            PhysicalPlanKind::CreateNodeTable,
145            PhysicalPlanKind::CreateRelationshipTable,
146            PhysicalPlanKind::CreateProperty,
147            PhysicalPlanKind::AlterTableState,
148            PhysicalPlanKind::AlterPropertyState,
149            PhysicalPlanKind::CreateIndex,
150            PhysicalPlanKind::CreateCompositeIndex,
151            PhysicalPlanKind::CreateRangeIndex,
152            PhysicalPlanKind::CreateFullTextIndex,
153            PhysicalPlanKind::CreateUniqueConstraint,
154            PhysicalPlanKind::CreateNodePropertyExistsConstraint,
155            PhysicalPlanKind::CreateRelationshipUniqueConstraint,
156            PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint,
157            PhysicalPlanKind::ProjectGraph,
158            PhysicalPlanKind::GraphAlgorithm,
159            PhysicalPlanKind::VectorSeedScan,
160            PhysicalPlanKind::CreateNode,
161            PhysicalPlanKind::UnwindMutationExec,
162            PhysicalPlanKind::MergeNode,
163            PhysicalPlanKind::MergeRelationship,
164            PhysicalPlanKind::MergeMatchedRelationship,
165            PhysicalPlanKind::MergeRelationshipFromMatchedRelationship,
166            PhysicalPlanKind::MergeRelationshipToMatchedTarget,
167            PhysicalPlanKind::MergeRelationshipFromMatchedTarget,
168            PhysicalPlanKind::CreateMatchedRelationship,
169            PhysicalPlanKind::SetNodeProperty,
170            PhysicalPlanKind::SetNodeProperties,
171            PhysicalPlanKind::SetNodePropertiesReturn,
172            PhysicalPlanKind::SetRelationshipProperty,
173            PhysicalPlanKind::SetRelationshipProperties,
174            PhysicalPlanKind::DeleteNode,
175            PhysicalPlanKind::DeleteRelationship,
176            PhysicalPlanKind::DeleteRelationshipTargetNodes,
177            PhysicalPlanKind::CreateRelationship,
178            PhysicalPlanKind::GraphMatchExec,
179            PhysicalPlanKind::EmptyExec,
180            PhysicalPlanKind::SeqNodeScan,
181            PhysicalPlanKind::NodeProjectionScanExec,
182            PhysicalPlanKind::SourceSegmentScan,
183            PhysicalPlanKind::NodeCartesianProductExec,
184            PhysicalPlanKind::HashJoinExec,
185            PhysicalPlanKind::NodeColumnLookupExec,
186            PhysicalPlanKind::IndexNodeSeek,
187            PhysicalPlanKind::IndexNodeMultiSeek,
188            PhysicalPlanKind::IndexNodeUnionSeek,
189            PhysicalPlanKind::IndexNodeCompositeSeek,
190            PhysicalPlanKind::IndexNodeCompositeRangeSeek,
191            PhysicalPlanKind::IndexNodeRangeSeek,
192            PhysicalPlanKind::IndexNodeTextSeek,
193            PhysicalPlanKind::AdjacencyExpandExec,
194            PhysicalPlanKind::AdjacencyExistsExec,
195            PhysicalPlanKind::OptionalDegreeExec,
196            PhysicalPlanKind::OptionalRelationshipCountSumExec,
197            PhysicalPlanKind::NodeCountExec,
198            PhysicalPlanKind::RelationshipCountExec,
199            PhysicalPlanKind::ThreadRepairStatsExec,
200            PhysicalPlanKind::ShortestPathExec,
201            PhysicalPlanKind::FilterExec,
202            PhysicalPlanKind::ProjectExec,
203            PhysicalPlanKind::AggregateExec,
204            PhysicalPlanKind::DistinctExec,
205            PhysicalPlanKind::SortExec,
206            PhysicalPlanKind::TopNExec,
207            PhysicalPlanKind::LimitExec,
208        ];
209        ALL
210    }
211
212    pub fn as_str(self) -> &'static str {
213        match self {
214            PhysicalPlanKind::CreateNodeLabel => "CreateNodeLabel",
215            PhysicalPlanKind::CreateRelationshipType => "CreateRelationshipType",
216            PhysicalPlanKind::CreateNodeTable => "CreateNodeTable",
217            PhysicalPlanKind::CreateRelationshipTable => "CreateRelationshipTable",
218            PhysicalPlanKind::CreateProperty => "CreateProperty",
219            PhysicalPlanKind::AlterTableState => "AlterTableState",
220            PhysicalPlanKind::AlterPropertyState => "AlterPropertyState",
221            PhysicalPlanKind::CreateIndex => "CreateIndex",
222            PhysicalPlanKind::CreateCompositeIndex => "CreateCompositeIndex",
223            PhysicalPlanKind::CreateRangeIndex => "CreateRangeIndex",
224            PhysicalPlanKind::CreateFullTextIndex => "CreateFullTextIndex",
225            PhysicalPlanKind::CreateUniqueConstraint => "CreateUniqueConstraint",
226            PhysicalPlanKind::CreateNodePropertyExistsConstraint => {
227                "CreateNodePropertyExistsConstraint"
228            }
229            PhysicalPlanKind::CreateRelationshipUniqueConstraint => {
230                "CreateRelationshipUniqueConstraint"
231            }
232            PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint => {
233                "CreateRelationshipPropertyExistsConstraint"
234            }
235            PhysicalPlanKind::ProjectGraph => "ProjectGraph",
236            PhysicalPlanKind::GraphAlgorithm => "GraphAlgorithm",
237            PhysicalPlanKind::VectorSeedScan => "VectorSeedScan",
238            PhysicalPlanKind::CreateNode => "CreateNode",
239            PhysicalPlanKind::UnwindMutationExec => "UnwindMutationExec",
240            PhysicalPlanKind::MergeNode => "MergeNode",
241            PhysicalPlanKind::MergeRelationship => "MergeRelationship",
242            PhysicalPlanKind::MergeMatchedRelationship => "MergeMatchedRelationship",
243            PhysicalPlanKind::MergeRelationshipFromMatchedRelationship => {
244                "MergeRelationshipFromMatchedRelationship"
245            }
246            PhysicalPlanKind::MergeRelationshipToMatchedTarget => {
247                "MergeRelationshipToMatchedTarget"
248            }
249            PhysicalPlanKind::MergeRelationshipFromMatchedTarget => {
250                "MergeRelationshipFromMatchedTarget"
251            }
252            PhysicalPlanKind::CreateMatchedRelationship => "CreateMatchedRelationship",
253            PhysicalPlanKind::SetNodeProperty => "SetNodeProperty",
254            PhysicalPlanKind::SetNodeProperties => "SetNodeProperties",
255            PhysicalPlanKind::SetNodePropertiesReturn => "SetNodePropertiesReturn",
256            PhysicalPlanKind::SetRelationshipProperty => "SetRelationshipProperty",
257            PhysicalPlanKind::SetRelationshipProperties => "SetRelationshipProperties",
258            PhysicalPlanKind::DeleteNode => "DeleteNode",
259            PhysicalPlanKind::DeleteRelationship => "DeleteRelationship",
260            PhysicalPlanKind::DeleteRelationshipTargetNodes => "DeleteRelationshipTargetNodes",
261            PhysicalPlanKind::CreateRelationship => "CreateRelationship",
262            PhysicalPlanKind::GraphMatchExec => "GraphMatchExec",
263            PhysicalPlanKind::EmptyExec => "EmptyExec",
264            PhysicalPlanKind::SeqNodeScan => "SeqNodeScan",
265            PhysicalPlanKind::NodeProjectionScanExec => "NodeProjectionScanExec",
266            PhysicalPlanKind::SourceSegmentScan => "SourceSegmentScan",
267            PhysicalPlanKind::HashJoinExec => "HashJoinExec",
268            PhysicalPlanKind::NodeCartesianProductExec => "NodeCartesianProductExec",
269            PhysicalPlanKind::NodeColumnLookupExec => "NodeColumnLookupExec",
270            PhysicalPlanKind::IndexNodeSeek => "IndexNodeSeek",
271            PhysicalPlanKind::IndexNodeMultiSeek => "IndexNodeMultiSeek",
272            PhysicalPlanKind::IndexNodeUnionSeek => "IndexNodeUnionSeek",
273            PhysicalPlanKind::IndexNodeCompositeSeek => "IndexNodeCompositeSeek",
274            PhysicalPlanKind::IndexNodeCompositeRangeSeek => "IndexNodeCompositeRangeSeek",
275            PhysicalPlanKind::IndexNodeRangeSeek => "IndexNodeRangeSeek",
276            PhysicalPlanKind::IndexNodeTextSeek => "IndexNodeTextSeek",
277            PhysicalPlanKind::AdjacencyExpandExec => "AdjacencyExpandExec",
278            PhysicalPlanKind::AdjacencyExistsExec => "AdjacencyExistsExec",
279            PhysicalPlanKind::OptionalDegreeExec => "OptionalDegreeExec",
280            PhysicalPlanKind::OptionalRelationshipCountSumExec => {
281                "OptionalRelationshipCountSumExec"
282            }
283            PhysicalPlanKind::NodeCountExec => "NodeCountExec",
284            PhysicalPlanKind::RelationshipCountExec => "RelationshipCountExec",
285            PhysicalPlanKind::ThreadRepairStatsExec => "ThreadRepairStatsExec",
286            PhysicalPlanKind::ShortestPathExec => "ShortestPathExec",
287            PhysicalPlanKind::FilterExec => "FilterExec",
288            PhysicalPlanKind::ProjectExec => "ProjectExec",
289            PhysicalPlanKind::AggregateExec => "AggregateExec",
290            PhysicalPlanKind::DistinctExec => "DistinctExec",
291            PhysicalPlanKind::SortExec => "SortExec",
292            PhysicalPlanKind::TopNExec => "TopNExec",
293            PhysicalPlanKind::LimitExec => "LimitExec",
294        }
295    }
296
297    pub fn class(self) -> PhysicalPlanClass {
298        match self {
299            PhysicalPlanKind::CreateNodeLabel
300            | PhysicalPlanKind::CreateRelationshipType
301            | PhysicalPlanKind::CreateNodeTable
302            | PhysicalPlanKind::CreateRelationshipTable
303            | PhysicalPlanKind::CreateProperty
304            | PhysicalPlanKind::AlterTableState
305            | PhysicalPlanKind::AlterPropertyState
306            | PhysicalPlanKind::CreateIndex
307            | PhysicalPlanKind::CreateCompositeIndex
308            | PhysicalPlanKind::CreateRangeIndex
309            | PhysicalPlanKind::CreateFullTextIndex
310            | PhysicalPlanKind::CreateUniqueConstraint
311            | PhysicalPlanKind::CreateNodePropertyExistsConstraint
312            | PhysicalPlanKind::CreateRelationshipUniqueConstraint
313            | PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint => {
314                PhysicalPlanClass::Schema
315            }
316            PhysicalPlanKind::CreateNode
317            | PhysicalPlanKind::UnwindMutationExec
318            | PhysicalPlanKind::MergeNode
319            | PhysicalPlanKind::MergeRelationship
320            | PhysicalPlanKind::MergeMatchedRelationship
321            | PhysicalPlanKind::MergeRelationshipFromMatchedRelationship
322            | PhysicalPlanKind::MergeRelationshipToMatchedTarget
323            | PhysicalPlanKind::MergeRelationshipFromMatchedTarget
324            | PhysicalPlanKind::CreateMatchedRelationship
325            | PhysicalPlanKind::SetNodeProperty
326            | PhysicalPlanKind::SetNodeProperties
327            | PhysicalPlanKind::SetNodePropertiesReturn
328            | PhysicalPlanKind::SetRelationshipProperty
329            | PhysicalPlanKind::SetRelationshipProperties
330            | PhysicalPlanKind::DeleteNode
331            | PhysicalPlanKind::DeleteRelationship
332            | PhysicalPlanKind::DeleteRelationshipTargetNodes
333            | PhysicalPlanKind::CreateRelationship => PhysicalPlanClass::Mutation,
334            PhysicalPlanKind::SeqNodeScan
335            | PhysicalPlanKind::NodeProjectionScanExec
336            | PhysicalPlanKind::SourceSegmentScan
337            | PhysicalPlanKind::NodeColumnLookupExec
338            | PhysicalPlanKind::IndexNodeSeek
339            | PhysicalPlanKind::IndexNodeMultiSeek
340            | PhysicalPlanKind::IndexNodeUnionSeek
341            | PhysicalPlanKind::IndexNodeCompositeSeek
342            | PhysicalPlanKind::IndexNodeCompositeRangeSeek
343            | PhysicalPlanKind::IndexNodeRangeSeek
344            | PhysicalPlanKind::IndexNodeTextSeek => PhysicalPlanClass::Access,
345            PhysicalPlanKind::GraphMatchExec
346            | PhysicalPlanKind::AdjacencyExpandExec
347            | PhysicalPlanKind::AdjacencyExistsExec
348            | PhysicalPlanKind::OptionalDegreeExec
349            | PhysicalPlanKind::OptionalRelationshipCountSumExec
350            | PhysicalPlanKind::ShortestPathExec => PhysicalPlanClass::Traversal,
351            PhysicalPlanKind::NodeCountExec
352            | PhysicalPlanKind::RelationshipCountExec
353            | PhysicalPlanKind::EmptyExec
354            | PhysicalPlanKind::NodeCartesianProductExec
355            | PhysicalPlanKind::HashJoinExec
356            | PhysicalPlanKind::FilterExec
357            | PhysicalPlanKind::ProjectExec
358            | PhysicalPlanKind::AggregateExec
359            | PhysicalPlanKind::DistinctExec
360            | PhysicalPlanKind::SortExec
361            | PhysicalPlanKind::TopNExec
362            | PhysicalPlanKind::LimitExec => PhysicalPlanClass::Relational,
363            PhysicalPlanKind::ProjectGraph
364            | PhysicalPlanKind::GraphAlgorithm
365            | PhysicalPlanKind::VectorSeedScan
366            | PhysicalPlanKind::ThreadRepairStatsExec => PhysicalPlanClass::Procedure,
367        }
368    }
369}
370
371impl PhysicalPlanClass {
372    pub fn all() -> &'static [Self] {
373        const ALL: &[PhysicalPlanClass] = &[
374            PhysicalPlanClass::Schema,
375            PhysicalPlanClass::Mutation,
376            PhysicalPlanClass::Access,
377            PhysicalPlanClass::Traversal,
378            PhysicalPlanClass::Relational,
379            PhysicalPlanClass::Procedure,
380        ];
381        ALL
382    }
383
384    pub fn as_str(self) -> &'static str {
385        match self {
386            PhysicalPlanClass::Schema => "schema",
387            PhysicalPlanClass::Mutation => "mutation",
388            PhysicalPlanClass::Access => "access",
389            PhysicalPlanClass::Traversal => "traversal",
390            PhysicalPlanClass::Relational => "relational",
391            PhysicalPlanClass::Procedure => "procedure",
392        }
393    }
394}
395
396impl FromStr for PhysicalPlanKind {
397    type Err = &'static str;
398
399    fn from_str(value: &str) -> Result<Self, Self::Err> {
400        Self::all()
401            .iter()
402            .copied()
403            .find(|kind| kind.as_str() == value)
404            .ok_or("unknown physical plan kind")
405    }
406}
407
408impl FromStr for PhysicalPlanClass {
409    type Err = &'static str;
410
411    fn from_str(value: &str) -> Result<Self, Self::Err> {
412        Self::all()
413            .iter()
414            .copied()
415            .find(|class| class.as_str() == value)
416            .ok_or("unknown physical plan class")
417    }
418}
419
420impl<'a, P> PlanChildren<'a, P> {
421    pub fn len(self) -> usize {
422        match self {
423            PlanChildren::None => 0,
424            PlanChildren::Unary(_) => 1,
425            PlanChildren::Binary(_, _) => 2,
426        }
427    }
428
429    pub fn is_empty(self) -> bool {
430        self.len() == 0
431    }
432}
433
434pub fn plan_operator_counts<P>(plan: &P) -> BTreeMap<String, usize>
435where
436    P: PhysicalPlanNode,
437{
438    let mut counts = BTreeMap::new();
439    visit_plan(plan, &mut |node| {
440        *counts.entry(node.kind().as_str().to_string()).or_default() += 1;
441    });
442    counts
443}
444
445pub fn plan_class_counts<P>(plan: &P) -> BTreeMap<String, usize>
446where
447    P: PhysicalPlanNode,
448{
449    let mut counts = BTreeMap::new();
450    visit_plan(plan, &mut |node| {
451        *counts.entry(node.class().as_str().to_string()).or_default() += 1;
452    });
453    counts
454}
455
456pub fn visit_plan<P>(plan: &P, visitor: &mut impl FnMut(&P))
457where
458    P: PhysicalPlanNode,
459{
460    visitor(plan);
461    match plan.children() {
462        PlanChildren::None => {}
463        PlanChildren::Unary(input) => visit_plan(input, visitor),
464        PlanChildren::Binary(left, right) => {
465            visit_plan(left, visitor);
466            visit_plan(right, visitor);
467        }
468    }
469}
470
471/// Visits a physical plan in pre-order and assigns each operator its plan-local identity.
472pub fn visit_plan_with_ids<P>(plan: &P, visitor: &mut impl FnMut(PhysicalOperatorId, &P))
473where
474    P: PhysicalPlanNode,
475{
476    fn visit<P>(
477        plan: &P,
478        next_ordinal: &mut usize,
479        visitor: &mut impl FnMut(PhysicalOperatorId, &P),
480    ) where
481        P: PhysicalPlanNode,
482    {
483        let operator_id = PhysicalOperatorId::from_ordinal(*next_ordinal);
484        *next_ordinal = next_ordinal
485            .checked_add(1)
486            .expect("physical plan operator count exceeds usize");
487        visitor(operator_id, plan);
488        match plan.children() {
489            PlanChildren::None => {}
490            PlanChildren::Unary(input) => visit(input, next_ordinal, visitor),
491            PlanChildren::Binary(left, right) => {
492                visit(left, next_ordinal, visitor);
493                visit(right, next_ordinal, visitor);
494            }
495        }
496    }
497
498    let mut next_ordinal = 0;
499    visit(plan, &mut next_ordinal, visitor);
500}
501
502#[cfg(test)]
503mod tests {
504    use super::{
505        plan_class_counts, plan_operator_counts, visit_plan, visit_plan_with_ids,
506        PhysicalPlanClass, PhysicalPlanKind, PhysicalPlanNode, PlanChildren,
507    };
508
509    #[test]
510    fn physical_plan_kind_exposes_stable_strings_and_classes() {
511        assert_eq!(PhysicalPlanKind::IndexNodeSeek.as_str(), "IndexNodeSeek");
512        assert_eq!(
513            PhysicalPlanKind::IndexNodeSeek.class(),
514            PhysicalPlanClass::Access
515        );
516        assert_eq!(
517            PhysicalPlanKind::AdjacencyExpandExec.class(),
518            PhysicalPlanClass::Traversal
519        );
520        assert_eq!(
521            PhysicalPlanKind::ProjectExec.class(),
522            PhysicalPlanClass::Relational
523        );
524    }
525
526    #[test]
527    fn physical_plan_kind_strings_round_trip_for_diagnostics() {
528        for kind in PhysicalPlanKind::all() {
529            assert_eq!(kind.as_str().parse::<PhysicalPlanKind>(), Ok(*kind));
530        }
531        assert!("UnknownExec".parse::<PhysicalPlanKind>().is_err());
532    }
533
534    #[test]
535    fn physical_plan_class_exposes_stable_strings() {
536        assert_eq!(PhysicalPlanClass::Schema.as_str(), "schema");
537        assert_eq!(PhysicalPlanClass::Mutation.as_str(), "mutation");
538        assert_eq!(PhysicalPlanClass::Access.as_str(), "access");
539        assert_eq!(PhysicalPlanClass::Traversal.as_str(), "traversal");
540        assert_eq!(PhysicalPlanClass::Relational.as_str(), "relational");
541        assert_eq!(PhysicalPlanClass::Procedure.as_str(), "procedure");
542    }
543
544    #[test]
545    fn physical_plan_class_strings_round_trip_for_diagnostics() {
546        for class in PhysicalPlanClass::all() {
547            assert_eq!(class.as_str().parse::<PhysicalPlanClass>(), Ok(*class));
548        }
549        assert!("unknown".parse::<PhysicalPlanClass>().is_err());
550    }
551
552    #[test]
553    fn plan_children_reports_arity_without_knowing_plan_type() {
554        assert_eq!(PlanChildren::<&str>::None.len(), 0);
555        assert!(PlanChildren::<&str>::None.is_empty());
556
557        let child = "scan";
558        assert_eq!(PlanChildren::Unary(&child).len(), 1);
559
560        let left = "left";
561        let right = "right";
562        assert_eq!(PlanChildren::Binary(&left, &right).len(), 2);
563    }
564
565    #[derive(Debug)]
566    enum TestPlan {
567        Seek,
568        Project(Box<TestPlan>),
569        Join(Box<TestPlan>, Box<TestPlan>),
570    }
571
572    impl PhysicalPlanNode for TestPlan {
573        fn kind(&self) -> PhysicalPlanKind {
574            match self {
575                TestPlan::Seek => PhysicalPlanKind::IndexNodeSeek,
576                TestPlan::Project(_) => PhysicalPlanKind::ProjectExec,
577                TestPlan::Join(_, _) => PhysicalPlanKind::NodeCartesianProductExec,
578            }
579        }
580
581        fn children(&self) -> PlanChildren<'_, Self> {
582            match self {
583                TestPlan::Seek => PlanChildren::None,
584                TestPlan::Project(input) => PlanChildren::Unary(input),
585                TestPlan::Join(left, right) => PlanChildren::Binary(left, right),
586            }
587        }
588    }
589
590    #[test]
591    fn plan_node_helpers_traverse_without_knowing_plan_payloads() {
592        let plan = TestPlan::Project(Box::new(TestPlan::Join(
593            Box::new(TestPlan::Seek),
594            Box::new(TestPlan::Seek),
595        )));
596
597        let mut visit_order = Vec::new();
598        visit_plan(&plan, &mut |node| {
599            visit_order.push(node.kind().as_str());
600        });
601
602        assert_eq!(
603            visit_order,
604            vec![
605                "ProjectExec",
606                "NodeCartesianProductExec",
607                "IndexNodeSeek",
608                "IndexNodeSeek"
609            ]
610        );
611        assert_eq!(plan_operator_counts(&plan)["IndexNodeSeek"], 2);
612        assert_eq!(plan_operator_counts(&plan)["ProjectExec"], 1);
613        assert_eq!(plan_class_counts(&plan)["access"], 2);
614        assert_eq!(plan_class_counts(&plan)["relational"], 2);
615
616        let mut identified = Vec::new();
617        visit_plan_with_ids(&plan, &mut |operator_id, node| {
618            identified.push((operator_id.ordinal(), node.kind().as_str()));
619        });
620        assert_eq!(
621            identified,
622            vec![
623                (0, "ProjectExec"),
624                (1, "NodeCartesianProductExec"),
625                (2, "IndexNodeSeek"),
626                (3, "IndexNodeSeek"),
627            ]
628        );
629    }
630}