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,
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#[cfg_attr(
87    doc,
88    doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
89)]
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct ExplainAggregateTerminalPlan {
92    pub(in crate::db) query: ExplainPlan,
93    pub(in crate::db) terminal: AggregateKind,
94    pub(in crate::db) execution: ExplainExecutionDescriptor,
95}
96
97#[cfg_attr(
98    doc,
99    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
100)]
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub enum ExplainExecutionOrderingSource {
103    AccessOrder,
104    Materialized,
105    IndexSeekFirst { fetch: usize },
106    IndexSeekLast { fetch: usize },
107}
108
109#[cfg_attr(
110    doc,
111    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
112)]
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum ExplainExecutionMode {
115    Streaming,
116    Materialized,
117}
118
119#[cfg_attr(
120    doc,
121    doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
122)]
123#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct ExplainExecutionDescriptor {
125    pub(in crate::db) access_strategy: ExplainAccessPath,
126    pub(in crate::db) covering_projection: bool,
127    pub(in crate::db) aggregation: AggregateKind,
128    pub(in crate::db) execution_mode: ExplainExecutionMode,
129    pub(in crate::db) ordering_source: ExplainExecutionOrderingSource,
130    pub(in crate::db) limit: Option<u32>,
131    pub(in crate::db) cursor: bool,
132    pub(in crate::db) node_properties: ExplainPropertyMap,
133}
134
135#[cfg_attr(
136    doc,
137    doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
138)]
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub enum ExplainExecutionNodeType {
141    ByKeyLookup,
142    ByKeysLookup,
143    PrimaryKeyRangeScan,
144    IndexPrefixScan,
145    IndexRangeScan,
146    IndexMultiLookup,
147    IndexBranchSet,
148    FullScan,
149    Union,
150    Intersection,
151    IndexPredicatePrefilter,
152    ResidualFilter,
153    OrderByAccessSatisfied,
154    OrderByMaterializedSort,
155    DistinctPreOrdered,
156    DistinctMaterialized,
157    ProjectionMaterialized,
158    CoveringRead,
159    LimitOffset,
160    CursorResume,
161    IndexRangeLimitPushdown,
162    TopNSeek,
163    AggregateCount,
164    AggregateExists,
165    AggregateMin,
166    AggregateMax,
167    AggregateFirst,
168    AggregateLast,
169    AggregateSum,
170    AggregateSeekFirst,
171    AggregateSeekLast,
172    GroupedAggregateHashMaterialized,
173    GroupedAggregateOrderedMaterialized,
174    SecondaryOrderPushdown,
175}
176
177#[cfg_attr(
178    doc,
179    doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
180)]
181#[derive(Clone, Debug, Eq, PartialEq)]
182pub struct ExplainExecutionNodeDescriptor {
183    pub(in crate::db) node_type: ExplainExecutionNodeType,
184    pub(in crate::db) execution_mode: ExplainExecutionMode,
185    pub(in crate::db) access_strategy: Option<ExplainAccessPath>,
186    pub(in crate::db) predicate_pushdown: Option<String>,
187    pub(in crate::db) filter_expr: Option<String>,
188    pub(in crate::db) residual_filter_expr: Option<String>,
189    pub(in crate::db) residual_filter_predicate: Option<ExplainPredicate>,
190    pub(in crate::db) projection: Option<String>,
191    pub(in crate::db) ordering_source: Option<ExplainExecutionOrderingSource>,
192    pub(in crate::db) limit: Option<u32>,
193    pub(in crate::db) cursor: Option<bool>,
194    pub(in crate::db) covering_scan: Option<bool>,
195    pub(in crate::db) rows_expected: Option<u64>,
196    pub(in crate::db) children: Vec<Self>,
197    pub(in crate::db) node_properties: ExplainPropertyMap,
198}
199
200///
201/// FinalizedQueryDiagnostics
202///
203/// FinalizedQueryDiagnostics freezes one immutable execution-explain
204/// diagnostics artifact after descriptor assembly and plan-level diagnostics
205/// projection are complete.
206/// Session and SQL wrappers render this artifact directly instead of
207/// reconstructing verbose diagnostics from separate local line builders.
208///
209
210#[derive(Clone, Debug, Eq, PartialEq)]
211pub(in crate::db) struct FinalizedQueryDiagnostics {
212    pub(in crate::db) execution: ExplainExecutionNodeDescriptor,
213    pub(in crate::db) route_diagnostics: Vec<String>,
214    pub(in crate::db) logical_diagnostics: Vec<String>,
215    pub(in crate::db) reuse: Option<TraceReuseEvent>,
216}
217
218impl ExplainAggregateTerminalPlan {
219    /// Borrow the underlying query explain payload.
220    #[must_use]
221    pub const fn query(&self) -> &ExplainPlan {
222        &self.query
223    }
224
225    /// Return terminal aggregate kind.
226    #[must_use]
227    pub const fn terminal(&self) -> AggregateKind {
228        self.terminal
229    }
230
231    /// Borrow projected execution descriptor.
232    #[must_use]
233    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
234        &self.execution
235    }
236
237    #[must_use]
238    pub(in crate::db) const fn new(
239        query: ExplainPlan,
240        terminal: AggregateKind,
241        execution: ExplainExecutionDescriptor,
242    ) -> Self {
243        Self {
244            query,
245            terminal,
246            execution,
247        }
248    }
249}
250
251impl ExplainExecutionDescriptor {
252    /// Borrow projected access strategy.
253    #[must_use]
254    pub const fn access_strategy(&self) -> &ExplainAccessPath {
255        &self.access_strategy
256    }
257
258    /// Return whether projection can be served from index payload only.
259    #[must_use]
260    pub const fn covering_projection(&self) -> bool {
261        self.covering_projection
262    }
263
264    /// Return projected aggregate kind.
265    #[must_use]
266    pub const fn aggregation(&self) -> AggregateKind {
267        self.aggregation
268    }
269
270    /// Return projected execution mode.
271    #[must_use]
272    pub const fn execution_mode(&self) -> ExplainExecutionMode {
273        self.execution_mode
274    }
275
276    /// Return projected ordering source.
277    #[must_use]
278    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
279        self.ordering_source
280    }
281
282    /// Return projected execution limit.
283    #[must_use]
284    pub const fn limit(&self) -> Option<u32> {
285        self.limit
286    }
287
288    /// Return whether continuation was applied.
289    #[must_use]
290    pub const fn cursor(&self) -> bool {
291        self.cursor
292    }
293
294    /// Borrow projected execution node properties.
295    #[must_use]
296    pub const fn node_properties(&self) -> &ExplainPropertyMap {
297        &self.node_properties
298    }
299}
300
301impl FinalizedQueryDiagnostics {
302    /// Construct one immutable execution diagnostics artifact.
303    #[must_use]
304    pub(in crate::db) const fn new(
305        execution: ExplainExecutionNodeDescriptor,
306        route_diagnostics: Vec<String>,
307        logical_diagnostics: Vec<String>,
308        reuse: Option<TraceReuseEvent>,
309    ) -> Self {
310        Self {
311            execution,
312            route_diagnostics,
313            logical_diagnostics,
314            reuse,
315        }
316    }
317
318    /// Borrow the frozen execution descriptor carried by this artifact.
319    #[must_use]
320    pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
321        &self.execution
322    }
323}
324
325impl ExplainAggregateTerminalPlan {
326    /// Build an execution-node descriptor for aggregate terminal plans.
327    #[must_use]
328    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
329        ExplainExecutionNodeDescriptor {
330            node_type: match self.execution.ordering_source {
331                ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
332                    ExplainExecutionNodeType::AggregateSeekFirst
333                }
334                ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
335                    ExplainExecutionNodeType::AggregateSeekLast
336                }
337                ExplainExecutionOrderingSource::AccessOrder
338                | ExplainExecutionOrderingSource::Materialized => {
339                    self.terminal.explain_execution_node_type()
340                }
341            },
342            execution_mode: self.execution.execution_mode,
343            access_strategy: Some(self.execution.access_strategy.clone()),
344            predicate_pushdown: None,
345            filter_expr: None,
346            residual_filter_expr: None,
347            residual_filter_predicate: None,
348            projection: None,
349            ordering_source: Some(self.execution.ordering_source),
350            limit: self.execution.limit,
351            cursor: Some(self.execution.cursor),
352            covering_scan: Some(self.execution.covering_projection),
353            rows_expected: None,
354            children: Vec::new(),
355            node_properties: self.execution.node_properties.clone(),
356        }
357    }
358}
359
360impl AggregateKind {
361    /// Return the canonical explain execution-node type for this aggregate
362    /// terminal kind when no seek-first/seek-last override applies.
363    #[must_use]
364    pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
365        match self {
366            Self::Count => ExplainExecutionNodeType::AggregateCount,
367            Self::Exists => ExplainExecutionNodeType::AggregateExists,
368            Self::Min => ExplainExecutionNodeType::AggregateMin,
369            Self::Max => ExplainExecutionNodeType::AggregateMax,
370            Self::First => ExplainExecutionNodeType::AggregateFirst,
371            Self::Last => ExplainExecutionNodeType::AggregateLast,
372            Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
373        }
374    }
375}
376
377impl ExplainExecutionNodeType {
378    /// Return the stable string label used by explain renderers.
379    #[must_use]
380    pub const fn as_str(self) -> &'static str {
381        match self {
382            Self::ByKeyLookup => "ByKeyLookup",
383            Self::ByKeysLookup => "ByKeysLookup",
384            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
385            Self::IndexPrefixScan => "IndexPrefixScan",
386            Self::IndexRangeScan => "IndexRangeScan",
387            Self::IndexMultiLookup => "IndexMultiLookup",
388            Self::IndexBranchSet => "IndexBranchSet",
389            Self::FullScan => "FullScan",
390            Self::Union => "Union",
391            Self::Intersection => "Intersection",
392            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
393            Self::ResidualFilter => "ResidualFilter",
394            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
395            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
396            Self::DistinctPreOrdered => "DistinctPreOrdered",
397            Self::DistinctMaterialized => "DistinctMaterialized",
398            Self::ProjectionMaterialized => "ProjectionMaterialized",
399            Self::CoveringRead => "CoveringRead",
400            Self::LimitOffset => "LimitOffset",
401            Self::CursorResume => "CursorResume",
402            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
403            Self::TopNSeek => "TopNSeek",
404            Self::AggregateCount => "AggregateCount",
405            Self::AggregateExists => "AggregateExists",
406            Self::AggregateMin => "AggregateMin",
407            Self::AggregateMax => "AggregateMax",
408            Self::AggregateFirst => "AggregateFirst",
409            Self::AggregateLast => "AggregateLast",
410            Self::AggregateSum => "AggregateSum",
411            Self::AggregateSeekFirst => "AggregateSeekFirst",
412            Self::AggregateSeekLast => "AggregateSeekLast",
413            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
414            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
415            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
416        }
417    }
418
419    /// Return the owning execution layer label for this node type.
420    #[must_use]
421    pub const fn layer_label(self) -> &'static str {
422        crate::db::query::explain::nodes::layer_label(self)
423    }
424}
425
426impl ExplainExecutionNodeDescriptor {
427    /// Visit this execution-descriptor tree in deterministic preorder.
428    pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
429        visit(self);
430
431        for child in self.children() {
432            child.for_each_preorder(visit);
433        }
434    }
435
436    /// Return whether this descriptor tree contains the requested node type.
437    #[must_use]
438    pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
439        let mut found = false;
440        self.for_each_preorder(&mut |node| {
441            if node.node_type() == target {
442                found = true;
443            }
444        });
445
446        found
447    }
448
449    /// Return node type.
450    #[must_use]
451    pub const fn node_type(&self) -> ExplainExecutionNodeType {
452        self.node_type
453    }
454
455    /// Return execution mode.
456    #[must_use]
457    pub const fn execution_mode(&self) -> ExplainExecutionMode {
458        self.execution_mode
459    }
460
461    /// Borrow optional access strategy annotation.
462    #[must_use]
463    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
464        self.access_strategy.as_ref()
465    }
466
467    /// Borrow optional predicate pushdown annotation.
468    #[must_use]
469    pub fn predicate_pushdown(&self) -> Option<&str> {
470        self.predicate_pushdown.as_deref()
471    }
472
473    /// Borrow optional semantic scalar filter expression annotation.
474    #[must_use]
475    pub fn filter_expr(&self) -> Option<&str> {
476        self.filter_expr.as_deref()
477    }
478
479    /// Borrow the optional explicit residual scalar filter expression.
480    #[must_use]
481    pub fn residual_filter_expr(&self) -> Option<&str> {
482        self.residual_filter_expr.as_deref()
483    }
484
485    /// Borrow the optional derived residual predicate annotation emitted
486    /// alongside `filter_expr` when execution still benefits from predicate
487    /// pushdown labeling.
488    #[must_use]
489    pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
490        self.residual_filter_predicate.as_ref()
491    }
492
493    /// Borrow optional projection annotation.
494    #[must_use]
495    pub fn projection(&self) -> Option<&str> {
496        self.projection.as_deref()
497    }
498
499    /// Return optional ordering source annotation.
500    #[must_use]
501    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
502        self.ordering_source
503    }
504
505    /// Return optional limit annotation.
506    #[must_use]
507    pub const fn limit(&self) -> Option<u32> {
508        self.limit
509    }
510
511    /// Return optional continuation annotation.
512    #[must_use]
513    pub const fn cursor(&self) -> Option<bool> {
514        self.cursor
515    }
516
517    /// Return optional covering-scan annotation.
518    #[must_use]
519    pub const fn covering_scan(&self) -> Option<bool> {
520        self.covering_scan
521    }
522
523    /// Return optional row-count expectation annotation.
524    #[must_use]
525    pub const fn rows_expected(&self) -> Option<u64> {
526        self.rows_expected
527    }
528
529    /// Borrow child execution nodes.
530    #[must_use]
531    pub const fn children(&self) -> &[Self] {
532        self.children.as_slice()
533    }
534
535    /// Borrow node properties.
536    #[must_use]
537    pub const fn node_properties(&self) -> &ExplainPropertyMap {
538        &self.node_properties
539    }
540}
541
542pub(in crate::db::query::explain) const fn execution_mode_label(
543    mode: ExplainExecutionMode,
544) -> &'static str {
545    match mode {
546        ExplainExecutionMode::Streaming => "Streaming",
547        ExplainExecutionMode::Materialized => "Materialized",
548    }
549}
550
551pub(in crate::db::query::explain) const fn ordering_source_label(
552    ordering_source: ExplainExecutionOrderingSource,
553) -> &'static str {
554    match ordering_source {
555        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
556        ExplainExecutionOrderingSource::Materialized => "Materialized",
557        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
558        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
559    }
560}
561
562///
563/// TESTS
564///
565
566#[cfg(test)]
567mod tests {
568    use crate::db::query::explain::{
569        ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
570        ExplainPropertyMap,
571    };
572
573    fn node(
574        node_type: ExplainExecutionNodeType,
575        children: Vec<ExplainExecutionNodeDescriptor>,
576    ) -> ExplainExecutionNodeDescriptor {
577        ExplainExecutionNodeDescriptor {
578            node_type,
579            execution_mode: ExplainExecutionMode::Materialized,
580            access_strategy: None,
581            predicate_pushdown: None,
582            filter_expr: None,
583            residual_filter_expr: None,
584            residual_filter_predicate: None,
585            projection: None,
586            ordering_source: None,
587            limit: None,
588            cursor: None,
589            covering_scan: None,
590            rows_expected: None,
591            children,
592            node_properties: ExplainPropertyMap::new(),
593        }
594    }
595
596    #[test]
597    fn execution_node_contains_type_scans_preorder_tree() {
598        let root = node(
599            ExplainExecutionNodeType::Union,
600            vec![
601                node(ExplainExecutionNodeType::FullScan, Vec::new()),
602                node(
603                    ExplainExecutionNodeType::Intersection,
604                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
605                ),
606            ],
607        );
608
609        assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
610        assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
611    }
612
613    #[test]
614    fn execution_node_preorder_visits_parent_before_children() {
615        let root = node(
616            ExplainExecutionNodeType::Union,
617            vec![
618                node(ExplainExecutionNodeType::FullScan, Vec::new()),
619                node(
620                    ExplainExecutionNodeType::Intersection,
621                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
622                ),
623            ],
624        );
625        let mut visited = Vec::new();
626
627        root.for_each_preorder(&mut |node| visited.push(node.node_type()));
628
629        assert_eq!(
630            visited,
631            vec![
632                ExplainExecutionNodeType::Union,
633                ExplainExecutionNodeType::FullScan,
634                ExplainExecutionNodeType::Intersection,
635                ExplainExecutionNodeType::ResidualFilter,
636            ],
637        );
638    }
639}