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