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::query::plan::OrderDirection;
7
8///
9/// ExecutionOptimization
10///
11/// Load optimization label selected during execution and recorded in
12/// diagnostics traces.
13/// Diagnostics owns the DTO; executor code only chooses which label to attach.
14///
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ExecutionOptimization {
18    PrimaryKey,
19    PrimaryKeyTopNSeek,
20    SecondaryOrderPushdown,
21    SecondaryOrderTopNSeek,
22    IndexRangeLimitPushdown,
23}
24
25///
26/// ExecutionStats
27///
28/// Diagnostics-owned operator stats snapshot for one traced query execution.
29/// Executor profiling maps its internal counters into this DTO at the trace
30/// boundary so diagnostics does not depend on executor-owned types.
31///
32
33#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
34pub struct ExecutionStats {
35    rows_scanned_pre_filter: u64,
36    rows_after_predicate: u64,
37    rows_after_projection: u64,
38    rows_after_distinct: u64,
39    rows_sorted: u64,
40    keys_streamed: u64,
41    key_stream_micros: u64,
42    ordering_micros: u64,
43    projection_micros: u64,
44    aggregation_micros: u64,
45}
46
47impl ExecutionStats {
48    /// Build one diagnostics stats DTO from already-normalized counters.
49    #[must_use]
50    #[expect(
51        clippy::too_many_arguments,
52        reason = "diagnostics stats DTO exposes the exact flat trace counter surface"
53    )]
54    pub(in crate::db) const fn new(
55        rows_scanned_pre_filter: u64,
56        rows_after_predicate: u64,
57        rows_after_projection: u64,
58        rows_after_distinct: u64,
59        rows_sorted: u64,
60        keys_streamed: u64,
61        key_stream_micros: u64,
62        ordering_micros: u64,
63        projection_micros: u64,
64        aggregation_micros: u64,
65    ) -> Self {
66        Self {
67            rows_scanned_pre_filter,
68            rows_after_predicate,
69            rows_after_projection,
70            rows_after_distinct,
71            rows_sorted,
72            keys_streamed,
73            key_stream_micros,
74            ordering_micros,
75            projection_micros,
76            aggregation_micros,
77        }
78    }
79
80    /// Return rows encountered before post-access predicate filtering.
81    #[cfg(test)]
82    #[must_use]
83    pub(in crate::db) const fn rows_scanned_pre_filter(&self) -> u64 {
84        self.rows_scanned_pre_filter
85    }
86
87    /// Return rows retained after predicate filtering.
88    #[cfg(test)]
89    #[must_use]
90    pub(in crate::db) const fn rows_after_predicate(&self) -> u64 {
91        self.rows_after_predicate
92    }
93
94    /// Return rows retained after final projection/materialization.
95    #[cfg(test)]
96    #[must_use]
97    pub(in crate::db) const fn rows_after_projection(&self) -> u64 {
98        self.rows_after_projection
99    }
100
101    /// Return number of physical keys yielded by ordered key streams.
102    #[cfg(test)]
103    #[must_use]
104    pub(in crate::db) const fn keys_streamed(&self) -> u64 {
105        self.keys_streamed
106    }
107}
108
109#[cfg_attr(
110    doc,
111    doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
112)]
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum ExecutionAccessPathVariant {
115    ByKey,
116    ByKeys,
117    FullScan,
118    IndexBranchSet,
119    IndexMultiLookup,
120    IndexPrefix,
121    IndexRange,
122    KeyRange,
123    Union,
124    Intersection,
125}
126
127#[cfg_attr(
128    doc,
129    doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
130)]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub struct ExecutionTrace {
133    pub(crate) access_path_variant: ExecutionAccessPathVariant,
134    pub(crate) direction: OrderDirection,
135    pub(crate) optimization: Option<ExecutionOptimization>,
136    pub(crate) keys_scanned: u64,
137    pub(crate) rows_materialized: u64,
138    pub(crate) rows_returned: u64,
139    pub(crate) execution_time_micros: u64,
140    pub(crate) index_only: bool,
141    pub(crate) continuation_applied: bool,
142    pub(crate) index_predicate_applied: bool,
143    pub(crate) index_predicate_keys_rejected: u64,
144    pub(crate) distinct_keys_deduped: u64,
145    pub(crate) execution_stats: Option<ExecutionStats>,
146}
147
148#[cfg_attr(
149    doc,
150    doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
151)]
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct ExecutionMetrics {
154    pub(crate) rows_scanned: u64,
155    pub(crate) rows_materialized: u64,
156    pub(crate) execution_time_micros: u64,
157    pub(crate) index_only: bool,
158}
159
160impl ExecutionTrace {
161    /// Build one trace payload from an executor-projected access shape.
162    #[must_use]
163    pub(in crate::db) const fn new_from_variant(
164        access_path_variant: ExecutionAccessPathVariant,
165        direction: OrderDirection,
166        continuation_applied: bool,
167    ) -> Self {
168        Self {
169            access_path_variant,
170            direction,
171            optimization: None,
172            keys_scanned: 0,
173            rows_materialized: 0,
174            rows_returned: 0,
175            execution_time_micros: 0,
176            index_only: false,
177            continuation_applied,
178            index_predicate_applied: false,
179            index_predicate_keys_rejected: 0,
180            distinct_keys_deduped: 0,
181            execution_stats: None,
182        }
183    }
184
185    /// Apply one finalized path outcome to this trace snapshot.
186    #[expect(clippy::too_many_arguments)]
187    pub(in crate::db) fn set_path_outcome(
188        &mut self,
189        optimization: Option<ExecutionOptimization>,
190        keys_scanned: usize,
191        rows_materialized: usize,
192        rows_returned: usize,
193        execution_time_micros: u64,
194        index_only: bool,
195        index_predicate_applied: bool,
196        index_predicate_keys_rejected: u64,
197        distinct_keys_deduped: u64,
198    ) {
199        self.optimization = optimization;
200        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
201        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
202        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
203        self.execution_time_micros = execution_time_micros;
204        self.index_only = index_only;
205        self.index_predicate_applied = index_predicate_applied;
206        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
207        self.distinct_keys_deduped = distinct_keys_deduped;
208        debug_assert_eq!(
209            self.keys_scanned,
210            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
211            "execution trace keys_scanned must match rows_scanned metrics input",
212        );
213    }
214
215    /// Attach optional operator-level execution stats to this trace.
216    pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
217        self.execution_stats = stats;
218    }
219
220    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
221    #[must_use]
222    pub const fn metrics(&self) -> ExecutionMetrics {
223        ExecutionMetrics {
224            rows_scanned: self.keys_scanned,
225            rows_materialized: self.rows_materialized,
226            execution_time_micros: self.execution_time_micros,
227            index_only: self.index_only,
228        }
229    }
230
231    /// Return the coarse executed access-path variant.
232    #[must_use]
233    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
234        self.access_path_variant
235    }
236
237    /// Return executed order direction.
238    #[must_use]
239    pub const fn direction(&self) -> OrderDirection {
240        self.direction
241    }
242
243    /// Return selected optimization, if any.
244    #[must_use]
245    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
246        self.optimization
247    }
248
249    /// Return number of keys scanned.
250    #[must_use]
251    pub const fn keys_scanned(&self) -> u64 {
252        self.keys_scanned
253    }
254
255    /// Return number of rows materialized.
256    #[must_use]
257    pub const fn rows_materialized(&self) -> u64 {
258        self.rows_materialized
259    }
260
261    /// Return number of rows returned.
262    #[must_use]
263    pub const fn rows_returned(&self) -> u64 {
264        self.rows_returned
265    }
266
267    /// Return execution time in microseconds.
268    #[must_use]
269    pub const fn execution_time_micros(&self) -> u64 {
270        self.execution_time_micros
271    }
272
273    /// Return whether execution remained index-only.
274    #[must_use]
275    pub const fn index_only(&self) -> bool {
276        self.index_only
277    }
278
279    /// Return whether continuation was applied.
280    #[must_use]
281    pub const fn continuation_applied(&self) -> bool {
282        self.continuation_applied
283    }
284
285    /// Return whether index predicate pushdown was applied.
286    #[must_use]
287    pub const fn index_predicate_applied(&self) -> bool {
288        self.index_predicate_applied
289    }
290
291    /// Return number of keys rejected by index predicate pushdown.
292    #[must_use]
293    pub const fn index_predicate_keys_rejected(&self) -> u64 {
294        self.index_predicate_keys_rejected
295    }
296
297    /// Return number of deduplicated keys under DISTINCT processing.
298    #[must_use]
299    pub const fn distinct_keys_deduped(&self) -> u64 {
300        self.distinct_keys_deduped
301    }
302
303    /// Return optional operator-level execution stats for this trace.
304    #[cfg(test)]
305    #[must_use]
306    pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
307        self.execution_stats
308    }
309}
310
311impl ExecutionMetrics {
312    /// Return number of rows scanned.
313    #[must_use]
314    pub const fn rows_scanned(&self) -> u64 {
315        self.rows_scanned
316    }
317
318    /// Return number of rows materialized.
319    #[must_use]
320    pub const fn rows_materialized(&self) -> u64 {
321        self.rows_materialized
322    }
323
324    /// Return execution time in microseconds.
325    #[must_use]
326    pub const fn execution_time_micros(&self) -> u64 {
327        self.execution_time_micros
328    }
329
330    /// Return whether execution remained index-only.
331    #[must_use]
332    pub const fn index_only(&self) -> bool {
333        self.index_only
334    }
335}