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