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