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