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            trace::TraceReuseEvent,
13        },
14    },
15    value::Value,
16};
17use std::fmt::{self, Debug};
18
19#[cfg_attr(
20    doc,
21    doc = "ExplainPropertyMap\n\nStable ordered property map for EXPLAIN metadata.\nKeeps deterministic key order without `BTreeMap`."
22)]
23#[derive(Clone, Default, Eq, PartialEq)]
24pub struct ExplainPropertyMap {
25    entries: Vec<(&'static str, Value)>,
26}
27
28impl ExplainPropertyMap {
29    /// Build an empty EXPLAIN property map.
30    #[must_use]
31    pub const fn new() -> Self {
32        Self {
33            entries: Vec::new(),
34        }
35    }
36
37    /// Insert or replace one stable property.
38    pub fn insert(&mut self, key: &'static str, value: Value) -> Option<Value> {
39        match self
40            .entries
41            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
42        {
43            Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
44            Err(index) => {
45                self.entries.insert(index, (key, value));
46                None
47            }
48        }
49    }
50
51    /// Borrow one property value by key.
52    #[must_use]
53    pub fn get(&self, key: &str) -> Option<&Value> {
54        self.entries
55            .binary_search_by_key(&key, |(existing_key, _)| *existing_key)
56            .ok()
57            .map(|index| &self.entries[index].1)
58    }
59
60    /// Return whether the property map contains the given key.
61    #[must_use]
62    pub fn contains_key(&self, key: &str) -> bool {
63        self.get(key).is_some()
64    }
65
66    /// Return whether the property map is empty.
67    #[must_use]
68    pub const fn is_empty(&self) -> bool {
69        self.entries.is_empty()
70    }
71
72    /// Iterate over all stored properties in deterministic key order.
73    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &Value)> {
74        self.entries.iter().map(|(key, value)| (*key, value))
75    }
76}
77
78impl Debug for ExplainPropertyMap {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let mut map = f.debug_map();
81        for (key, value) in self.iter() {
82            map.entry(&key, value);
83        }
84        map.finish()
85    }
86}
87
88#[cfg_attr(
89    doc,
90    doc = "ExplainAggregateTerminalPlan\n\nCombined EXPLAIN payload for one scalar aggregate request."
91)]
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct ExplainAggregateTerminalPlan {
94    pub(crate) query: ExplainPlan,
95    pub(crate) terminal: AggregateKind,
96    pub(crate) execution: ExplainExecutionDescriptor,
97}
98
99#[cfg_attr(
100    doc,
101    doc = "ExplainExecutionOrderingSource\n\nOrdering-origin label used by execution EXPLAIN output."
102)]
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum ExplainExecutionOrderingSource {
105    AccessOrder,
106    Materialized,
107    IndexSeekFirst { fetch: usize },
108    IndexSeekLast { fetch: usize },
109}
110
111#[cfg_attr(
112    doc,
113    doc = "ExplainExecutionMode\n\nExecution mode used by EXPLAIN descriptors."
114)]
115pub type ExplainExecutionMode = RouteExecutionMode;
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    ResidualFilter,
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) filter_expr: Option<String>,
185    pub(crate) residual_filter_expr: Option<String>,
186    pub(crate) residual_filter_predicate: Option<ExplainPredicate>,
187    pub(crate) projection: Option<String>,
188    pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
189    pub(crate) limit: Option<u32>,
190    pub(crate) cursor: Option<bool>,
191    pub(crate) covering_scan: Option<bool>,
192    pub(crate) rows_expected: Option<u64>,
193    pub(crate) children: Vec<Self>,
194    pub(crate) node_properties: ExplainPropertyMap,
195}
196
197///
198/// FinalizedQueryDiagnostics
199///
200/// FinalizedQueryDiagnostics freezes one immutable execution-explain
201/// diagnostics artifact after descriptor assembly and plan-level diagnostics
202/// projection are complete.
203/// Session and SQL wrappers render this artifact directly instead of
204/// reconstructing verbose diagnostics from separate local line builders.
205///
206
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub(in crate::db) struct FinalizedQueryDiagnostics {
209    pub(crate) execution: ExplainExecutionNodeDescriptor,
210    pub(crate) route_diagnostics: Vec<String>,
211    pub(crate) logical_diagnostics: Vec<String>,
212    pub(crate) reuse: Option<TraceReuseEvent>,
213}
214
215impl ExplainAggregateTerminalPlan {
216    /// Borrow the underlying query explain payload.
217    #[must_use]
218    pub const fn query(&self) -> &ExplainPlan {
219        &self.query
220    }
221
222    /// Return terminal aggregate kind.
223    #[must_use]
224    pub const fn terminal(&self) -> AggregateKind {
225        self.terminal
226    }
227
228    /// Borrow projected execution descriptor.
229    #[must_use]
230    pub const fn execution(&self) -> &ExplainExecutionDescriptor {
231        &self.execution
232    }
233
234    #[must_use]
235    pub(in crate::db) const fn new(
236        query: ExplainPlan,
237        terminal: AggregateKind,
238        execution: ExplainExecutionDescriptor,
239    ) -> Self {
240        Self {
241            query,
242            terminal,
243            execution,
244        }
245    }
246}
247
248impl ExplainExecutionDescriptor {
249    /// Borrow projected access strategy.
250    #[must_use]
251    pub const fn access_strategy(&self) -> &ExplainAccessPath {
252        &self.access_strategy
253    }
254
255    /// Return whether projection can be served from index payload only.
256    #[must_use]
257    pub const fn covering_projection(&self) -> bool {
258        self.covering_projection
259    }
260
261    /// Return projected aggregate kind.
262    #[must_use]
263    pub const fn aggregation(&self) -> AggregateKind {
264        self.aggregation
265    }
266
267    /// Return projected execution mode.
268    #[must_use]
269    pub const fn execution_mode(&self) -> ExplainExecutionMode {
270        self.execution_mode
271    }
272
273    /// Return projected ordering source.
274    #[must_use]
275    pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
276        self.ordering_source
277    }
278
279    /// Return projected execution limit.
280    #[must_use]
281    pub const fn limit(&self) -> Option<u32> {
282        self.limit
283    }
284
285    /// Return whether continuation was applied.
286    #[must_use]
287    pub const fn cursor(&self) -> bool {
288        self.cursor
289    }
290
291    /// Borrow projected execution node properties.
292    #[must_use]
293    pub const fn node_properties(&self) -> &ExplainPropertyMap {
294        &self.node_properties
295    }
296}
297
298impl FinalizedQueryDiagnostics {
299    /// Construct one immutable execution diagnostics artifact.
300    #[must_use]
301    pub(in crate::db) const fn new(
302        execution: ExplainExecutionNodeDescriptor,
303        route_diagnostics: Vec<String>,
304        logical_diagnostics: Vec<String>,
305        reuse: Option<TraceReuseEvent>,
306    ) -> Self {
307        Self {
308            execution,
309            route_diagnostics,
310            logical_diagnostics,
311            reuse,
312        }
313    }
314
315    /// Borrow the frozen execution descriptor carried by this artifact.
316    #[must_use]
317    pub(in crate::db) const fn execution(&self) -> &ExplainExecutionNodeDescriptor {
318        &self.execution
319    }
320}
321
322impl ExplainAggregateTerminalPlan {
323    /// Build an execution-node descriptor for aggregate terminal plans.
324    #[must_use]
325    pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
326        ExplainExecutionNodeDescriptor {
327            node_type: match self.execution.ordering_source {
328                ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
329                    ExplainExecutionNodeType::AggregateSeekFirst
330                }
331                ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
332                    ExplainExecutionNodeType::AggregateSeekLast
333                }
334                ExplainExecutionOrderingSource::AccessOrder
335                | ExplainExecutionOrderingSource::Materialized => {
336                    ExplainExecutionNodeType::aggregate_terminal(self.terminal)
337                }
338            },
339            execution_mode: self.execution.execution_mode,
340            access_strategy: Some(self.execution.access_strategy.clone()),
341            predicate_pushdown: None,
342            filter_expr: None,
343            residual_filter_expr: None,
344            residual_filter_predicate: None,
345            projection: None,
346            ordering_source: Some(self.execution.ordering_source),
347            limit: self.execution.limit,
348            cursor: Some(self.execution.cursor),
349            covering_scan: Some(self.execution.covering_projection),
350            rows_expected: None,
351            children: Vec::new(),
352            node_properties: self.execution.node_properties.clone(),
353        }
354    }
355}
356
357impl ExplainExecutionNodeType {
358    /// Return the canonical execution-node type for one aggregate terminal kind.
359    #[must_use]
360    pub(in crate::db) const fn aggregate_terminal(kind: AggregateKind) -> Self {
361        match kind {
362            AggregateKind::Count => Self::AggregateCount,
363            AggregateKind::Exists => Self::AggregateExists,
364            AggregateKind::Min => Self::AggregateMin,
365            AggregateKind::Max => Self::AggregateMax,
366            AggregateKind::First => Self::AggregateFirst,
367            AggregateKind::Last => Self::AggregateLast,
368            AggregateKind::Sum | AggregateKind::Avg => Self::AggregateSum,
369        }
370    }
371
372    /// Return the stable string label used by explain renderers.
373    #[must_use]
374    pub const fn as_str(self) -> &'static str {
375        match self {
376            Self::ByKeyLookup => "ByKeyLookup",
377            Self::ByKeysLookup => "ByKeysLookup",
378            Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
379            Self::IndexPrefixScan => "IndexPrefixScan",
380            Self::IndexRangeScan => "IndexRangeScan",
381            Self::IndexMultiLookup => "IndexMultiLookup",
382            Self::FullScan => "FullScan",
383            Self::Union => "Union",
384            Self::Intersection => "Intersection",
385            Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
386            Self::ResidualFilter => "ResidualFilter",
387            Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
388            Self::OrderByMaterializedSort => "OrderByMaterializedSort",
389            Self::DistinctPreOrdered => "DistinctPreOrdered",
390            Self::DistinctMaterialized => "DistinctMaterialized",
391            Self::ProjectionMaterialized => "ProjectionMaterialized",
392            Self::CoveringRead => "CoveringRead",
393            Self::LimitOffset => "LimitOffset",
394            Self::CursorResume => "CursorResume",
395            Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
396            Self::TopNSeek => "TopNSeek",
397            Self::AggregateCount => "AggregateCount",
398            Self::AggregateExists => "AggregateExists",
399            Self::AggregateMin => "AggregateMin",
400            Self::AggregateMax => "AggregateMax",
401            Self::AggregateFirst => "AggregateFirst",
402            Self::AggregateLast => "AggregateLast",
403            Self::AggregateSum => "AggregateSum",
404            Self::AggregateSeekFirst => "AggregateSeekFirst",
405            Self::AggregateSeekLast => "AggregateSeekLast",
406            Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
407            Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
408            Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
409        }
410    }
411
412    /// Return the owning execution layer label for this node type.
413    #[must_use]
414    pub const fn layer_label(self) -> &'static str {
415        crate::db::query::explain::nodes::layer_label(self)
416    }
417}
418
419impl ExplainExecutionNodeDescriptor {
420    /// Return node type.
421    #[must_use]
422    pub const fn node_type(&self) -> ExplainExecutionNodeType {
423        self.node_type
424    }
425
426    /// Return execution mode.
427    #[must_use]
428    pub const fn execution_mode(&self) -> ExplainExecutionMode {
429        self.execution_mode
430    }
431
432    /// Borrow optional access strategy annotation.
433    #[must_use]
434    pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
435        self.access_strategy.as_ref()
436    }
437
438    /// Borrow optional predicate pushdown annotation.
439    #[must_use]
440    pub fn predicate_pushdown(&self) -> Option<&str> {
441        self.predicate_pushdown.as_deref()
442    }
443
444    /// Borrow optional semantic scalar filter expression annotation.
445    #[must_use]
446    pub fn filter_expr(&self) -> Option<&str> {
447        self.filter_expr.as_deref()
448    }
449
450    /// Borrow the optional explicit residual scalar filter expression.
451    #[must_use]
452    pub fn residual_filter_expr(&self) -> Option<&str> {
453        self.residual_filter_expr.as_deref()
454    }
455
456    /// Borrow the optional derived residual predicate annotation emitted
457    /// alongside `filter_expr` when execution still benefits from predicate
458    /// pushdown labeling.
459    #[must_use]
460    pub const fn residual_filter_predicate(&self) -> Option<&ExplainPredicate> {
461        self.residual_filter_predicate.as_ref()
462    }
463
464    /// Borrow optional projection annotation.
465    #[must_use]
466    pub fn projection(&self) -> Option<&str> {
467        self.projection.as_deref()
468    }
469
470    /// Return optional ordering source annotation.
471    #[must_use]
472    pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
473        self.ordering_source
474    }
475
476    /// Return optional limit annotation.
477    #[must_use]
478    pub const fn limit(&self) -> Option<u32> {
479        self.limit
480    }
481
482    /// Return optional continuation annotation.
483    #[must_use]
484    pub const fn cursor(&self) -> Option<bool> {
485        self.cursor
486    }
487
488    /// Return optional covering-scan annotation.
489    #[must_use]
490    pub const fn covering_scan(&self) -> Option<bool> {
491        self.covering_scan
492    }
493
494    /// Return optional row-count expectation annotation.
495    #[must_use]
496    pub const fn rows_expected(&self) -> Option<u64> {
497        self.rows_expected
498    }
499
500    /// Borrow child execution nodes.
501    #[must_use]
502    pub const fn children(&self) -> &[Self] {
503        self.children.as_slice()
504    }
505
506    /// Borrow node properties.
507    #[must_use]
508    pub const fn node_properties(&self) -> &ExplainPropertyMap {
509        &self.node_properties
510    }
511}
512
513pub(in crate::db::query::explain) const fn execution_mode_label(
514    mode: ExplainExecutionMode,
515) -> &'static str {
516    match mode {
517        ExplainExecutionMode::Streaming => "Streaming",
518        ExplainExecutionMode::Materialized => "Materialized",
519    }
520}
521
522pub(in crate::db::query::explain) const fn ordering_source_label(
523    ordering_source: ExplainExecutionOrderingSource,
524) -> &'static str {
525    match ordering_source {
526        ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
527        ExplainExecutionOrderingSource::Materialized => "Materialized",
528        ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
529        ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
530    }
531}