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                    self.terminal.explain_execution_node_type()
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 AggregateKind {
359    /// Return the canonical explain execution-node type for this aggregate
360    /// terminal kind when no seek-first/seek-last override applies.
361    #[must_use]
362    pub(in crate::db) const fn explain_execution_node_type(self) -> ExplainExecutionNodeType {
363        match self {
364            Self::Count => ExplainExecutionNodeType::AggregateCount,
365            Self::Exists => ExplainExecutionNodeType::AggregateExists,
366            Self::Min => ExplainExecutionNodeType::AggregateMin,
367            Self::Max => ExplainExecutionNodeType::AggregateMax,
368            Self::First => ExplainExecutionNodeType::AggregateFirst,
369            Self::Last => ExplainExecutionNodeType::AggregateLast,
370            Self::Sum | Self::Avg => ExplainExecutionNodeType::AggregateSum,
371        }
372    }
373}
374
375impl ExplainExecutionNodeType {
376    /// Return the stable string label used by explain renderers.
377    #[must_use]
378    pub const fn as_str(self) -> &'static str {
379        match self {
380            Self::ByKeyLookup => "ByKeyLookup",
381            Self::ByKeysLookup => "ByKeysLookup",
382            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
383            Self::IndexPrefixScan => "IndexPrefixScan",
384            Self::IndexRangeScan => "IndexRangeScan",
385            Self::IndexMultiLookup => "IndexMultiLookup",
386            Self::FullScan => "FullScan",
387            Self::Union => "Union",
388            Self::Intersection => "Intersection",
389            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
390            Self::ResidualFilter => "ResidualFilter",
391            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
392            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
393            Self::DistinctPreOrdered => "DistinctPreOrdered",
394            Self::DistinctMaterialized => "DistinctMaterialized",
395            Self::ProjectionMaterialized => "ProjectionMaterialized",
396            Self::CoveringRead => "CoveringRead",
397            Self::LimitOffset => "LimitOffset",
398            Self::CursorResume => "CursorResume",
399            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
400            Self::TopNSeek => "TopNSeek",
401            Self::AggregateCount => "AggregateCount",
402            Self::AggregateExists => "AggregateExists",
403            Self::AggregateMin => "AggregateMin",
404            Self::AggregateMax => "AggregateMax",
405            Self::AggregateFirst => "AggregateFirst",
406            Self::AggregateLast => "AggregateLast",
407            Self::AggregateSum => "AggregateSum",
408            Self::AggregateSeekFirst => "AggregateSeekFirst",
409            Self::AggregateSeekLast => "AggregateSeekLast",
410            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
411            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
412            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
413        }
414    }
415
416    /// Return the owning execution layer label for this node type.
417    #[must_use]
418    pub const fn layer_label(self) -> &'static str {
419        crate::db::query::explain::nodes::layer_label(self)
420    }
421}
422
423impl ExplainExecutionNodeDescriptor {
424    /// Visit this execution-descriptor tree in deterministic preorder.
425    pub(in crate::db) fn for_each_preorder(&self, visit: &mut impl FnMut(&Self)) {
426        visit(self);
427
428        for child in self.children() {
429            child.for_each_preorder(visit);
430        }
431    }
432
433    /// Return whether this descriptor tree contains the requested node type.
434    #[must_use]
435    pub(in crate::db) fn contains_type(&self, target: ExplainExecutionNodeType) -> bool {
436        let mut found = false;
437        self.for_each_preorder(&mut |node| {
438            if node.node_type() == target {
439                found = true;
440            }
441        });
442
443        found
444    }
445
446    /// Return node type.
447    #[must_use]
448    pub const fn node_type(&self) -> ExplainExecutionNodeType {
449        self.node_type
450    }
451
452    /// Return execution mode.
453    #[must_use]
454    pub const fn execution_mode(&self) -> ExplainExecutionMode {
455        self.execution_mode
456    }
457
458    /// Borrow optional access strategy annotation.
459    #[must_use]
460    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
461        self.access_strategy.as_ref()
462    }
463
464    /// Borrow optional predicate pushdown annotation.
465    #[must_use]
466    pub fn predicate_pushdown(&self) -> Option<&str> {
467        self.predicate_pushdown.as_deref()
468    }
469
470    /// Borrow optional semantic scalar filter expression annotation.
471    #[must_use]
472    pub fn filter_expr(&self) -> Option<&str> {
473        self.filter_expr.as_deref()
474    }
475
476    /// Borrow the optional explicit residual scalar filter expression.
477    #[must_use]
478    pub fn residual_filter_expr(&self) -> Option<&str> {
479        self.residual_filter_expr.as_deref()
480    }
481
482    /// Borrow the optional derived residual predicate annotation emitted
483    /// alongside `filter_expr` when execution still benefits from predicate
484    /// pushdown labeling.
485    #[must_use]
486    pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
487        self.residual_filter_predicate.as_ref()
488    }
489
490    /// Borrow optional projection annotation.
491    #[must_use]
492    pub fn projection(&self) -> Option<&str> {
493        self.projection.as_deref()
494    }
495
496    /// Return optional ordering source annotation.
497    #[must_use]
498    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
499        self.ordering_source
500    }
501
502    /// Return optional limit annotation.
503    #[must_use]
504    pub const fn limit(&self) -> Option<u32> {
505        self.limit
506    }
507
508    /// Return optional continuation annotation.
509    #[must_use]
510    pub const fn cursor(&self) -> Option<bool> {
511        self.cursor
512    }
513
514    /// Return optional covering-scan annotation.
515    #[must_use]
516    pub const fn covering_scan(&self) -> Option<bool> {
517        self.covering_scan
518    }
519
520    /// Return optional row-count expectation annotation.
521    #[must_use]
522    pub const fn rows_expected(&self) -> Option<u64> {
523        self.rows_expected
524    }
525
526    /// Borrow child execution nodes.
527    #[must_use]
528    pub const fn children(&self) -> &[Self] {
529        self.children.as_slice()
530    }
531
532    /// Borrow node properties.
533    #[must_use]
534    pub const fn node_properties(&self) -> &ExplainPropertyMap {
535        &self.node_properties
536    }
537}
538
539pub(in crate::db::query::explain) const fn execution_mode_label(
540    mode: ExplainExecutionMode,
541) -> &'static str {
542    match mode {
543        ExplainExecutionMode::Streaming => "Streaming",
544        ExplainExecutionMode::Materialized => "Materialized",
545    }
546}
547
548pub(in crate::db::query::explain) const fn ordering_source_label(
549    ordering_source: ExplainExecutionOrderingSource,
550) -> &'static str {
551    match ordering_source {
552        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
553        ExplainExecutionOrderingSource::Materialized => "Materialized",
554        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
555        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
556    }
557}
558
559///
560/// TESTS
561///
562
563#[cfg(test)]
564mod tests {
565    use crate::db::query::explain::{
566        ExplainExecutionMode, ExplainExecutionNodeDescriptor, ExplainExecutionNodeType,
567        ExplainPropertyMap,
568    };
569
570    fn node(
571        node_type: ExplainExecutionNodeType,
572        children: Vec<ExplainExecutionNodeDescriptor>,
573    ) -> ExplainExecutionNodeDescriptor {
574        ExplainExecutionNodeDescriptor {
575            node_type,
576            execution_mode: ExplainExecutionMode::Materialized,
577            access_strategy: None,
578            predicate_pushdown: None,
579            filter_expr: None,
580            residual_filter_expr: None,
581            residual_filter_predicate: None,
582            projection: None,
583            ordering_source: None,
584            limit: None,
585            cursor: None,
586            covering_scan: None,
587            rows_expected: None,
588            children,
589            node_properties: ExplainPropertyMap::new(),
590        }
591    }
592
593    #[test]
594    fn execution_node_contains_type_scans_preorder_tree() {
595        let root = node(
596            ExplainExecutionNodeType::Union,
597            vec![
598                node(ExplainExecutionNodeType::FullScan, Vec::new()),
599                node(
600                    ExplainExecutionNodeType::Intersection,
601                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
602                ),
603            ],
604        );
605
606        assert!(root.contains_type(ExplainExecutionNodeType::ResidualFilter));
607        assert!(!root.contains_type(ExplainExecutionNodeType::TopNSeek));
608    }
609
610    #[test]
611    fn execution_node_preorder_visits_parent_before_children() {
612        let root = node(
613            ExplainExecutionNodeType::Union,
614            vec![
615                node(ExplainExecutionNodeType::FullScan, Vec::new()),
616                node(
617                    ExplainExecutionNodeType::Intersection,
618                    vec![node(ExplainExecutionNodeType::ResidualFilter, Vec::new())],
619                ),
620            ],
621        );
622        let mut visited = Vec::new();
623
624        root.for_each_preorder(&mut |node| visited.push(node.node_type()));
625
626        assert_eq!(
627            visited,
628            vec![
629                ExplainExecutionNodeType::Union,
630                ExplainExecutionNodeType::FullScan,
631                ExplainExecutionNodeType::Intersection,
632                ExplainExecutionNodeType::ResidualFilter,
633            ],
634        );
635    }
636}