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