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