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) filter_expr: Option<String>,
184    pub(crate) residual_predicate: Option<ExplainPredicate>,
185    pub(crate) projection: Option<String>,
186    pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
187    pub(crate) limit: Option<u32>,
188    pub(crate) cursor: Option<bool>,
189    pub(crate) covering_scan: Option<bool>,
190    pub(crate) rows_expected: Option<u64>,
191    pub(crate) children: Vec<Self>,
192    pub(crate) node_properties: ExplainPropertyMap,
193}
194
195impl ExplainAggregateTerminalPlan {
196    /// Borrow the underlying query explain payload.
197    #[must_use]
198    pub const fn query(&self) -> &ExplainPlan {
199        &self.query
200    }
201
202    /// Return terminal aggregate kind.
203    #[must_use]
204    pub const fn terminal(&self) -> AggregateKind {
205        self.terminal
206    }
207
208    /// Borrow projected execution descriptor.
209    #[must_use]
210    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
211        &self.execution
212    }
213
214    #[must_use]
215    pub(in crate::db) const fn new(
216        query: ExplainPlan,
217        terminal: AggregateKind,
218        execution: ExplainExecutionDescriptor,
219    ) -> Self {
220        Self {
221            query,
222            terminal,
223            execution,
224        }
225    }
226}
227
228impl ExplainExecutionDescriptor {
229    /// Borrow projected access strategy.
230    #[must_use]
231    pub const fn access_strategy(&self) -> &ExplainAccessPath {
232        &self.access_strategy
233    }
234
235    /// Return whether projection can be served from index payload only.
236    #[must_use]
237    pub const fn covering_projection(&self) -> bool {
238        self.covering_projection
239    }
240
241    /// Return projected aggregate kind.
242    #[must_use]
243    pub const fn aggregation(&self) -> AggregateKind {
244        self.aggregation
245    }
246
247    /// Return projected execution mode.
248    #[must_use]
249    pub const fn execution_mode(&self) -> ExplainExecutionMode {
250        self.execution_mode
251    }
252
253    /// Return projected ordering source.
254    #[must_use]
255    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
256        self.ordering_source
257    }
258
259    /// Return projected execution limit.
260    #[must_use]
261    pub const fn limit(&self) -> Option<u32> {
262        self.limit
263    }
264
265    /// Return whether continuation was applied.
266    #[must_use]
267    pub const fn cursor(&self) -> bool {
268        self.cursor
269    }
270
271    /// Borrow projected execution node properties.
272    #[must_use]
273    pub const fn node_properties(&self) -> &ExplainPropertyMap {
274        &self.node_properties
275    }
276}
277
278impl ExplainAggregateTerminalPlan {
279    /// Build an execution-node descriptor for aggregate terminal plans.
280    #[must_use]
281    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
282        ExplainExecutionNodeDescriptor {
283            node_type: match self.execution.ordering_source {
284                ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
285                    ExplainExecutionNodeType::AggregateSeekFirst
286                }
287                ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
288                    ExplainExecutionNodeType::AggregateSeekLast
289                }
290                ExplainExecutionOrderingSource::AccessOrder
291                | ExplainExecutionOrderingSource::Materialized => {
292                    ExplainExecutionNodeType::aggregate_terminal(self.terminal)
293                }
294            },
295            execution_mode: self.execution.execution_mode,
296            access_strategy: Some(self.execution.access_strategy.clone()),
297            predicate_pushdown: None,
298            filter_expr: None,
299            residual_predicate: None,
300            projection: None,
301            ordering_source: Some(self.execution.ordering_source),
302            limit: self.execution.limit,
303            cursor: Some(self.execution.cursor),
304            covering_scan: Some(self.execution.covering_projection),
305            rows_expected: None,
306            children: Vec::new(),
307            node_properties: self.execution.node_properties.clone(),
308        }
309    }
310}
311
312impl ExplainExecutionNodeType {
313    /// Return the canonical execution-node type for one aggregate terminal kind.
314    #[must_use]
315    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
316        match kind {
317            AggregateKind::Count => Self::AggregateCount,
318            AggregateKind::Exists => Self::AggregateExists,
319            AggregateKind::Min => Self::AggregateMin,
320            AggregateKind::Max => Self::AggregateMax,
321            AggregateKind::First => Self::AggregateFirst,
322            AggregateKind::Last => Self::AggregateLast,
323            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
324        }
325    }
326
327    /// Return the stable string label used by explain renderers.
328    #[must_use]
329    pub const fn as_str(self) -> &'static str {
330        match self {
331            Self::ByKeyLookup => "ByKeyLookup",
332            Self::ByKeysLookup => "ByKeysLookup",
333            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
334            Self::IndexPrefixScan => "IndexPrefixScan",
335            Self::IndexRangeScan => "IndexRangeScan",
336            Self::IndexMultiLookup => "IndexMultiLookup",
337            Self::FullScan => "FullScan",
338            Self::Union => "Union",
339            Self::Intersection => "Intersection",
340            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
341            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
342            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
343            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
344            Self::DistinctPreOrdered => "DistinctPreOrdered",
345            Self::DistinctMaterialized => "DistinctMaterialized",
346            Self::ProjectionMaterialized => "ProjectionMaterialized",
347            Self::CoveringRead => "CoveringRead",
348            Self::LimitOffset => "LimitOffset",
349            Self::CursorResume => "CursorResume",
350            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
351            Self::TopNSeek => "TopNSeek",
352            Self::AggregateCount => "AggregateCount",
353            Self::AggregateExists => "AggregateExists",
354            Self::AggregateMin => "AggregateMin",
355            Self::AggregateMax => "AggregateMax",
356            Self::AggregateFirst => "AggregateFirst",
357            Self::AggregateLast => "AggregateLast",
358            Self::AggregateSum => "AggregateSum",
359            Self::AggregateSeekFirst => "AggregateSeekFirst",
360            Self::AggregateSeekLast => "AggregateSeekLast",
361            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
362            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
363            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
364        }
365    }
366
367    /// Return the owning execution layer label for this node type.
368    #[must_use]
369    pub const fn layer_label(self) -> &'static str {
370        crate::db::query::explain::nodes::layer_label(self)
371    }
372}
373
374impl ExplainExecutionNodeDescriptor {
375    /// Return node type.
376    #[must_use]
377    pub const fn node_type(&self) -> ExplainExecutionNodeType {
378        self.node_type
379    }
380
381    /// Return execution mode.
382    #[must_use]
383    pub const fn execution_mode(&self) -> ExplainExecutionMode {
384        self.execution_mode
385    }
386
387    /// Borrow optional access strategy annotation.
388    #[must_use]
389    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
390        self.access_strategy.as_ref()
391    }
392
393    /// Borrow optional predicate pushdown annotation.
394    #[must_use]
395    pub fn predicate_pushdown(&self) -> Option<&str> {
396        self.predicate_pushdown.as_deref()
397    }
398
399    /// Borrow optional semantic scalar filter expression annotation.
400    #[must_use]
401    pub fn filter_expr(&self) -> Option<&str> {
402        self.filter_expr.as_deref()
403    }
404
405    /// Borrow the optional derived residual predicate annotation emitted
406    /// alongside `filter_expr` when execution still benefits from predicate
407    /// pushdown labeling.
408    #[must_use]
409    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
410        self.residual_predicate.as_ref()
411    }
412
413    /// Borrow optional projection annotation.
414    #[must_use]
415    pub fn projection(&self) -> Option<&str> {
416        self.projection.as_deref()
417    }
418
419    /// Return optional ordering source annotation.
420    #[must_use]
421    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
422        self.ordering_source
423    }
424
425    /// Return optional limit annotation.
426    #[must_use]
427    pub const fn limit(&self) -> Option<u32> {
428        self.limit
429    }
430
431    /// Return optional continuation annotation.
432    #[must_use]
433    pub const fn cursor(&self) -> Option<bool> {
434        self.cursor
435    }
436
437    /// Return optional covering-scan annotation.
438    #[must_use]
439    pub const fn covering_scan(&self) -> Option<bool> {
440        self.covering_scan
441    }
442
443    /// Return optional row-count expectation annotation.
444    #[must_use]
445    pub const fn rows_expected(&self) -> Option<u64> {
446        self.rows_expected
447    }
448
449    /// Borrow child execution nodes.
450    #[must_use]
451    pub const fn children(&self) -> &[Self] {
452        self.children.as_slice()
453    }
454
455    /// Borrow node properties.
456    #[must_use]
457    pub const fn node_properties(&self) -> &ExplainPropertyMap {
458        &self.node_properties
459    }
460}
461
462pub(in crate::db::query::explain) const fn execution_mode_label(
463    mode: ExplainExecutionMode,
464) -> &'static str {
465    match mode {
466        ExplainExecutionMode::Streaming => "Streaming",
467        ExplainExecutionMode::Materialized => "Materialized",
468    }
469}
470
471pub(in crate::db::query::explain) const fn ordering_source_label(
472    ordering_source: ExplainExecutionOrderingSource,
473) -> &'static str {
474    match ordering_source {
475        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
476        ExplainExecutionOrderingSource::Materialized => "Materialized",
477        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
478        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
479    }
480}