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    ProjectionIndexOnly,
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 => match terminal {
350            AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
351            AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
352            AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
353            AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
354            AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
355            AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
356            AggregateKind::Sum | AggregateKind::Avg => ExplainExecutionNodeType::AggregateSum,
357        },
358    }
359}
360
361impl ExplainExecutionNodeType {
362    /// Return the stable string label used by explain renderers.
363    #[must_use]
364    pub const fn as_str(self) -> &'static str {
365        match self {
366            Self::ByKeyLookup => "ByKeyLookup",
367            Self::ByKeysLookup => "ByKeysLookup",
368            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
369            Self::IndexPrefixScan => "IndexPrefixScan",
370            Self::IndexRangeScan => "IndexRangeScan",
371            Self::IndexMultiLookup => "IndexMultiLookup",
372            Self::FullScan => "FullScan",
373            Self::Union => "Union",
374            Self::Intersection => "Intersection",
375            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
376            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
377            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
378            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
379            Self::DistinctPreOrdered => "DistinctPreOrdered",
380            Self::DistinctMaterialized => "DistinctMaterialized",
381            Self::ProjectionMaterialized => "ProjectionMaterialized",
382            Self::ProjectionIndexOnly => "ProjectionIndexOnly",
383            Self::LimitOffset => "LimitOffset",
384            Self::CursorResume => "CursorResume",
385            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
386            Self::TopNSeek => "TopNSeek",
387            Self::AggregateCount => "AggregateCount",
388            Self::AggregateExists => "AggregateExists",
389            Self::AggregateMin => "AggregateMin",
390            Self::AggregateMax => "AggregateMax",
391            Self::AggregateFirst => "AggregateFirst",
392            Self::AggregateLast => "AggregateLast",
393            Self::AggregateSum => "AggregateSum",
394            Self::AggregateSeekFirst => "AggregateSeekFirst",
395            Self::AggregateSeekLast => "AggregateSeekLast",
396            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
397            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
398            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
399        }
400    }
401
402    /// Return the owning execution layer label for this node type.
403    #[must_use]
404    pub const fn layer_label(self) -> &'static str {
405        crate::db::query::explain::nodes::layer_label(self)
406    }
407}
408
409impl ExplainExecutionNodeDescriptor {
410    /// Return node type.
411    #[must_use]
412    pub const fn node_type(&self) -> ExplainExecutionNodeType {
413        self.node_type
414    }
415
416    /// Return execution mode.
417    #[must_use]
418    pub const fn execution_mode(&self) -> ExplainExecutionMode {
419        self.execution_mode
420    }
421
422    /// Borrow optional access strategy annotation.
423    #[must_use]
424    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
425        self.access_strategy.as_ref()
426    }
427
428    /// Borrow optional predicate pushdown annotation.
429    #[must_use]
430    pub fn predicate_pushdown(&self) -> Option<&str> {
431        self.predicate_pushdown.as_deref()
432    }
433
434    /// Borrow optional residual predicate annotation.
435    #[must_use]
436    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
437        self.residual_predicate.as_ref()
438    }
439
440    /// Borrow optional projection annotation.
441    #[must_use]
442    pub fn projection(&self) -> Option<&str> {
443        self.projection.as_deref()
444    }
445
446    /// Return optional ordering source annotation.
447    #[must_use]
448    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
449        self.ordering_source
450    }
451
452    /// Return optional limit annotation.
453    #[must_use]
454    pub const fn limit(&self) -> Option<u32> {
455        self.limit
456    }
457
458    /// Return optional continuation annotation.
459    #[must_use]
460    pub const fn cursor(&self) -> Option<bool> {
461        self.cursor
462    }
463
464    /// Return optional covering-scan annotation.
465    #[must_use]
466    pub const fn covering_scan(&self) -> Option<bool> {
467        self.covering_scan
468    }
469
470    /// Return optional row-count expectation annotation.
471    #[must_use]
472    pub const fn rows_expected(&self) -> Option<u64> {
473        self.rows_expected
474    }
475
476    /// Borrow child execution nodes.
477    #[must_use]
478    pub const fn children(&self) -> &[Self] {
479        self.children.as_slice()
480    }
481
482    /// Borrow node properties.
483    #[must_use]
484    pub const fn node_properties(&self) -> &ExplainPropertyMap {
485        &self.node_properties
486    }
487}
488
489pub(in crate::db::query::explain) const fn execution_mode_label(
490    mode: ExplainExecutionMode,
491) -> &'static str {
492    match mode {
493        ExplainExecutionMode::Streaming => "Streaming",
494        ExplainExecutionMode::Materialized => "Materialized",
495    }
496}
497
498pub(in crate::db::query::explain) const fn ordering_source_label(
499    ordering_source: ExplainExecutionOrderingSource,
500) -> &'static str {
501    match ordering_source {
502        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
503        ExplainExecutionOrderingSource::Materialized => "Materialized",
504        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
505        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
506    }
507}