Skip to main content

hawdb_plan_cypher/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    ScoringRerankExec,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
112pub enum PhysicalPlanClass {
113    Schema,
114    Mutation,
115    Access,
116    Traversal,
117    Relational,
118    Procedure,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
122pub enum PlanChildren<'a, P> {
123    None,
124    Unary(&'a P),
125    Binary(&'a P, &'a P),
126}
127
128pub trait PhysicalPlanNode {
129    fn kind(&self) -> PhysicalPlanKind;
130
131    fn children(&self) -> PlanChildren<'_, Self>
132    where
133        Self: Sized;
134
135    fn class(&self) -> PhysicalPlanClass {
136        self.kind().class()
137    }
138}
139
140impl PhysicalPlanKind {
141    pub fn all() -> &'static [Self] {
142        const ALL: &[PhysicalPlanKind] = &[
143            PhysicalPlanKind::CreateNodeLabel,
144            PhysicalPlanKind::CreateRelationshipType,
145            PhysicalPlanKind::CreateNodeTable,
146            PhysicalPlanKind::CreateRelationshipTable,
147            PhysicalPlanKind::CreateProperty,
148            PhysicalPlanKind::AlterTableState,
149            PhysicalPlanKind::AlterPropertyState,
150            PhysicalPlanKind::CreateIndex,
151            PhysicalPlanKind::CreateCompositeIndex,
152            PhysicalPlanKind::CreateRangeIndex,
153            PhysicalPlanKind::CreateFullTextIndex,
154            PhysicalPlanKind::CreateUniqueConstraint,
155            PhysicalPlanKind::CreateNodePropertyExistsConstraint,
156            PhysicalPlanKind::CreateRelationshipUniqueConstraint,
157            PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint,
158            PhysicalPlanKind::ProjectGraph,
159            PhysicalPlanKind::GraphAlgorithm,
160            PhysicalPlanKind::VectorSeedScan,
161            PhysicalPlanKind::CreateNode,
162            PhysicalPlanKind::UnwindMutationExec,
163            PhysicalPlanKind::MergeNode,
164            PhysicalPlanKind::MergeRelationship,
165            PhysicalPlanKind::MergeMatchedRelationship,
166            PhysicalPlanKind::MergeRelationshipFromMatchedRelationship,
167            PhysicalPlanKind::MergeRelationshipToMatchedTarget,
168            PhysicalPlanKind::MergeRelationshipFromMatchedTarget,
169            PhysicalPlanKind::CreateMatchedRelationship,
170            PhysicalPlanKind::SetNodeProperty,
171            PhysicalPlanKind::SetNodeProperties,
172            PhysicalPlanKind::SetNodePropertiesReturn,
173            PhysicalPlanKind::SetRelationshipProperty,
174            PhysicalPlanKind::SetRelationshipProperties,
175            PhysicalPlanKind::DeleteNode,
176            PhysicalPlanKind::DeleteRelationship,
177            PhysicalPlanKind::DeleteRelationshipTargetNodes,
178            PhysicalPlanKind::CreateRelationship,
179            PhysicalPlanKind::GraphMatchExec,
180            PhysicalPlanKind::EmptyExec,
181            PhysicalPlanKind::SeqNodeScan,
182            PhysicalPlanKind::NodeProjectionScanExec,
183            PhysicalPlanKind::SourceSegmentScan,
184            PhysicalPlanKind::NodeCartesianProductExec,
185            PhysicalPlanKind::HashJoinExec,
186            PhysicalPlanKind::NodeColumnLookupExec,
187            PhysicalPlanKind::IndexNodeSeek,
188            PhysicalPlanKind::IndexNodeMultiSeek,
189            PhysicalPlanKind::IndexNodeUnionSeek,
190            PhysicalPlanKind::IndexNodeCompositeSeek,
191            PhysicalPlanKind::IndexNodeCompositeRangeSeek,
192            PhysicalPlanKind::IndexNodeRangeSeek,
193            PhysicalPlanKind::IndexNodeTextSeek,
194            PhysicalPlanKind::AdjacencyExpandExec,
195            PhysicalPlanKind::AdjacencyExistsExec,
196            PhysicalPlanKind::OptionalDegreeExec,
197            PhysicalPlanKind::OptionalRelationshipCountSumExec,
198            PhysicalPlanKind::NodeCountExec,
199            PhysicalPlanKind::RelationshipCountExec,
200            PhysicalPlanKind::ThreadRepairStatsExec,
201            PhysicalPlanKind::ShortestPathExec,
202            PhysicalPlanKind::FilterExec,
203            PhysicalPlanKind::ProjectExec,
204            PhysicalPlanKind::AggregateExec,
205            PhysicalPlanKind::DistinctExec,
206            PhysicalPlanKind::SortExec,
207            PhysicalPlanKind::TopNExec,
208            PhysicalPlanKind::LimitExec,
209            PhysicalPlanKind::ScoringRerankExec,
210        ];
211        ALL
212    }
213
214    pub fn as_str(self) -> &'static str {
215        match self {
216            PhysicalPlanKind::CreateNodeLabel => "CreateNodeLabel",
217            PhysicalPlanKind::CreateRelationshipType => "CreateRelationshipType",
218            PhysicalPlanKind::CreateNodeTable => "CreateNodeTable",
219            PhysicalPlanKind::CreateRelationshipTable => "CreateRelationshipTable",
220            PhysicalPlanKind::CreateProperty => "CreateProperty",
221            PhysicalPlanKind::AlterTableState => "AlterTableState",
222            PhysicalPlanKind::AlterPropertyState => "AlterPropertyState",
223            PhysicalPlanKind::CreateIndex => "CreateIndex",
224            PhysicalPlanKind::CreateCompositeIndex => "CreateCompositeIndex",
225            PhysicalPlanKind::CreateRangeIndex => "CreateRangeIndex",
226            PhysicalPlanKind::CreateFullTextIndex => "CreateFullTextIndex",
227            PhysicalPlanKind::CreateUniqueConstraint => "CreateUniqueConstraint",
228            PhysicalPlanKind::CreateNodePropertyExistsConstraint => {
229                "CreateNodePropertyExistsConstraint"
230            }
231            PhysicalPlanKind::CreateRelationshipUniqueConstraint => {
232                "CreateRelationshipUniqueConstraint"
233            }
234            PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint => {
235                "CreateRelationshipPropertyExistsConstraint"
236            }
237            PhysicalPlanKind::ProjectGraph => "ProjectGraph",
238            PhysicalPlanKind::GraphAlgorithm => "GraphAlgorithm",
239            PhysicalPlanKind::VectorSeedScan => "VectorSeedScan",
240            PhysicalPlanKind::CreateNode => "CreateNode",
241            PhysicalPlanKind::UnwindMutationExec => "UnwindMutationExec",
242            PhysicalPlanKind::MergeNode => "MergeNode",
243            PhysicalPlanKind::MergeRelationship => "MergeRelationship",
244            PhysicalPlanKind::MergeMatchedRelationship => "MergeMatchedRelationship",
245            PhysicalPlanKind::MergeRelationshipFromMatchedRelationship => {
246                "MergeRelationshipFromMatchedRelationship"
247            }
248            PhysicalPlanKind::MergeRelationshipToMatchedTarget => {
249                "MergeRelationshipToMatchedTarget"
250            }
251            PhysicalPlanKind::MergeRelationshipFromMatchedTarget => {
252                "MergeRelationshipFromMatchedTarget"
253            }
254            PhysicalPlanKind::CreateMatchedRelationship => "CreateMatchedRelationship",
255            PhysicalPlanKind::SetNodeProperty => "SetNodeProperty",
256            PhysicalPlanKind::SetNodeProperties => "SetNodeProperties",
257            PhysicalPlanKind::SetNodePropertiesReturn => "SetNodePropertiesReturn",
258            PhysicalPlanKind::SetRelationshipProperty => "SetRelationshipProperty",
259            PhysicalPlanKind::SetRelationshipProperties => "SetRelationshipProperties",
260            PhysicalPlanKind::DeleteNode => "DeleteNode",
261            PhysicalPlanKind::DeleteRelationship => "DeleteRelationship",
262            PhysicalPlanKind::DeleteRelationshipTargetNodes => "DeleteRelationshipTargetNodes",
263            PhysicalPlanKind::CreateRelationship => "CreateRelationship",
264            PhysicalPlanKind::GraphMatchExec => "GraphMatchExec",
265            PhysicalPlanKind::EmptyExec => "EmptyExec",
266            PhysicalPlanKind::SeqNodeScan => "SeqNodeScan",
267            PhysicalPlanKind::NodeProjectionScanExec => "NodeProjectionScanExec",
268            PhysicalPlanKind::SourceSegmentScan => "SourceSegmentScan",
269            PhysicalPlanKind::HashJoinExec => "HashJoinExec",
270            PhysicalPlanKind::NodeCartesianProductExec => "NodeCartesianProductExec",
271            PhysicalPlanKind::NodeColumnLookupExec => "NodeColumnLookupExec",
272            PhysicalPlanKind::IndexNodeSeek => "IndexNodeSeek",
273            PhysicalPlanKind::IndexNodeMultiSeek => "IndexNodeMultiSeek",
274            PhysicalPlanKind::IndexNodeUnionSeek => "IndexNodeUnionSeek",
275            PhysicalPlanKind::IndexNodeCompositeSeek => "IndexNodeCompositeSeek",
276            PhysicalPlanKind::IndexNodeCompositeRangeSeek => "IndexNodeCompositeRangeSeek",
277            PhysicalPlanKind::IndexNodeRangeSeek => "IndexNodeRangeSeek",
278            PhysicalPlanKind::IndexNodeTextSeek => "IndexNodeTextSeek",
279            PhysicalPlanKind::AdjacencyExpandExec => "AdjacencyExpandExec",
280            PhysicalPlanKind::AdjacencyExistsExec => "AdjacencyExistsExec",
281            PhysicalPlanKind::OptionalDegreeExec => "OptionalDegreeExec",
282            PhysicalPlanKind::OptionalRelationshipCountSumExec => {
283                "OptionalRelationshipCountSumExec"
284            }
285            PhysicalPlanKind::NodeCountExec => "NodeCountExec",
286            PhysicalPlanKind::RelationshipCountExec => "RelationshipCountExec",
287            PhysicalPlanKind::ThreadRepairStatsExec => "ThreadRepairStatsExec",
288            PhysicalPlanKind::ShortestPathExec => "ShortestPathExec",
289            PhysicalPlanKind::FilterExec => "FilterExec",
290            PhysicalPlanKind::ProjectExec => "ProjectExec",
291            PhysicalPlanKind::AggregateExec => "AggregateExec",
292            PhysicalPlanKind::DistinctExec => "DistinctExec",
293            PhysicalPlanKind::SortExec => "SortExec",
294            PhysicalPlanKind::TopNExec => "TopNExec",
295            PhysicalPlanKind::LimitExec => "LimitExec",
296            PhysicalPlanKind::ScoringRerankExec => "ScoringRerankExec",
297        }
298    }
299
300    pub fn class(self) -> PhysicalPlanClass {
301        match self {
302            PhysicalPlanKind::CreateNodeLabel
303            | PhysicalPlanKind::CreateRelationshipType
304            | PhysicalPlanKind::CreateNodeTable
305            | PhysicalPlanKind::CreateRelationshipTable
306            | PhysicalPlanKind::CreateProperty
307            | PhysicalPlanKind::AlterTableState
308            | PhysicalPlanKind::AlterPropertyState
309            | PhysicalPlanKind::CreateIndex
310            | PhysicalPlanKind::CreateCompositeIndex
311            | PhysicalPlanKind::CreateRangeIndex
312            | PhysicalPlanKind::CreateFullTextIndex
313            | PhysicalPlanKind::CreateUniqueConstraint
314            | PhysicalPlanKind::CreateNodePropertyExistsConstraint
315            | PhysicalPlanKind::CreateRelationshipUniqueConstraint
316            | PhysicalPlanKind::CreateRelationshipPropertyExistsConstraint => {
317                PhysicalPlanClass::Schema
318            }
319            PhysicalPlanKind::CreateNode
320            | PhysicalPlanKind::UnwindMutationExec
321            | PhysicalPlanKind::MergeNode
322            | PhysicalPlanKind::MergeRelationship
323            | PhysicalPlanKind::MergeMatchedRelationship
324            | PhysicalPlanKind::MergeRelationshipFromMatchedRelationship
325            | PhysicalPlanKind::MergeRelationshipToMatchedTarget
326            | PhysicalPlanKind::MergeRelationshipFromMatchedTarget
327            | PhysicalPlanKind::CreateMatchedRelationship
328            | PhysicalPlanKind::SetNodeProperty
329            | PhysicalPlanKind::SetNodeProperties
330            | PhysicalPlanKind::SetNodePropertiesReturn
331            | PhysicalPlanKind::SetRelationshipProperty
332            | PhysicalPlanKind::SetRelationshipProperties
333            | PhysicalPlanKind::DeleteNode
334            | PhysicalPlanKind::DeleteRelationship
335            | PhysicalPlanKind::DeleteRelationshipTargetNodes
336            | PhysicalPlanKind::CreateRelationship => PhysicalPlanClass::Mutation,
337            PhysicalPlanKind::SeqNodeScan
338            | PhysicalPlanKind::NodeProjectionScanExec
339            | PhysicalPlanKind::SourceSegmentScan
340            | PhysicalPlanKind::NodeColumnLookupExec
341            | PhysicalPlanKind::IndexNodeSeek
342            | PhysicalPlanKind::IndexNodeMultiSeek
343            | PhysicalPlanKind::IndexNodeUnionSeek
344            | PhysicalPlanKind::IndexNodeCompositeSeek
345            | PhysicalPlanKind::IndexNodeCompositeRangeSeek
346            | PhysicalPlanKind::IndexNodeRangeSeek
347            | PhysicalPlanKind::IndexNodeTextSeek => PhysicalPlanClass::Access,
348            PhysicalPlanKind::GraphMatchExec
349            | PhysicalPlanKind::AdjacencyExpandExec
350            | PhysicalPlanKind::AdjacencyExistsExec
351            | PhysicalPlanKind::OptionalDegreeExec
352            | PhysicalPlanKind::OptionalRelationshipCountSumExec
353            | PhysicalPlanKind::ShortestPathExec => PhysicalPlanClass::Traversal,
354            PhysicalPlanKind::NodeCountExec
355            | PhysicalPlanKind::RelationshipCountExec
356            | PhysicalPlanKind::EmptyExec
357            | PhysicalPlanKind::NodeCartesianProductExec
358            | PhysicalPlanKind::HashJoinExec
359            | PhysicalPlanKind::FilterExec
360            | PhysicalPlanKind::ProjectExec
361            | PhysicalPlanKind::AggregateExec
362            | PhysicalPlanKind::DistinctExec
363            | PhysicalPlanKind::SortExec
364            | PhysicalPlanKind::TopNExec
365            | PhysicalPlanKind::LimitExec
366            | PhysicalPlanKind::ScoringRerankExec => PhysicalPlanClass::Relational,
367            PhysicalPlanKind::ProjectGraph
368            | PhysicalPlanKind::GraphAlgorithm
369            | PhysicalPlanKind::VectorSeedScan
370            | PhysicalPlanKind::ThreadRepairStatsExec => PhysicalPlanClass::Procedure,
371        }
372    }
373}
374
375impl PhysicalPlanClass {
376    pub fn all() -> &'static [Self] {
377        const ALL: &[PhysicalPlanClass] = &[
378            PhysicalPlanClass::Schema,
379            PhysicalPlanClass::Mutation,
380            PhysicalPlanClass::Access,
381            PhysicalPlanClass::Traversal,
382            PhysicalPlanClass::Relational,
383            PhysicalPlanClass::Procedure,
384        ];
385        ALL
386    }
387
388    pub fn as_str(self) -> &'static str {
389        match self {
390            PhysicalPlanClass::Schema => "schema",
391            PhysicalPlanClass::Mutation => "mutation",
392            PhysicalPlanClass::Access => "access",
393            PhysicalPlanClass::Traversal => "traversal",
394            PhysicalPlanClass::Relational => "relational",
395            PhysicalPlanClass::Procedure => "procedure",
396        }
397    }
398}
399
400impl FromStr for PhysicalPlanKind {
401    type Err = &'static str;
402
403    fn from_str(value: &str) -> Result<Self, Self::Err> {
404        Self::all()
405            .iter()
406            .copied()
407            .find(|kind| kind.as_str() == value)
408            .ok_or("unknown physical plan kind")
409    }
410}
411
412impl FromStr for PhysicalPlanClass {
413    type Err = &'static str;
414
415    fn from_str(value: &str) -> Result<Self, Self::Err> {
416        Self::all()
417            .iter()
418            .copied()
419            .find(|class| class.as_str() == value)
420            .ok_or("unknown physical plan class")
421    }
422}
423
424impl<'a, P> PlanChildren<'a, P> {
425    pub fn len(self) -> usize {
426        match self {
427            PlanChildren::None => 0,
428            PlanChildren::Unary(_) => 1,
429            PlanChildren::Binary(_, _) => 2,
430        }
431    }
432
433    pub fn is_empty(self) -> bool {
434        self.len() == 0
435    }
436}
437
438pub fn plan_operator_counts<P>(plan: &P) -> BTreeMap<String, usize>
439where
440    P: PhysicalPlanNode,
441{
442    let mut counts = BTreeMap::new();
443    visit_plan(plan, &mut |node| {
444        *counts.entry(node.kind().as_str().to_string()).or_default() += 1;
445    });
446    counts
447}
448
449pub fn plan_class_counts<P>(plan: &P) -> BTreeMap<String, usize>
450where
451    P: PhysicalPlanNode,
452{
453    let mut counts = BTreeMap::new();
454    visit_plan(plan, &mut |node| {
455        *counts.entry(node.class().as_str().to_string()).or_default() += 1;
456    });
457    counts
458}
459
460pub fn visit_plan<P>(plan: &P, visitor: &mut impl FnMut(&P))
461where
462    P: PhysicalPlanNode,
463{
464    visitor(plan);
465    match plan.children() {
466        PlanChildren::None => {}
467        PlanChildren::Unary(input) => visit_plan(input, visitor),
468        PlanChildren::Binary(left, right) => {
469            visit_plan(left, visitor);
470            visit_plan(right, visitor);
471        }
472    }
473}
474
475/// Visits a physical plan in pre-order and assigns each operator its plan-local identity.
476pub fn visit_plan_with_ids<P>(plan: &P, visitor: &mut impl FnMut(PhysicalOperatorId, &P))
477where
478    P: PhysicalPlanNode,
479{
480    fn visit<P>(
481        plan: &P,
482        next_ordinal: &mut usize,
483        visitor: &mut impl FnMut(PhysicalOperatorId, &P),
484    ) where
485        P: PhysicalPlanNode,
486    {
487        let operator_id = PhysicalOperatorId::from_ordinal(*next_ordinal);
488        *next_ordinal = next_ordinal
489            .checked_add(1)
490            .expect("physical plan operator count exceeds usize");
491        visitor(operator_id, plan);
492        match plan.children() {
493            PlanChildren::None => {}
494            PlanChildren::Unary(input) => visit(input, next_ordinal, visitor),
495            PlanChildren::Binary(left, right) => {
496                visit(left, next_ordinal, visitor);
497                visit(right, next_ordinal, visitor);
498            }
499        }
500    }
501
502    let mut next_ordinal = 0;
503    visit(plan, &mut next_ordinal, visitor);
504}
505
506#[cfg(test)]
507mod tests {
508    use super::{
509        plan_class_counts, plan_operator_counts, visit_plan, visit_plan_with_ids,
510        PhysicalPlanClass, PhysicalPlanKind, PhysicalPlanNode, PlanChildren,
511    };
512
513    #[test]
514    fn physical_plan_kind_exposes_stable_strings_and_classes() {
515        assert_eq!(PhysicalPlanKind::IndexNodeSeek.as_str(), "IndexNodeSeek");
516        assert_eq!(
517            PhysicalPlanKind::IndexNodeSeek.class(),
518            PhysicalPlanClass::Access
519        );
520        assert_eq!(
521            PhysicalPlanKind::AdjacencyExpandExec.class(),
522            PhysicalPlanClass::Traversal
523        );
524        assert_eq!(
525            PhysicalPlanKind::ProjectExec.class(),
526            PhysicalPlanClass::Relational
527        );
528    }
529
530    #[test]
531    fn physical_plan_kind_strings_round_trip_for_diagnostics() {
532        for kind in PhysicalPlanKind::all() {
533            assert_eq!(kind.as_str().parse::<PhysicalPlanKind>(), Ok(*kind));
534        }
535        assert!("UnknownExec".parse::<PhysicalPlanKind>().is_err());
536    }
537
538    #[test]
539    fn physical_plan_class_exposes_stable_strings() {
540        assert_eq!(PhysicalPlanClass::Schema.as_str(), "schema");
541        assert_eq!(PhysicalPlanClass::Mutation.as_str(), "mutation");
542        assert_eq!(PhysicalPlanClass::Access.as_str(), "access");
543        assert_eq!(PhysicalPlanClass::Traversal.as_str(), "traversal");
544        assert_eq!(PhysicalPlanClass::Relational.as_str(), "relational");
545        assert_eq!(PhysicalPlanClass::Procedure.as_str(), "procedure");
546    }
547
548    #[test]
549    fn physical_plan_class_strings_round_trip_for_diagnostics() {
550        for class in PhysicalPlanClass::all() {
551            assert_eq!(class.as_str().parse::<PhysicalPlanClass>(), Ok(*class));
552        }
553        assert!("unknown".parse::<PhysicalPlanClass>().is_err());
554    }
555
556    #[test]
557    fn plan_children_reports_arity_without_knowing_plan_type() {
558        assert_eq!(PlanChildren::<&str>::None.len(), 0);
559        assert!(PlanChildren::<&str>::None.is_empty());
560
561        let child = "scan";
562        assert_eq!(PlanChildren::Unary(&child).len(), 1);
563
564        let left = "left";
565        let right = "right";
566        assert_eq!(PlanChildren::Binary(&left, &right).len(), 2);
567    }
568
569    #[derive(Debug)]
570    enum TestPlan {
571        Seek,
572        Project(Box<TestPlan>),
573        Join(Box<TestPlan>, Box<TestPlan>),
574    }
575
576    impl PhysicalPlanNode for TestPlan {
577        fn kind(&self) -> PhysicalPlanKind {
578            match self {
579                TestPlan::Seek => PhysicalPlanKind::IndexNodeSeek,
580                TestPlan::Project(_) => PhysicalPlanKind::ProjectExec,
581                TestPlan::Join(_, _) => PhysicalPlanKind::NodeCartesianProductExec,
582            }
583        }
584
585        fn children(&self) -> PlanChildren<'_, Self> {
586            match self {
587                TestPlan::Seek => PlanChildren::None,
588                TestPlan::Project(input) => PlanChildren::Unary(input),
589                TestPlan::Join(left, right) => PlanChildren::Binary(left, right),
590            }
591        }
592    }
593
594    #[test]
595    fn plan_node_helpers_traverse_without_knowing_plan_payloads() {
596        let plan = TestPlan::Project(Box::new(TestPlan::Join(
597            Box::new(TestPlan::Seek),
598            Box::new(TestPlan::Seek),
599        )));
600
601        let mut visit_order = Vec::new();
602        visit_plan(&plan, &mut |node| {
603            visit_order.push(node.kind().as_str());
604        });
605
606        assert_eq!(
607            visit_order,
608            vec![
609                "ProjectExec",
610                "NodeCartesianProductExec",
611                "IndexNodeSeek",
612                "IndexNodeSeek"
613            ]
614        );
615        assert_eq!(plan_operator_counts(&plan)["IndexNodeSeek"], 2);
616        assert_eq!(plan_operator_counts(&plan)["ProjectExec"], 1);
617        assert_eq!(plan_class_counts(&plan)["access"], 2);
618        assert_eq!(plan_class_counts(&plan)["relational"], 2);
619
620        let mut identified = Vec::new();
621        visit_plan_with_ids(&plan, &mut |operator_id, node| {
622            identified.push((operator_id.ordinal(), node.kind().as_str()));
623        });
624        assert_eq!(
625            identified,
626            vec![
627                (0, "ProjectExec"),
628                (1, "NodeCartesianProductExec"),
629                (2, "IndexNodeSeek"),
630                (3, "IndexNodeSeek"),
631            ]
632        );
633    }
634}