Skip to main content

icydb_core/db/diagnostics/
execution_trace.rs

1//! Module: diagnostics::execution_trace
2//! Responsibility: execution trace contracts and mutation boundaries.
3//! Does not own: execution routing policy or stream/materialization behavior.
4//! Boundary: shared trace surface used by executor and response APIs.
5
6use crate::db::{
7    executor::{ExecutionOptimization, ExecutionStats},
8    query::plan::OrderDirection,
9};
10
11#[cfg_attr(
12    doc,
13    doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
14)]
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ExecutionAccessPathVariant {
17    ByKey,
18    ByKeys,
19    KeyRange,
20    IndexPrefix,
21    IndexMultiLookup,
22    IndexRange,
23    FullScan,
24    Union,
25    Intersection,
26}
27
28#[cfg_attr(
29    doc,
30    doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
31)]
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct ExecutionTrace {
34    pub(crate) access_path_variant: ExecutionAccessPathVariant,
35    pub(crate) direction: OrderDirection,
36    pub(crate) optimization: Option<ExecutionOptimization>,
37    pub(crate) keys_scanned: u64,
38    pub(crate) rows_materialized: u64,
39    pub(crate) rows_returned: u64,
40    pub(crate) execution_time_micros: u64,
41    pub(crate) index_only: bool,
42    pub(crate) continuation_applied: bool,
43    pub(crate) index_predicate_applied: bool,
44    pub(crate) index_predicate_keys_rejected: u64,
45    pub(crate) distinct_keys_deduped: u64,
46    pub(crate) execution_stats: Option<ExecutionStats>,
47}
48
49#[cfg_attr(
50    doc,
51    doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
52)]
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct ExecutionMetrics {
55    pub(crate) rows_scanned: u64,
56    pub(crate) rows_materialized: u64,
57    pub(crate) execution_time_micros: u64,
58    pub(crate) index_only: bool,
59}
60
61impl ExecutionTrace {
62    /// Build one trace payload from an executor-projected access shape.
63    #[must_use]
64    pub(crate) const fn new_from_variant(
65        access_path_variant: ExecutionAccessPathVariant,
66        direction: OrderDirection,
67        continuation_applied: bool,
68    ) -> Self {
69        Self {
70            access_path_variant,
71            direction,
72            optimization: None,
73            keys_scanned: 0,
74            rows_materialized: 0,
75            rows_returned: 0,
76            execution_time_micros: 0,
77            index_only: false,
78            continuation_applied,
79            index_predicate_applied: false,
80            index_predicate_keys_rejected: 0,
81            distinct_keys_deduped: 0,
82            execution_stats: None,
83        }
84    }
85
86    /// Apply one finalized path outcome to this trace snapshot.
87    #[expect(clippy::too_many_arguments)]
88    pub(in crate::db) fn set_path_outcome(
89        &mut self,
90        optimization: Option<ExecutionOptimization>,
91        keys_scanned: usize,
92        rows_materialized: usize,
93        rows_returned: usize,
94        execution_time_micros: u64,
95        index_only: bool,
96        index_predicate_applied: bool,
97        index_predicate_keys_rejected: u64,
98        distinct_keys_deduped: u64,
99    ) {
100        self.optimization = optimization;
101        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
102        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
103        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
104        self.execution_time_micros = execution_time_micros;
105        self.index_only = index_only;
106        self.index_predicate_applied = index_predicate_applied;
107        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
108        self.distinct_keys_deduped = distinct_keys_deduped;
109        debug_assert_eq!(
110            self.keys_scanned,
111            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
112            "execution trace keys_scanned must match rows_scanned metrics input",
113        );
114    }
115
116    /// Attach optional operator-level execution stats to this trace.
117    pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
118        self.execution_stats = stats;
119    }
120
121    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
122    #[must_use]
123    pub const fn metrics(&self) -> ExecutionMetrics {
124        ExecutionMetrics {
125            rows_scanned: self.keys_scanned,
126            rows_materialized: self.rows_materialized,
127            execution_time_micros: self.execution_time_micros,
128            index_only: self.index_only,
129        }
130    }
131
132    /// Return the coarse executed access-path variant.
133    #[must_use]
134    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
135        self.access_path_variant
136    }
137
138    /// Return executed order direction.
139    #[must_use]
140    pub const fn direction(&self) -> OrderDirection {
141        self.direction
142    }
143
144    /// Return selected optimization, if any.
145    #[must_use]
146    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
147        self.optimization
148    }
149
150    /// Return number of keys scanned.
151    #[must_use]
152    pub const fn keys_scanned(&self) -> u64 {
153        self.keys_scanned
154    }
155
156    /// Return number of rows materialized.
157    #[must_use]
158    pub const fn rows_materialized(&self) -> u64 {
159        self.rows_materialized
160    }
161
162    /// Return number of rows returned.
163    #[must_use]
164    pub const fn rows_returned(&self) -> u64 {
165        self.rows_returned
166    }
167
168    /// Return execution time in microseconds.
169    #[must_use]
170    pub const fn execution_time_micros(&self) -> u64 {
171        self.execution_time_micros
172    }
173
174    /// Return whether execution remained index-only.
175    #[must_use]
176    pub const fn index_only(&self) -> bool {
177        self.index_only
178    }
179
180    /// Return whether continuation was applied.
181    #[must_use]
182    pub const fn continuation_applied(&self) -> bool {
183        self.continuation_applied
184    }
185
186    /// Return whether index predicate pushdown was applied.
187    #[must_use]
188    pub const fn index_predicate_applied(&self) -> bool {
189        self.index_predicate_applied
190    }
191
192    /// Return number of keys rejected by index predicate pushdown.
193    #[must_use]
194    pub const fn index_predicate_keys_rejected(&self) -> u64 {
195        self.index_predicate_keys_rejected
196    }
197
198    /// Return number of deduplicated keys under DISTINCT processing.
199    #[must_use]
200    pub const fn distinct_keys_deduped(&self) -> u64 {
201        self.distinct_keys_deduped
202    }
203
204    /// Return optional operator-level execution stats for this trace.
205    #[must_use]
206    #[cfg_attr(
207        not(test),
208        expect(
209            dead_code,
210            reason = "execution stats are an internal diagnostics/testing surface"
211        )
212    )]
213    pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
214        self.execution_stats
215    }
216}
217
218impl ExecutionMetrics {
219    /// Return number of rows scanned.
220    #[must_use]
221    pub const fn rows_scanned(&self) -> u64 {
222        self.rows_scanned
223    }
224
225    /// Return number of rows materialized.
226    #[must_use]
227    pub const fn rows_materialized(&self) -> u64 {
228        self.rows_materialized
229    }
230
231    /// Return execution time in microseconds.
232    #[must_use]
233    pub const fn execution_time_micros(&self) -> u64 {
234        self.execution_time_micros
235    }
236
237    /// Return whether execution remained index-only.
238    #[must_use]
239    pub const fn index_only(&self) -> bool {
240        self.index_only
241    }
242}