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    },
11    value::Value,
12};
13use std::fmt::{self, Debug};
14
15#[cfg_attr(
16    doc,
17    doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
18)]
19#[derive(Clone, Default, Eq, PartialEq)]
20pub struct ExplainPropertyMap {
21    entries: Vec<(&'static str, Value)>,
22}
23
24impl ExplainPropertyMap {
25    /// Build an empty EXPLAIN property map.
26    #[must_use]
27    pub const fn new() -> Self {
28        Self {
29            entries: Vec::new(),
30        }
31    }
32
33    /// Insert or replace one stable property.
34    pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
35        match self
36            .entries
37            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
38        {
39            Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
40            Err(index) => {
41                self.entries.insert(index, (key, value));
42                None
43            }
44        }
45    }
46
47    /// Borrow one property value by key.
48    #[must_use]
49    pub fn get(&self, key: &str) -> Option<&Value> {
50        self.entries
51            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
52            .ok()
53            .map(|index| &self.entries[index].1)
54    }
55
56    /// Return whether the property map contains the given key.
57    #[must_use]
58    pub fn contains_key(&self, key: &str) -> bool {
59        self.get(key).is_some()
60    }
61
62    /// Return whether the property map is empty.
63    #[must_use]
64    pub const fn is_empty(&self) -> bool {
65        self.entries.is_empty()
66    }
67
68    /// Iterate over all stored properties in deterministic key order.
69    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
70        self.entries.iter().map(|(key, value)| (*key, value))
71    }
72}
73
74impl Debug for ExplainPropertyMap {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        let mut map = f.debug_map();
77        for (key, value) in self.iter() {
78            map.entry(&key, value);
79        }
80        map.finish()
81    }
82}
83
84#[cfg_attr(
85    doc,
86    doc = "ExplainAggregateTerminalRoute\n\nScalar aggregate route label used by EXPLAIN output."
87)]
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum ExplainAggregateTerminalRoute {
90    Standard,
91    IndexSeekFirst { fetch: usize },
92    IndexSeekLast { fetch: usize },
93}
94
95#[cfg_attr(
96    doc,
97    doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
98)]
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct ExplainAggregateTerminalPlan {
101    pub(crate) query: ExplainPlan,
102    pub(crate) terminal: AggregateKind,
103    pub(crate) route: ExplainAggregateTerminalRoute,
104    pub(crate) execution: ExplainExecutionDescriptor,
105}
106
107#[cfg_attr(
108    doc,
109    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
110)]
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum ExplainExecutionOrderingSource {
113    AccessOrder,
114    Materialized,
115    IndexSeekFirst { fetch: usize },
116    IndexSeekLast { fetch: usize },
117}
118
119#[cfg_attr(
120    doc,
121    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
122)]
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum ExplainExecutionMode {
125    Streaming,
126    Materialized,
127}
128
129#[cfg_attr(
130    doc,
131    doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
132)]
133#[derive(Clone, Debug, Eq, PartialEq)]
134pub struct ExplainExecutionDescriptor {
135    pub(crate) access_strategy: ExplainAccessPath,
136    pub(crate) covering_projection: bool,
137    pub(crate) aggregation: AggregateKind,
138    pub(crate) execution_mode: ExplainExecutionMode,
139    pub(crate) ordering_source: ExplainExecutionOrderingSource,
140    pub(crate) limit: Option<u32>,
141    pub(crate) cursor: bool,
142    pub(crate) node_properties: ExplainPropertyMap,
143}
144
145#[cfg_attr(
146    doc,
147    doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
148)]
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub enum ExplainExecutionNodeType {
151    ByKeyLookup,
152    ByKeysLookup,
153    PrimaryKeyRangeScan,
154    IndexPrefixScan,
155    IndexRangeScan,
156    IndexMultiLookup,
157    FullScan,
158    Union,
159    Intersection,
160    IndexPredicatePrefilter,
161    ResidualPredicateFilter,
162    OrderByAccessSatisfied,
163    OrderByMaterializedSort,
164    DistinctPreOrdered,
165    DistinctMaterialized,
166    ProjectionMaterialized,
167    CoveringRead,
168    LimitOffset,
169    CursorResume,
170    IndexRangeLimitPushdown,
171    TopNSeek,
172    AggregateCount,
173    AggregateExists,
174    AggregateMin,
175    AggregateMax,
176    AggregateFirst,
177    AggregateLast,
178    AggregateSum,
179    AggregateSeekFirst,
180    AggregateSeekLast,
181    GroupedAggregateHashMaterialized,
182    GroupedAggregateOrderedMaterialized,
183    SecondaryOrderPushdown,
184}
185
186#[cfg_attr(
187    doc,
188    doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
189)]
190#[derive(Clone, Debug, Eq, PartialEq)]
191pub struct ExplainExecutionNodeDescriptor {
192    pub(crate) node_type: ExplainExecutionNodeType,
193    pub(crate) execution_mode: ExplainExecutionMode,
194    pub(crate) access_strategy: Option<ExplainAccessPath>,
195    pub(crate) predicate_pushdown: Option<String>,
196    pub(crate) residual_predicate: Option<ExplainPredicate>,
197    pub(crate) projection: Option<String>,
198    pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
199    pub(crate) limit: Option<u32>,
200    pub(crate) cursor: Option<bool>,
201    pub(crate) covering_scan: Option<bool>,
202    pub(crate) rows_expected: Option<u64>,
203    pub(crate) children: Vec<Self>,
204    pub(crate) node_properties: ExplainPropertyMap,
205}
206
207impl ExplainAggregateTerminalPlan {
208    /// Borrow the underlying query explain payload.
209    #[must_use]
210    pub const fn query(&self) -> &ExplainPlan {
211        &self.query
212    }
213
214    /// Return terminal aggregate kind.
215    #[must_use]
216    pub const fn terminal(&self) -> AggregateKind {
217        self.terminal
218    }
219
220    /// Return projected aggregate terminal route.
221    #[must_use]
222    pub const fn route(&self) -> ExplainAggregateTerminalRoute {
223        self.route
224    }
225
226    /// Borrow projected execution descriptor.
227    #[must_use]
228    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
229        &self.execution
230    }
231
232    #[must_use]
233    pub(in crate::db) const fn new(
234        query: ExplainPlan,
235        terminal: AggregateKind,
236        execution: ExplainExecutionDescriptor,
237    ) -> Self {
238        let route = execution.route();
239
240        Self {
241            query,
242            terminal,
243            route,
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    #[must_use]
299    pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
300        match self.ordering_source {
301            ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
302                ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
303            }
304            ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
305                ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
306            }
307            ExplainExecutionOrderingSource::AccessOrder
308            | ExplainExecutionOrderingSource::Materialized => {
309                ExplainAggregateTerminalRoute::Standard
310            }
311        }
312    }
313}
314
315impl ExplainAggregateTerminalPlan {
316    /// Build an execution-node descriptor for aggregate terminal plans.
317    #[must_use]
318    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
319        ExplainExecutionNodeDescriptor {
320            node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
321            execution_mode: self.execution.execution_mode,
322            access_strategy: Some(self.execution.access_strategy.clone()),
323            predicate_pushdown: None,
324            residual_predicate: None,
325            projection: None,
326            ordering_source: Some(self.execution.ordering_source),
327            limit: self.execution.limit,
328            cursor: Some(self.execution.cursor),
329            covering_scan: Some(self.execution.covering_projection),
330            rows_expected: None,
331            children: Vec::new(),
332            node_properties: self.execution.node_properties.clone(),
333        }
334    }
335}
336
337const fn aggregate_execution_node_type(
338    terminal: AggregateKind,
339    ordering_source: ExplainExecutionOrderingSource,
340) -> ExplainExecutionNodeType {
341    match ordering_source {
342        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
343            ExplainExecutionNodeType::AggregateSeekFirst
344        }
345        ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
346            ExplainExecutionNodeType::AggregateSeekLast
347        }
348        ExplainExecutionOrderingSource::AccessOrder
349        | ExplainExecutionOrderingSource::Materialized => {
350            ExplainExecutionNodeType::aggregate_terminal(terminal)
351        }
352    }
353}
354
355impl ExplainExecutionNodeType {
356    /// Return the canonical execution-node type for one aggregate terminal kind.
357    #[must_use]
358    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
359        match kind {
360            AggregateKind::Count => Self::AggregateCount,
361            AggregateKind::Exists => Self::AggregateExists,
362            AggregateKind::Min => Self::AggregateMin,
363            AggregateKind::Max => Self::AggregateMax,
364            AggregateKind::First => Self::AggregateFirst,
365            AggregateKind::Last => Self::AggregateLast,
366            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
367        }
368    }
369
370    /// Return the stable string label used by explain renderers.
371    #[must_use]
372    pub const fn as_str(self) -> &'static str {
373        match self {
374            Self::ByKeyLookup => "ByKeyLookup",
375            Self::ByKeysLookup => "ByKeysLookup",
376            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
377            Self::IndexPrefixScan => "IndexPrefixScan",
378            Self::IndexRangeScan => "IndexRangeScan",
379            Self::IndexMultiLookup => "IndexMultiLookup",
380            Self::FullScan => "FullScan",
381            Self::Union => "Union",
382            Self::Intersection => "Intersection",
383            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
384            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
385            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
386            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
387            Self::DistinctPreOrdered => "DistinctPreOrdered",
388            Self::DistinctMaterialized => "DistinctMaterialized",
389            Self::ProjectionMaterialized => "ProjectionMaterialized",
390            Self::CoveringRead => "CoveringRead",
391            Self::LimitOffset => "LimitOffset",
392            Self::CursorResume => "CursorResume",
393            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
394            Self::TopNSeek => "TopNSeek",
395            Self::AggregateCount => "AggregateCount",
396            Self::AggregateExists => "AggregateExists",
397            Self::AggregateMin => "AggregateMin",
398            Self::AggregateMax => "AggregateMax",
399            Self::AggregateFirst => "AggregateFirst",
400            Self::AggregateLast => "AggregateLast",
401            Self::AggregateSum => "AggregateSum",
402            Self::AggregateSeekFirst => "AggregateSeekFirst",
403            Self::AggregateSeekLast => "AggregateSeekLast",
404            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
405            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
406            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
407        }
408    }
409
410    /// Return the owning execution layer label for this node type.
411    #[must_use]
412    pub const fn layer_label(self) -> &'static str {
413        crate::db::query::explain::nodes::layer_label(self)
414    }
415}
416
417impl ExplainExecutionNodeDescriptor {
418    /// Return node type.
419    #[must_use]
420    pub const fn node_type(&self) -> ExplainExecutionNodeType {
421        self.node_type
422    }
423
424    /// Return execution mode.
425    #[must_use]
426    pub const fn execution_mode(&self) -> ExplainExecutionMode {
427        self.execution_mode
428    }
429
430    /// Borrow optional access strategy annotation.
431    #[must_use]
432    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
433        self.access_strategy.as_ref()
434    }
435
436    /// Borrow optional predicate pushdown annotation.
437    #[must_use]
438    pub fn predicate_pushdown(&self) -> Option<&str> {
439        self.predicate_pushdown.as_deref()
440    }
441
442    /// Borrow optional residual predicate annotation.
443    #[must_use]
444    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
445        self.residual_predicate.as_ref()
446    }
447
448    /// Borrow optional projection annotation.
449    #[must_use]
450    pub fn projection(&self) -> Option<&str> {
451        self.projection.as_deref()
452    }
453
454    /// Return optional ordering source annotation.
455    #[must_use]
456    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
457        self.ordering_source
458    }
459
460    /// Return optional limit annotation.
461    #[must_use]
462    pub const fn limit(&self) -> Option<u32> {
463        self.limit
464    }
465
466    /// Return optional continuation annotation.
467    #[must_use]
468    pub const fn cursor(&self) -> Option<bool> {
469        self.cursor
470    }
471
472    /// Return optional covering-scan annotation.
473    #[must_use]
474    pub const fn covering_scan(&self) -> Option<bool> {
475        self.covering_scan
476    }
477
478    /// Return optional row-count expectation annotation.
479    #[must_use]
480    pub const fn rows_expected(&self) -> Option<u64> {
481        self.rows_expected
482    }
483
484    /// Borrow child execution nodes.
485    #[must_use]
486    pub const fn children(&self) -> &[Self] {
487        self.children.as_slice()
488    }
489
490    /// Borrow node properties.
491    #[must_use]
492    pub const fn node_properties(&self) -> &ExplainPropertyMap {
493        &self.node_properties
494    }
495}
496
497pub(in crate::db::query::explain) const fn execution_mode_label(
498    mode: ExplainExecutionMode,
499) -> &'static str {
500    match mode {
501        ExplainExecutionMode::Streaming => "Streaming",
502        ExplainExecutionMode::Materialized => "Materialized",
503    }
504}
505
506pub(in crate::db::query::explain) const fn ordering_source_label(
507    ordering_source: ExplainExecutionOrderingSource,
508) -> &'static str {
509    match ordering_source {
510        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
511        ExplainExecutionOrderingSource::Materialized => "Materialized",
512        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
513        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
514    }
515}