Skip to main content

icydb_core/db/query/explain/
execution.rs

1//! Module: query::explain::execution
2//! Responsibility: stable execution-descriptor vocabulary for EXPLAIN.
3//! Does not own: logical plan projection or rendering logic.
4//! Boundary: execution descriptor types consumed by explain renderers.
5
6use crate::{
7    db::query::{
8        admission::QueryAdmissionSummary,
9        explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
10        plan::{AggregateKind, ResidualFilterShape},
11        trace::TraceReuseEvent,
12    },
13    value::Value,
14};
15use std::fmt::{self, Debug};
16
17#[cfg_attr(
18    doc,
19    doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
20)]
21#[derive(Clone, Default, Eq, PartialEq)]
22pub struct ExplainPropertyMap {
23    entries: Vec<(&'static str, Value)>,
24}
25
26impl ExplainPropertyMap {
27    /// Build an empty EXPLAIN property map.
28    #[must_use]
29    pub const fn new() -> Self {
30        Self {
31            entries: Vec::new(),
32        }
33    }
34
35    /// Insert or replace one stable property.
36    pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
37        match self
38            .entries
39            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
40        {
41            Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
42            Err(index) => {
43                self.entries.insert(index, (key, value));
44                None
45            }
46        }
47    }
48
49    /// Borrow one property value by key.
50    #[must_use]
51    pub fn get(&self, key: &str) -> Option<&Value> {
52        self.entries
53            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
54            .ok()
55            .map(|index| &self.entries[index].1)
56    }
57
58    /// Return whether the property map contains the given key.
59    #[must_use]
60    #[cfg(test)]
61    pub fn contains_key(&self, key: &str) -> bool {
62        self.get(key).is_some()
63    }
64
65    /// Return whether the property map is empty.
66    #[must_use]
67    pub const fn is_empty(&self) -> bool {
68        self.entries.is_empty()
69    }
70
71    /// Iterate over all stored properties in deterministic key order.
72    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
73        self.entries.iter().map(|(key, value)| (*key, value))
74    }
75}
76
77impl Debug for ExplainPropertyMap {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        let mut map = f.debug_map();
80        for (key, value) in self.iter() {
81            map.entry(&key, value);
82        }
83        map.finish()
84    }
85}
86
87/// Stable EXPLAIN node-property key vocabulary shared by descriptor builders
88/// and renderers.
89pub(in crate::db) mod property_keys {
90    pub(in crate::db) const ACCESS_ALTERNATIVES: &str = "acc_alts";
91    pub(in crate::db) const ACCESS_CHOICE: &str = "acc_choice";
92    pub(in crate::db) const ACCESS_REASON: &str = "acc_reason";
93    pub(in crate::db) const ACCESS_REJECTIONS: &str = "acc_reject";
94    pub(in crate::db) const AGGREGATE_CONTRACT: &str = "aggregate_contract";
95    pub(in crate::db) const AGGREGATE_PHYSICAL: &str = "aggregate_physical";
96    pub(in crate::db) const CONTINUATION_MODE: &str = "cont_mode";
97    pub(in crate::db) const COUNT_FOLD: &str = "count_fold";
98    pub(in crate::db) const COVERING_FIELDS: &str = "covering_fields";
99    pub(in crate::db) const COVERING_KIND: &str = "covering_kind";
100    pub(in crate::db) const COVERING_ORDER: &str = "covering_order";
101    pub(in crate::db) const COVERING_READ_KIND: &str = "cov_read_kind";
102    pub(in crate::db) const COVERING_READ_ROUTE: &str = "cov_read_route";
103    pub(in crate::db) const COVERING_SCAN_REASON: &str = "cov_scan_reason";
104    pub(in crate::db) const COVERING_SOURCES: &str = "covering_sources";
105    pub(in crate::db) const EXISTING_ROW_MODE: &str = "existing_row_mode";
106    #[cfg(feature = "sql-explain")]
107    pub(in crate::db) const FILTER_EXPR: &str = "filter_expr";
108    pub(in crate::db) const FAST_PATH: &str = "fast_path";
109    pub(in crate::db) const FAST_REASON: &str = "fast_reason";
110    pub(in crate::db) const FAST_REJECTIONS: &str = "fast_reject";
111    pub(in crate::db) const FETCH: &str = "fetch";
112    pub(in crate::db) const GROUPED_EXECUTION_MODE: &str = "grouped_execution_mode";
113    pub(in crate::db) const GROUPED_PLAN_FALLBACK_REASON: &str = "grouped_plan_fallback_reason";
114    pub(in crate::db) const GROUPED_ROUTE_ELIGIBLE: &str = "grouped_route_eligible";
115    pub(in crate::db) const GROUPED_ROUTE_OUTCOME: &str = "grouped_route_outcome";
116    pub(in crate::db) const GROUPED_ROUTE_REJECTION_REASON: &str = "grouped_route_rejection_reason";
117    #[cfg(feature = "sql-explain")]
118    pub(in crate::db) const AGGREGATE_DIRECT_COUNT_METADATA_ELIGIBLE: &str =
119        "aggregate_direct_count_metadata_eligible";
120    #[cfg(feature = "sql-explain")]
121    pub(in crate::db) const AGGREGATE_DIRECT_COUNT_PREFIXES: &str =
122        "aggregate_direct_count_prefixes";
123    pub(in crate::db) const INDEX: &str = "index";
124    pub(in crate::db) const LIMIT_STOP_AFTER: &str = "limit_stop_after";
125    pub(in crate::db) const OFFSET: &str = "offset";
126    pub(in crate::db) const ORDER_BY_INDEX: &str = "order_by_idx";
127    pub(in crate::db) const ORDER_BY_INDEX_HINT: &str = "order_by_idx_hint";
128    pub(in crate::db) const ORDER_ROUTE_MODE: &str = "ord_route_mode";
129    pub(in crate::db) const ORDER_ROUTE_REASON: &str = "ord_route_reason";
130    pub(in crate::db) const PREDICATE_INDEX_CAPABILITY: &str = "pred_idx_cap";
131    pub(in crate::db) const PREFIX_LEN: &str = "prefix_len";
132    pub(in crate::db) const PREFIX_VALUES: &str = "prefix_values";
133    pub(in crate::db) const PROJECTION_FIELD: &str = "proj_field";
134    pub(in crate::db) const PROJECTION_FIELDS: &str = "proj_fields";
135    #[cfg(feature = "sql-explain")]
136    pub(in crate::db) const PROJECTION_MATERIALIZATION: &str = "proj_materialization";
137    pub(in crate::db) const PROJECTION_MODE: &str = "proj_mode";
138    pub(in crate::db) const PROJECTION_PUSHDOWN: &str = "proj_pushdown";
139    pub(in crate::db) const PUSHDOWN: &str = "pushdown";
140    pub(in crate::db) const RESIDUAL_FILTER_SHAPE: &str = "residual_filter_shape";
141    pub(in crate::db) const RESUME_FROM: &str = "resume_from";
142    pub(in crate::db) const ROUTE_FAMILY: &str = "route_family";
143    pub(in crate::db) const ROUTE_OUTCOME: &str = "route_outcome";
144    pub(in crate::db) const ROUTE_REASON: &str = "route_reason";
145    pub(in crate::db) const SCAN_DIRECTION: &str = "scan_dir";
146    pub(in crate::db) const TERMINAL: &str = "terminal";
147    pub(in crate::db) const TERMINAL_FIELD: &str = "terminal_field";
148    pub(in crate::db) const TERMINAL_INDEX_ONLY: &str = "terminal_index_only";
149    pub(in crate::db) const TERMINAL_OUTPUT: &str = "terminal_output";
150    pub(in crate::db) const TERMINAL_PROJECTION_MODE: &str = "terminal_projection_mode";
151}
152
153/// Stable EXPLAIN scalar label vocabulary shared only where the same semantic
154/// label is intentionally projected on multiple surfaces.
155pub(in crate::db) mod property_values {
156    pub(in crate::db) const COVERING_READ: &str = "covering_read";
157    #[cfg(feature = "sql-explain")]
158    pub(in crate::db) const DIRECT_SLOT_ROW: &str = "direct_slot_row";
159    pub(in crate::db) const HYBRID_COVERING: &str = "hybrid_covering";
160    pub(in crate::db) const MATERIALIZED: &str = "materialized";
161    pub(in crate::db) const NONE: &str = "none";
162    pub(in crate::db) const PURE_COVERING: &str = "pure_covering";
163    #[cfg(feature = "sql-explain")]
164    pub(in crate::db) const SCALAR_PROJECTION: &str = "scalar_projection";
165    pub(in crate::db) const STRICT_ALL_OR_NONE: &str = "strict_all_or_none";
166}
167
168#[cfg_attr(
169    doc,
170    doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
171)]
172#[derive(Clone, Debug, Eq, PartialEq)]
173pub struct ExplainAggregateTerminalPlan {
174    pub(in crate::db) query: ExplainPlan,
175    pub(in crate::db) terminal: AggregateKind,
176    pub(in crate::db) execution: ExplainExecutionDescriptor,
177}
178
179#[cfg_attr(
180    doc,
181    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
182)]
183#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub enum ExplainExecutionOrderingSource {
185    AccessOrder,
186    Materialized,
187    IndexSeekFirst { fetch: usize },
188    IndexSeekLast { fetch: usize },
189}
190
191#[cfg_attr(
192    doc,
193    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
194)]
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
196pub enum ExplainExecutionMode {
197    Streaming,
198    Materialized,
199}
200
201#[cfg_attr(
202    doc,
203    doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
204)]
205#[derive(Clone, Debug, Eq, PartialEq)]
206pub struct ExplainExecutionDescriptor {
207    pub(in crate::db) access_strategy: ExplainAccessPath,
208    pub(in crate::db) covering_projection: bool,
209    pub(in crate::db) aggregation: AggregateKind,
210    pub(in crate::db) execution_mode: ExplainExecutionMode,
211    pub(in crate::db) ordering_source: ExplainExecutionOrderingSource,
212    pub(in crate::db) limit: Option<u32>,
213    pub(in crate::db) cursor: bool,
214    pub(in crate::db) node_properties: ExplainPropertyMap,
215}
216
217#[cfg_attr(
218    doc,
219    doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
220)]
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum ExplainExecutionNodeType {
223    ByKeyLookup,
224    ByKeysLookup,
225    PrimaryKeyRangeScan,
226    IndexPrefixScan,
227    IndexRangeScan,
228    IndexMultiLookup,
229    IndexBranchSet,
230    FullScan,
231    Union,
232    Intersection,
233    IndexPredicatePrefilter,
234    ResidualFilter,
235    OrderByAccessSatisfied,
236    OrderByMaterializedSort,
237    DistinctPreOrdered,
238    DistinctMaterialized,
239    ProjectionMaterialized,
240    CoveringRead,
241    LimitOffset,
242    CursorResume,
243    IndexRangeLimitPushdown,
244    TopNSeek,
245    AggregateCount,
246    AggregateExists,
247    AggregateMin,
248    AggregateMax,
249    AggregateFirst,
250    AggregateLast,
251    AggregateSum,
252    AggregateSeekFirst,
253    AggregateSeekLast,
254    GroupedAggregateHashMaterialized,
255    GroupedAggregateOrderedMaterialized,
256    SecondaryOrderPushdown,
257}
258
259#[cfg_attr(
260    doc,
261    doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
262)]
263#[derive(Clone, Debug, Eq, PartialEq)]
264pub struct ExplainExecutionNodeDescriptor {
265    pub(in crate::db) node_type: ExplainExecutionNodeType,
266    pub(in crate::db) execution_mode: ExplainExecutionMode,
267    pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
268    pub(in crate::db) predicate_pushdown: Option<String>,
269    pub(in crate::db) filter_expr: Option<String>,
270    pub(in crate::db) residual_filter_expr: Option<String>,
271    pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
272    pub(in crate::db) projection: Option<String>,
273    pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
274    pub(in crate::db) limit: Option<u32>,
275    pub(in crate::db) cursor: Option<bool>,
276    pub(in crate::db) covering_scan: Option<bool>,
277    pub(in crate::db) rows_expected: Option<u64>,
278    pub(in crate::db) children: Vec<Self>,
279    pub(in crate::db) node_properties: ExplainPropertyMap,
280}
281
282///
283/// FinalizedQueryDiagnostics
284///
285/// FinalizedQueryDiagnostics freezes one immutable execution-explain
286/// diagnostics artifact after descriptor assembly and plan-level diagnostics
287/// projection are complete.
288/// Session and SQL wrappers render this artifact directly instead of
289/// reconstructing verbose diagnostics from separate local line builders.
290///
291
292#[derive(Clone, Debug, Eq, PartialEq)]
293pub(in crate::db) struct FinalizedQueryDiagnostics {
294    pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
295    pub(in crate::db) admission: Option<QueryAdmissionSummary>,
296    pub(in crate::db) route_diagnostics: Vec<String>,
297    pub(in crate::db) logical_diagnostics: Vec<String>,
298    pub(in crate::db) reuse: Option<TraceReuseEvent>,
299}
300
301impl ExplainAggregateTerminalPlan {
302    /// Borrow the underlying query explain payload.
303    #[must_use]
304    pub const fn query(&self) -> &ExplainPlan {
305        &self.query
306    }
307
308    /// Return terminal aggregate kind.
309    #[must_use]
310    pub const fn terminal(&self) -> AggregateKind {
311        self.terminal
312    }
313
314    /// Borrow projected execution descriptor.
315    #[must_use]
316    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
317        &self.execution
318    }
319
320    #[must_use]
321    pub(in crate::db) const fn new(
322        query: ExplainPlan,
323        terminal: AggregateKind,
324        execution: ExplainExecutionDescriptor,
325    ) -> Self {
326        Self {
327            query,
328            terminal,
329            execution,
330        }
331    }
332}
333
334impl ExplainExecutionDescriptor {
335    /// Borrow projected access strategy.
336    #[must_use]
337    pub const fn access_strategy(&self) -> &ExplainAccessPath {
338        &self.access_strategy
339    }
340
341    /// Return whether projection can be served from index payload only.
342    #[must_use]
343    pub const fn covering_projection(&self) -> bool {
344        self.covering_projection
345    }
346
347    /// Return projected aggregate kind.
348    #[must_use]
349    pub const fn aggregation(&self) -> AggregateKind {
350        self.aggregation
351    }
352
353    /// Return projected execution mode.
354    #[must_use]
355    pub const fn execution_mode(&self) -> ExplainExecutionMode {
356        self.execution_mode
357    }
358
359    /// Return projected ordering source.
360    #[must_use]
361    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
362        self.ordering_source
363    }
364
365    /// Return projected execution limit.
366    #[must_use]
367    pub const fn limit(&self) -> Option<u32> {
368        self.limit
369    }
370
371    /// Return whether continuation was applied.
372    #[must_use]
373    pub const fn cursor(&self) -> bool {
374        self.cursor
375    }
376
377    /// Borrow projected execution node properties.
378    #[must_use]
379    pub const fn node_properties(&self) -> &ExplainPropertyMap {
380        &self.node_properties
381    }
382}
383
384impl FinalizedQueryDiagnostics {
385    /// Construct one immutable execution diagnostics artifact.
386    #[must_use]
387    pub(in crate::db) const fn new(
388        execution: ExplainExecutionNodeDescriptor,
389        route_diagnostics: Vec<String>,
390        logical_diagnostics: Vec<String>,
391        reuse: Option<TraceReuseEvent>,
392    ) -> Self {
393        Self {
394            execution,
395            admission: None,
396            route_diagnostics,
397            logical_diagnostics,
398            reuse,
399        }
400    }
401
402    /// Borrow the frozen execution descriptor carried by this artifact.
403    #[must_use]
404    pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
405        &self.execution
406    }
407
408    /// Attach an admission summary to this diagnostics artifact.
409    #[must_use]
410    pub(in crate::db) fn with_admission(mut self, admission: QueryAdmissionSummary) -> Self {
411        self.admission = Some(admission);
412        self
413    }
414
415    /// Borrow the admission summary carried by this artifact, if present.
416    #[must_use]
417    pub(in crate::db) const fn admission(&self) -> Option<&QueryAdmissionSummary> {
418        self.admission.as_ref()
419    }
420}
421
422/// Annotate one aggregate execution node with the shared semantic/physical
423/// identity vocabulary consumed by SQL, fluent, and JSON EXPLAIN surfaces.
424pub(in crate::db) fn annotate_aggregate_execution_identity_properties(
425    node_properties: &mut ExplainPropertyMap,
426    contract: &'static str,
427    physical: &'static str,
428) {
429    node_properties.insert(property_keys::AGGREGATE_CONTRACT, Value::from(contract));
430    node_properties.insert(property_keys::AGGREGATE_PHYSICAL, Value::from(physical));
431}
432
433impl ExplainAggregateTerminalPlan {
434    /// Build an execution-node descriptor for aggregate terminal plans.
435    #[must_use]
436    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
437        let mut node_properties = self.execution.node_properties.clone();
438        annotate_aggregate_execution_identity_properties(
439            &mut node_properties,
440            "singleton",
441            scalar_aggregate_physical_label(self.execution.ordering_source),
442        );
443
444        ExplainExecutionNodeDescriptor {
445            node_type: match self.execution.ordering_source {
446                ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
447                    ExplainExecutionNodeType::AggregateSeekFirst
448                }
449                ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
450                    ExplainExecutionNodeType::AggregateSeekLast
451                }
452                ExplainExecutionOrderingSource::AccessOrder
453                | ExplainExecutionOrderingSource::Materialized => {
454                    self.terminal.explain_execution_node_type()
455                }
456            },
457            execution_mode: self.execution.execution_mode,
458            access_strategy: Some(self.execution.access_strategy.clone()),
459            predicate_pushdown: None,
460            filter_expr: None,
461            residual_filter_expr: None,
462            residual_filter_predicate: None,
463            projection: None,
464            ordering_source: Some(self.execution.ordering_source),
465            limit: self.execution.limit,
466            cursor: Some(self.execution.cursor),
467            covering_scan: Some(self.execution.covering_projection),
468            rows_expected: None,
469            children: Vec::new(),
470            node_properties,
471        }
472    }
473}
474
475const fn scalar_aggregate_physical_label(
476    ordering_source: ExplainExecutionOrderingSource,
477) -> &'static str {
478    match ordering_source {
479        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "scalar_seek_first",
480        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "scalar_seek_last",
481        ExplainExecutionOrderingSource::AccessOrder
482        | ExplainExecutionOrderingSource::Materialized => "scalar_terminal",
483    }
484}
485
486impl AggregateKind {
487    /// Return the canonical explain execution-node type for this aggregate
488    /// terminal kind when no seek-first/seek-last override applies.
489    #[must_use]
490    pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
491        match self {
492            Self::Count => ExplainExecutionNodeType::AggregateCount,
493            Self::Exists => ExplainExecutionNodeType::AggregateExists,
494            Self::Min => ExplainExecutionNodeType::AggregateMin,
495            Self::Max => ExplainExecutionNodeType::AggregateMax,
496            Self::First => ExplainExecutionNodeType::AggregateFirst,
497            Self::Last => ExplainExecutionNodeType::AggregateLast,
498            Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
499        }
500    }
501}
502
503impl ExplainExecutionNodeType {
504    /// Return the stable string label used by explain renderers.
505    #[must_use]
506    pub const fn as_str(self) -> &'static str {
507        match self {
508            Self::ByKeyLookup => "ByKeyLookup",
509            Self::ByKeysLookup => "ByKeysLookup",
510            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
511            Self::IndexPrefixScan => "IndexPrefixScan",
512            Self::IndexRangeScan => "IndexRangeScan",
513            Self::IndexMultiLookup => "IndexMultiLookup",
514            Self::IndexBranchSet => "IndexBranchSet",
515            Self::FullScan => "FullScan",
516            Self::Union => "Union",
517            Self::Intersection => "Intersection",
518            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
519            Self::ResidualFilter => "ResidualFilter",
520            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
521            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
522            Self::DistinctPreOrdered => "DistinctPreOrdered",
523            Self::DistinctMaterialized => "DistinctMaterialized",
524            Self::ProjectionMaterialized => "ProjectionMaterialized",
525            Self::CoveringRead => "CoveringRead",
526            Self::LimitOffset => "LimitOffset",
527            Self::CursorResume => "CursorResume",
528            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
529            Self::TopNSeek => "TopNSeek",
530            Self::AggregateCount => "AggregateCount",
531            Self::AggregateExists => "AggregateExists",
532            Self::AggregateMin => "AggregateMin",
533            Self::AggregateMax => "AggregateMax",
534            Self::AggregateFirst => "AggregateFirst",
535            Self::AggregateLast => "AggregateLast",
536            Self::AggregateSum => "AggregateSum",
537            Self::AggregateSeekFirst => "AggregateSeekFirst",
538            Self::AggregateSeekLast => "AggregateSeekLast",
539            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
540            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
541            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
542        }
543    }
544
545    /// Return the owning execution layer label for this node type.
546    #[must_use]
547    pub const fn layer_label(self) -> &'static str {
548        crate::db::query::explain::nodes::layer_label(self)
549    }
550}
551
552impl ExplainExecutionNodeDescriptor {
553    /// Visit this execution-descriptor tree in deterministic preorder.
554    pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
555        visit(self);
556
557        for child in self.children() {
558            child.for_each_preorder(visit);
559        }
560    }
561
562    /// Return whether this descriptor tree contains the requested node type.
563    #[must_use]
564    #[cfg(test)]
565    pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
566        let mut found = false;
567        self.for_each_preorder(&mut |node| {
568            if node.node_type() == target {
569                found = true;
570            }
571        });
572
573        found
574    }
575
576    /// Return node type.
577    #[must_use]
578    pub const fn node_type(&self) -> ExplainExecutionNodeType {
579        self.node_type
580    }
581
582    /// Return execution mode.
583    #[must_use]
584    pub const fn execution_mode(&self) -> ExplainExecutionMode {
585        self.execution_mode
586    }
587
588    /// Borrow optional access strategy annotation.
589    #[must_use]
590    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
591        self.access_strategy.as_ref()
592    }
593
594    /// Borrow optional predicate pushdown annotation.
595    #[must_use]
596    pub fn predicate_pushdown(&self) -> Option<&str> {
597        self.predicate_pushdown.as_deref()
598    }
599
600    /// Borrow optional semantic scalar filter expression annotation.
601    #[must_use]
602    pub fn filter_expr(&self) -> Option<&str> {
603        self.filter_expr.as_deref()
604    }
605
606    /// Borrow the optional explicit residual scalar filter expression.
607    #[must_use]
608    pub fn residual_filter_expr(&self) -> Option<&str> {
609        self.residual_filter_expr.as_deref()
610    }
611
612    /// Borrow the optional derived residual predicate annotation emitted
613    /// alongside `filter_expr` when execution still benefits from predicate
614    /// pushdown labeling.
615    #[must_use]
616    pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
617        self.residual_filter_predicate.as_ref()
618    }
619
620    /// Return this node's residual-filter annotation shape.
621    #[must_use]
622    pub(in crate::db) const fn residual_filter_shape(&self) -> ResidualFilterShape {
623        ResidualFilterShape::from_presence(
624            self.residual_filter_expr.is_some(),
625            self.residual_filter_predicate.is_some(),
626        )
627    }
628
629    /// Return whether this node carries any residual filter annotation.
630    #[must_use]
631    pub const fn has_residual_filter(&self) -> bool {
632        !self.residual_filter_shape().is_absent()
633    }
634
635    /// Borrow optional projection annotation.
636    #[must_use]
637    pub fn projection(&self) -> Option<&str> {
638        self.projection.as_deref()
639    }
640
641    /// Return optional ordering source annotation.
642    #[must_use]
643    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
644        self.ordering_source
645    }
646
647    /// Return optional limit annotation.
648    #[must_use]
649    pub const fn limit(&self) -> Option<u32> {
650        self.limit
651    }
652
653    /// Return optional continuation annotation.
654    #[must_use]
655    pub const fn cursor(&self) -> Option<bool> {
656        self.cursor
657    }
658
659    /// Return optional covering-scan annotation.
660    #[must_use]
661    pub const fn covering_scan(&self) -> Option<bool> {
662        self.covering_scan
663    }
664
665    /// Return optional row-count expectation annotation.
666    #[must_use]
667    pub const fn rows_expected(&self) -> Option<u64> {
668        self.rows_expected
669    }
670
671    /// Borrow child execution nodes.
672    #[must_use]
673    pub const fn children(&self) -> &[Self] {
674        self.children.as_slice()
675    }
676
677    /// Borrow node properties.
678    #[must_use]
679    pub const fn node_properties(&self) -> &ExplainPropertyMap {
680        &self.node_properties
681    }
682}
683
684pub(in crate::db::query::explain) const fn execution_mode_label(
685    mode: ExplainExecutionMode,
686) -> &'static str {
687    match mode {
688        ExplainExecutionMode::Streaming => "Streaming",
689        ExplainExecutionMode::Materialized => "Materialized",
690    }
691}
692
693pub(in crate::db::query::explain) const fn ordering_source_label(
694    ordering_source: ExplainExecutionOrderingSource,
695) -> &'static str {
696    match ordering_source {
697        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
698        ExplainExecutionOrderingSource::Materialized => "Materialized",
699        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
700        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
701    }
702}
703
704///
705/// TESTS
706///
707
708#[cfg(test)]
709mod tests {
710    use crate::db::query::explain::{
711        ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
712        ExplainPropertyMap,
713    };
714
715    fn node(
716        node_type: ExplainExecutionNodeType,
717        children: Vec<ExplainExecutionNodeDescriptor>,
718    ) -> ExplainExecutionNodeDescriptor {
719        ExplainExecutionNodeDescriptor {
720            node_type,
721            execution_mode: ExplainExecutionMode::Materialized,
722            access_strategy: None,
723            predicate_pushdown: None,
724            filter_expr: None,
725            residual_filter_expr: None,
726            residual_filter_predicate: None,
727            projection: None,
728            ordering_source: None,
729            limit: None,
730            cursor: None,
731            covering_scan: None,
732            rows_expected: None,
733            children,
734            node_properties: ExplainPropertyMap::new(),
735        }
736    }
737
738    #[test]
739    fn execution_node_contains_type_scans_preorder_tree() {
740        let root = node(
741            ExplainExecutionNodeType::Union,
742            vec![
743                node(ExplainExecutionNodeType::FullScan, Vec::new()),
744                node(
745                    ExplainExecutionNodeType::Intersection,
746                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
747                ),
748            ],
749        );
750
751        assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
752        assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
753    }
754
755    #[test]
756    fn execution_node_preorder_visits_parent_before_children() {
757        let root = node(
758            ExplainExecutionNodeType::Union,
759            vec![
760                node(ExplainExecutionNodeType::FullScan, Vec::new()),
761                node(
762                    ExplainExecutionNodeType::Intersection,
763                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
764                ),
765            ],
766        );
767        let mut visited = Vec::new();
768
769        root.for_each_preorder(&mut |node| visited.push(node.node_type()));
770
771        assert_eq!(
772            visited,
773            vec![
774                ExplainExecutionNodeType::Union,
775                ExplainExecutionNodeType::FullScan,
776                ExplainExecutionNodeType::Intersection,
777                ExplainExecutionNodeType::ResidualFilter,
778            ],
779        );
780    }
781}