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