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