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