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