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 = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
87)]
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct ExplainAggregateTerminalPlan {
90    pub(crate) query: ExplainPlan,
91    pub(crate) terminal: AggregateKind,
92    pub(crate) execution: ExplainExecutionDescriptor,
93}
94
95#[cfg_attr(
96    doc,
97    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
98)]
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum ExplainExecutionOrderingSource {
101    AccessOrder,
102    Materialized,
103    IndexSeekFirst { fetch: usize },
104    IndexSeekLast { fetch: usize },
105}
106
107#[cfg_attr(
108    doc,
109    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
110)]
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum ExplainExecutionMode {
113    Streaming,
114    Materialized,
115}
116
117#[cfg_attr(
118    doc,
119    doc = "ExplainExecutionDescriptor\n\nScalar execution descriptor consumed by terminal EXPLAIN surfaces.\nKeeps execution projection centralized for renderers."
120)]
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct ExplainExecutionDescriptor {
123    pub(crate) access_strategy: ExplainAccessPath,
124    pub(crate) covering_projection: bool,
125    pub(crate) aggregation: AggregateKind,
126    pub(crate) execution_mode: ExplainExecutionMode,
127    pub(crate) ordering_source: ExplainExecutionOrderingSource,
128    pub(crate) limit: Option<u32>,
129    pub(crate) cursor: bool,
130    pub(crate) node_properties: ExplainPropertyMap,
131}
132
133#[cfg_attr(
134    doc,
135    doc = "ExplainExecutionNodeType\n\nExecution-node vocabulary for EXPLAIN descriptors."
136)]
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
138pub enum ExplainExecutionNodeType {
139    ByKeyLookup,
140    ByKeysLookup,
141    PrimaryKeyRangeScan,
142    IndexPrefixScan,
143    IndexRangeScan,
144    IndexMultiLookup,
145    FullScan,
146    Union,
147    Intersection,
148    IndexPredicatePrefilter,
149    ResidualPredicateFilter,
150    OrderByAccessSatisfied,
151    OrderByMaterializedSort,
152    DistinctPreOrdered,
153    DistinctMaterialized,
154    ProjectionMaterialized,
155    CoveringRead,
156    LimitOffset,
157    CursorResume,
158    IndexRangeLimitPushdown,
159    TopNSeek,
160    AggregateCount,
161    AggregateExists,
162    AggregateMin,
163    AggregateMax,
164    AggregateFirst,
165    AggregateLast,
166    AggregateSum,
167    AggregateSeekFirst,
168    AggregateSeekLast,
169    GroupedAggregateHashMaterialized,
170    GroupedAggregateOrderedMaterialized,
171    SecondaryOrderPushdown,
172}
173
174#[cfg_attr(
175    doc,
176    doc = "ExplainExecutionNodeDescriptor\n\nCanonical execution-node descriptor for EXPLAIN renderers.\nOptional fields are node-family specific."
177)]
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub struct ExplainExecutionNodeDescriptor {
180    pub(crate) node_type: ExplainExecutionNodeType,
181    pub(crate) execution_mode: ExplainExecutionMode,
182    pub(crate) access_strategy: Option<ExplainAccessPath>,
183    pub(crate) predicate_pushdown: 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: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
284            execution_mode: self.execution.execution_mode,
285            access_strategy: Some(self.execution.access_strategy.clone()),
286            predicate_pushdown: None,
287            residual_predicate: None,
288            projection: None,
289            ordering_source: Some(self.execution.ordering_source),
290            limit: self.execution.limit,
291            cursor: Some(self.execution.cursor),
292            covering_scan: Some(self.execution.covering_projection),
293            rows_expected: None,
294            children: Vec::new(),
295            node_properties: self.execution.node_properties.clone(),
296        }
297    }
298}
299
300const fn aggregate_execution_node_type(
301    terminal: AggregateKind,
302    ordering_source: ExplainExecutionOrderingSource,
303) -> ExplainExecutionNodeType {
304    match ordering_source {
305        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
306            ExplainExecutionNodeType::AggregateSeekFirst
307        }
308        ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
309            ExplainExecutionNodeType::AggregateSeekLast
310        }
311        ExplainExecutionOrderingSource::AccessOrder
312        | ExplainExecutionOrderingSource::Materialized => {
313            ExplainExecutionNodeType::aggregate_terminal(terminal)
314        }
315    }
316}
317
318impl ExplainExecutionNodeType {
319    /// Return the canonical execution-node type for one aggregate terminal kind.
320    #[must_use]
321    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
322        match kind {
323            AggregateKind::Count => Self::AggregateCount,
324            AggregateKind::Exists => Self::AggregateExists,
325            AggregateKind::Min => Self::AggregateMin,
326            AggregateKind::Max => Self::AggregateMax,
327            AggregateKind::First => Self::AggregateFirst,
328            AggregateKind::Last => Self::AggregateLast,
329            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
330        }
331    }
332
333    /// Return the stable string label used by explain renderers.
334    #[must_use]
335    pub const fn as_str(self) -> &'static str {
336        match self {
337            Self::ByKeyLookup => "ByKeyLookup",
338            Self::ByKeysLookup => "ByKeysLookup",
339            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
340            Self::IndexPrefixScan => "IndexPrefixScan",
341            Self::IndexRangeScan => "IndexRangeScan",
342            Self::IndexMultiLookup => "IndexMultiLookup",
343            Self::FullScan => "FullScan",
344            Self::Union => "Union",
345            Self::Intersection => "Intersection",
346            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
347            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
348            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
349            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
350            Self::DistinctPreOrdered => "DistinctPreOrdered",
351            Self::DistinctMaterialized => "DistinctMaterialized",
352            Self::ProjectionMaterialized => "ProjectionMaterialized",
353            Self::CoveringRead => "CoveringRead",
354            Self::LimitOffset => "LimitOffset",
355            Self::CursorResume => "CursorResume",
356            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
357            Self::TopNSeek => "TopNSeek",
358            Self::AggregateCount => "AggregateCount",
359            Self::AggregateExists => "AggregateExists",
360            Self::AggregateMin => "AggregateMin",
361            Self::AggregateMax => "AggregateMax",
362            Self::AggregateFirst => "AggregateFirst",
363            Self::AggregateLast => "AggregateLast",
364            Self::AggregateSum => "AggregateSum",
365            Self::AggregateSeekFirst => "AggregateSeekFirst",
366            Self::AggregateSeekLast => "AggregateSeekLast",
367            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
368            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
369            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
370        }
371    }
372
373    /// Return the owning execution layer label for this node type.
374    #[must_use]
375    pub const fn layer_label(self) -> &'static str {
376        crate::db::query::explain::nodes::layer_label(self)
377    }
378}
379
380impl ExplainExecutionNodeDescriptor {
381    /// Return node type.
382    #[must_use]
383    pub const fn node_type(&self) -> ExplainExecutionNodeType {
384        self.node_type
385    }
386
387    /// Return execution mode.
388    #[must_use]
389    pub const fn execution_mode(&self) -> ExplainExecutionMode {
390        self.execution_mode
391    }
392
393    /// Borrow optional access strategy annotation.
394    #[must_use]
395    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
396        self.access_strategy.as_ref()
397    }
398
399    /// Borrow optional predicate pushdown annotation.
400    #[must_use]
401    pub fn predicate_pushdown(&self) -> Option<&str> {
402        self.predicate_pushdown.as_deref()
403    }
404
405    /// Borrow optional residual predicate annotation.
406    #[must_use]
407    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
408        self.residual_predicate.as_ref()
409    }
410
411    /// Borrow optional projection annotation.
412    #[must_use]
413    pub fn projection(&self) -> Option<&str> {
414        self.projection.as_deref()
415    }
416
417    /// Return optional ordering source annotation.
418    #[must_use]
419    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
420        self.ordering_source
421    }
422
423    /// Return optional limit annotation.
424    #[must_use]
425    pub const fn limit(&self) -> Option<u32> {
426        self.limit
427    }
428
429    /// Return optional continuation annotation.
430    #[must_use]
431    pub const fn cursor(&self) -> Option<bool> {
432        self.cursor
433    }
434
435    /// Return optional covering-scan annotation.
436    #[must_use]
437    pub const fn covering_scan(&self) -> Option<bool> {
438        self.covering_scan
439    }
440
441    /// Return optional row-count expectation annotation.
442    #[must_use]
443    pub const fn rows_expected(&self) -> Option<u64> {
444        self.rows_expected
445    }
446
447    /// Borrow child execution nodes.
448    #[must_use]
449    pub const fn children(&self) -> &[Self] {
450        self.children.as_slice()
451    }
452
453    /// Borrow node properties.
454    #[must_use]
455    pub const fn node_properties(&self) -> &ExplainPropertyMap {
456        &self.node_properties
457    }
458}
459
460pub(in crate::db::query::explain) const fn execution_mode_label(
461    mode: ExplainExecutionMode,
462) -> &'static str {
463    match mode {
464        ExplainExecutionMode::Streaming => "Streaming",
465        ExplainExecutionMode::Materialized => "Materialized",
466    }
467}
468
469pub(in crate::db::query::explain) const fn ordering_source_label(
470    ordering_source: ExplainExecutionOrderingSource,
471) -> &'static str {
472    match ordering_source {
473        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
474        ExplainExecutionOrderingSource::Materialized => "Materialized",
475        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
476        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
477    }
478}