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: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
283            execution_mode: self.execution.execution_mode,
284            access_strategy: Some(self.execution.access_strategy.clone()),
285            predicate_pushdown: None,
286            residual_predicate: None,
287            projection: None,
288            ordering_source: Some(self.execution.ordering_source),
289            limit: self.execution.limit,
290            cursor: Some(self.execution.cursor),
291            covering_scan: Some(self.execution.covering_projection),
292            rows_expected: None,
293            children: Vec::new(),
294            node_properties: self.execution.node_properties.clone(),
295        }
296    }
297}
298
299const fn aggregate_execution_node_type(
300    terminal: AggregateKind,
301    ordering_source: ExplainExecutionOrderingSource,
302) -> ExplainExecutionNodeType {
303    match ordering_source {
304        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
305            ExplainExecutionNodeType::AggregateSeekFirst
306        }
307        ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
308            ExplainExecutionNodeType::AggregateSeekLast
309        }
310        ExplainExecutionOrderingSource::AccessOrder
311        | ExplainExecutionOrderingSource::Materialized => {
312            ExplainExecutionNodeType::aggregate_terminal(terminal)
313        }
314    }
315}
316
317impl ExplainExecutionNodeType {
318    /// Return the canonical execution-node type for one aggregate terminal kind.
319    #[must_use]
320    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
321        match kind {
322            AggregateKind::Count => Self::AggregateCount,
323            AggregateKind::Exists => Self::AggregateExists,
324            AggregateKind::Min => Self::AggregateMin,
325            AggregateKind::Max => Self::AggregateMax,
326            AggregateKind::First => Self::AggregateFirst,
327            AggregateKind::Last => Self::AggregateLast,
328            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
329        }
330    }
331
332    /// Return the stable string label used by explain renderers.
333    #[must_use]
334    pub const fn as_str(self) -> &'static str {
335        match self {
336            Self::ByKeyLookup => "ByKeyLookup",
337            Self::ByKeysLookup => "ByKeysLookup",
338            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
339            Self::IndexPrefixScan => "IndexPrefixScan",
340            Self::IndexRangeScan => "IndexRangeScan",
341            Self::IndexMultiLookup => "IndexMultiLookup",
342            Self::FullScan => "FullScan",
343            Self::Union => "Union",
344            Self::Intersection => "Intersection",
345            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
346            Self::ResidualPredicateFilter => "ResidualPredicateFilter",
347            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
348            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
349            Self::DistinctPreOrdered => "DistinctPreOrdered",
350            Self::DistinctMaterialized => "DistinctMaterialized",
351            Self::ProjectionMaterialized => "ProjectionMaterialized",
352            Self::CoveringRead => "CoveringRead",
353            Self::LimitOffset => "LimitOffset",
354            Self::CursorResume => "CursorResume",
355            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
356            Self::TopNSeek => "TopNSeek",
357            Self::AggregateCount => "AggregateCount",
358            Self::AggregateExists => "AggregateExists",
359            Self::AggregateMin => "AggregateMin",
360            Self::AggregateMax => "AggregateMax",
361            Self::AggregateFirst => "AggregateFirst",
362            Self::AggregateLast => "AggregateLast",
363            Self::AggregateSum => "AggregateSum",
364            Self::AggregateSeekFirst => "AggregateSeekFirst",
365            Self::AggregateSeekLast => "AggregateSeekLast",
366            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
367            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
368            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
369        }
370    }
371
372    /// Return the owning execution layer label for this node type.
373    #[must_use]
374    pub const fn layer_label(self) -> &'static str {
375        crate::db::query::explain::nodes::layer_label(self)
376    }
377}
378
379impl ExplainExecutionNodeDescriptor {
380    /// Return node type.
381    #[must_use]
382    pub const fn node_type(&self) -> ExplainExecutionNodeType {
383        self.node_type
384    }
385
386    /// Return execution mode.
387    #[must_use]
388    pub const fn execution_mode(&self) -> ExplainExecutionMode {
389        self.execution_mode
390    }
391
392    /// Borrow optional access strategy annotation.
393    #[must_use]
394    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
395        self.access_strategy.as_ref()
396    }
397
398    /// Borrow optional predicate pushdown annotation.
399    #[must_use]
400    pub fn predicate_pushdown(&self) -> Option<&str> {
401        self.predicate_pushdown.as_deref()
402    }
403
404    /// Borrow optional residual predicate annotation.
405    #[must_use]
406    pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
407        self.residual_predicate.as_ref()
408    }
409
410    /// Borrow optional projection annotation.
411    #[must_use]
412    pub fn projection(&self) -> Option<&str> {
413        self.projection.as_deref()
414    }
415
416    /// Return optional ordering source annotation.
417    #[must_use]
418    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
419        self.ordering_source
420    }
421
422    /// Return optional limit annotation.
423    #[must_use]
424    pub const fn limit(&self) -> Option<u32> {
425        self.limit
426    }
427
428    /// Return optional continuation annotation.
429    #[must_use]
430    pub const fn cursor(&self) -> Option<bool> {
431        self.cursor
432    }
433
434    /// Return optional covering-scan annotation.
435    #[must_use]
436    pub const fn covering_scan(&self) -> Option<bool> {
437        self.covering_scan
438    }
439
440    /// Return optional row-count expectation annotation.
441    #[must_use]
442    pub const fn rows_expected(&self) -> Option<u64> {
443        self.rows_expected
444    }
445
446    /// Borrow child execution nodes.
447    #[must_use]
448    pub const fn children(&self) -> &[Self] {
449        self.children.as_slice()
450    }
451
452    /// Borrow node properties.
453    #[must_use]
454    pub const fn node_properties(&self) -> &ExplainPropertyMap {
455        &self.node_properties
456    }
457}
458
459pub(in crate::db::query::explain) const fn execution_mode_label(
460    mode: ExplainExecutionMode,
461) -> &'static str {
462    match mode {
463        ExplainExecutionMode::Streaming => "Streaming",
464        ExplainExecutionMode::Materialized => "Materialized",
465    }
466}
467
468pub(in crate::db::query::explain) const fn ordering_source_label(
469    ordering_source: ExplainExecutionOrderingSource,
470) -> &'static str {
471    match ordering_source {
472        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
473        ExplainExecutionOrderingSource::Materialized => "Materialized",
474        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
475        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
476    }
477}