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::{
8        executor::RouteExecutionMode,
9        query::{
10            explain::{ExplainAccessPath, ExplainPlan, ExplainPredicate},
11            plan::AggregateKind,
12        },
13    },
14    value::Value,
15};
16use std::fmt::{self, Debug};
17
18#[cfg_attr(
19    doc,
20    doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
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#[cfg_attr(
88    doc,
89    doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
90)]
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct ExplainAggregateTerminalPlan {
93    pub(crate) query: ExplainPlan,
94    pub(crate) terminal: AggregateKind,
95    pub(crate) execution: ExplainExecutionDescriptor,
96}
97
98#[cfg_attr(
99    doc,
100    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
101)]
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum ExplainExecutionOrderingSource {
104    AccessOrder,
105    Materialized,
106    IndexSeekFirst { fetch: usize },
107    IndexSeekLast { fetch: usize },
108}
109
110#[cfg_attr(
111    doc,
112    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
113)]
114pub type ExplainExecutionMode = RouteExecutionMode;
115
116#[cfg_attr(
117    doc,
118    doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
119)]
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub struct ExplainExecutionDescriptor {
122    pub(crate) access_strategy: ExplainAccessPath,
123    pub(crate) covering_projection: bool,
124    pub(crate) aggregation: AggregateKind,
125    pub(crate) execution_mode: ExplainExecutionMode,
126    pub(crate) ordering_source: ExplainExecutionOrderingSource,
127    pub(crate) limit: Option<u32>,
128    pub(crate) cursor: bool,
129    pub(crate) node_properties: ExplainPropertyMap,
130}
131
132#[cfg_attr(
133    doc,
134    doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
135)]
136#[derive(Clone, Copy, Debug, Eq, PartialEq)]
137pub enum ExplainExecutionNodeType {
138    ByKeyLookup,
139    ByKeysLookup,
140    PrimaryKeyRangeScan,
141    IndexPrefixScan,
142    IndexRangeScan,
143    IndexMultiLookup,
144    FullScan,
145    Union,
146    Intersection,
147    IndexPredicatePrefilter,
148    ResidualPredicateFilter,
149    OrderByAccessSatisfied,
150    OrderByMaterializedSort,
151    DistinctPreOrdered,
152    DistinctMaterialized,
153    ProjectionMaterialized,
154    CoveringRead,
155    LimitOffset,
156    CursorResume,
157    IndexRangeLimitPushdown,
158    TopNSeek,
159    AggregateCount,
160    AggregateExists,
161    AggregateMin,
162    AggregateMax,
163    AggregateFirst,
164    AggregateLast,
165    AggregateSum,
166    AggregateSeekFirst,
167    AggregateSeekLast,
168    GroupedAggregateHashMaterialized,
169    GroupedAggregateOrderedMaterialized,
170    SecondaryOrderPushdown,
171}
172
173#[cfg_attr(
174    doc,
175    doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
176)]
177#[derive(Clone, Debug, Eq, PartialEq)]
178pub struct ExplainExecutionNodeDescriptor {
179    pub(crate) node_type: ExplainExecutionNodeType,
180    pub(crate) execution_mode: ExplainExecutionMode,
181    pub(crate) access_strategy: Option<ExplainAccessPath>,
182    pub(crate) predicate_pushdown: Option<String>,
183    pub(crate) residual_predicate: Option<ExplainPredicate>,
184    pub(crate) projection: Option<String>,
185    pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
186    pub(crate) limit: Option<u32>,
187    pub(crate) cursor: Option<bool>,
188    pub(crate) covering_scan: Option<bool>,
189    pub(crate) rows_expected: Option<u64>,
190    pub(crate) children: Vec<Self>,
191    pub(crate) node_properties: ExplainPropertyMap,
192}
193
194impl ExplainAggregateTerminalPlan {
195    /// Borrow the underlying query explain payload.
196    #[must_use]
197    pub const fn query(&self) -> &ExplainPlan {
198        &self.query
199    }
200
201    /// Return terminal aggregate kind.
202    #[must_use]
203    pub const fn terminal(&self) -> AggregateKind {
204        self.terminal
205    }
206
207    /// Borrow projected execution descriptor.
208    #[must_use]
209    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
210        &self.execution
211    }
212
213    #[must_use]
214    pub(in crate::db) const fn new(
215        query: ExplainPlan,
216        terminal: AggregateKind,
217        execution: ExplainExecutionDescriptor,
218    ) -> Self {
219        Self {
220            query,
221            terminal,
222            execution,
223        }
224    }
225}
226
227impl ExplainExecutionDescriptor {
228    /// Borrow projected access strategy.
229    #[must_use]
230    pub const fn access_strategy(&self) -> &ExplainAccessPath {
231        &self.access_strategy
232    }
233
234    /// Return whether projection can be served from index payload only.
235    #[must_use]
236    pub const fn covering_projection(&self) -> bool {
237        self.covering_projection
238    }
239
240    /// Return projected aggregate kind.
241    #[must_use]
242    pub const fn aggregation(&self) -> AggregateKind {
243        self.aggregation
244    }
245
246    /// Return projected execution mode.
247    #[must_use]
248    pub const fn execution_mode(&self) -> ExplainExecutionMode {
249        self.execution_mode
250    }
251
252    /// Return projected ordering source.
253    #[must_use]
254    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
255        self.ordering_source
256    }
257
258    /// Return projected execution limit.
259    #[must_use]
260    pub const fn limit(&self) -> Option<u32> {
261        self.limit
262    }
263
264    /// Return whether continuation was applied.
265    #[must_use]
266    pub const fn cursor(&self) -> bool {
267        self.cursor
268    }
269
270    /// Borrow projected execution node properties.
271    #[must_use]
272    pub const fn node_properties(&self) -> &ExplainPropertyMap {
273        &self.node_properties
274    }
275}
276
277impl ExplainAggregateTerminalPlan {
278    /// Build an execution-node descriptor for aggregate terminal plans.
279    #[must_use]
280    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
281        ExplainExecutionNodeDescriptor {
282            node_type: match self.execution.ordering_source {
283                ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
284                    ExplainExecutionNodeType::AggregateSeekFirst
285                }
286                ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
287                    ExplainExecutionNodeType::AggregateSeekLast
288                }
289                ExplainExecutionOrderingSource::AccessOrder
290                | ExplainExecutionOrderingSource::Materialized => {
291                    ExplainExecutionNodeType::aggregate_terminal(self.terminal)
292                }
293            },
294            execution_mode: self.execution.execution_mode,
295            access_strategy: Some(self.execution.access_strategy.clone()),
296            predicate_pushdown: None,
297            residual_predicate: None,
298            projection: None,
299            ordering_source: Some(self.execution.ordering_source),
300            limit: self.execution.limit,
301            cursor: Some(self.execution.cursor),
302            covering_scan: Some(self.execution.covering_projection),
303            rows_expected: None,
304            children: Vec::new(),
305            node_properties: self.execution.node_properties.clone(),
306        }
307    }
308}
309
310impl ExplainExecutionNodeType {
311    /// Return the canonical execution-node type for one aggregate terminal kind.
312    #[must_use]
313    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
314        match kind {
315            AggregateKind::Count => Self::AggregateCount,
316            AggregateKind::Exists => Self::AggregateExists,
317            AggregateKind::Min => Self::AggregateMin,
318            AggregateKind::Max => Self::AggregateMax,
319            AggregateKind::First => Self::AggregateFirst,
320            AggregateKind::Last => Self::AggregateLast,
321            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
322        }
323    }
324
325    /// Return the stable string label used by explain renderers.
326    #[must_use]
327    pub const fn as_str(self) -> &'static str {
328        match self {
329            Self::ByKeyLookup => "ByKeyLookup",
330            Self::ByKeysLookup => "ByKeysLookup",
331            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
332            Self::IndexPrefixScan => "IndexPrefixScan",
333            Self::IndexRangeScan => "IndexRangeScan",
334            Self::IndexMultiLookup => "IndexMultiLookup",
335            Self::FullScan => "FullScan",
336            Self::Union => "Union",
337            Self::Intersection => "Intersection",
338            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
339            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
340            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
341            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
342            Self::DistinctPreOrdered => "DistinctPreOrdered",
343            Self::DistinctMaterialized => "DistinctMaterialized",
344            Self::ProjectionMaterialized => "ProjectionMaterialized",
345            Self::CoveringRead => "CoveringRead",
346            Self::LimitOffset => "LimitOffset",
347            Self::CursorResume => "CursorResume",
348            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
349            Self::TopNSeek => "TopNSeek",
350            Self::AggregateCount => "AggregateCount",
351            Self::AggregateExists => "AggregateExists",
352            Self::AggregateMin => "AggregateMin",
353            Self::AggregateMax => "AggregateMax",
354            Self::AggregateFirst => "AggregateFirst",
355            Self::AggregateLast => "AggregateLast",
356            Self::AggregateSum => "AggregateSum",
357            Self::AggregateSeekFirst => "AggregateSeekFirst",
358            Self::AggregateSeekLast => "AggregateSeekLast",
359            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
360            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
361            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
362        }
363    }
364
365    /// Return the owning execution layer label for this node type.
366    #[must_use]
367    pub const fn layer_label(self) -> &'static str {
368        crate::db::query::explain::nodes::layer_label(self)
369    }
370}
371
372impl ExplainExecutionNodeDescriptor {
373    /// Return node type.
374    #[must_use]
375    pub const fn node_type(&self) -> ExplainExecutionNodeType {
376        self.node_type
377    }
378
379    /// Return execution mode.
380    #[must_use]
381    pub const fn execution_mode(&self) -> ExplainExecutionMode {
382        self.execution_mode
383    }
384
385    /// Borrow optional access strategy annotation.
386    #[must_use]
387    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
388        self.access_strategy.as_ref()
389    }
390
391    /// Borrow optional predicate pushdown annotation.
392    #[must_use]
393    pub fn predicate_pushdown(&self) -> Option<&str> {
394        self.predicate_pushdown.as_deref()
395    }
396
397    /// Borrow optional residual predicate annotation.
398    #[must_use]
399    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
400        self.residual_predicate.as_ref()
401    }
402
403    /// Borrow optional projection annotation.
404    #[must_use]
405    pub fn projection(&self) -> Option<&str> {
406        self.projection.as_deref()
407    }
408
409    /// Return optional ordering source annotation.
410    #[must_use]
411    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
412        self.ordering_source
413    }
414
415    /// Return optional limit annotation.
416    #[must_use]
417    pub const fn limit(&self) -> Option<u32> {
418        self.limit
419    }
420
421    /// Return optional continuation annotation.
422    #[must_use]
423    pub const fn cursor(&self) -> Option<bool> {
424        self.cursor
425    }
426
427    /// Return optional covering-scan annotation.
428    #[must_use]
429    pub const fn covering_scan(&self) -> Option<bool> {
430        self.covering_scan
431    }
432
433    /// Return optional row-count expectation annotation.
434    #[must_use]
435    pub const fn rows_expected(&self) -> Option<u64> {
436        self.rows_expected
437    }
438
439    /// Borrow child execution nodes.
440    #[must_use]
441    pub const fn children(&self) -> &[Self] {
442        self.children.as_slice()
443    }
444
445    /// Borrow node properties.
446    #[must_use]
447    pub const fn node_properties(&self) -> &ExplainPropertyMap {
448        &self.node_properties
449    }
450}
451
452pub(in crate::db::query::explain) const fn execution_mode_label(
453    mode: ExplainExecutionMode,
454) -> &'static str {
455    match mode {
456        ExplainExecutionMode::Streaming => "Streaming",
457        ExplainExecutionMode::Materialized => "Materialized",
458    }
459}
460
461pub(in crate::db::query::explain) const fn ordering_source_label(
462    ordering_source: ExplainExecutionOrderingSource,
463) -> &'static str {
464    match ordering_source {
465        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
466        ExplainExecutionOrderingSource::Materialized => "Materialized",
467        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
468        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
469    }
470}