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    #[must_use]
82    #[cfg_attr(
83        not(test),
84        expect(
85            dead_code,
86            reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
87        )
88    )]
89    pub(in crate::db) const fn rows_scanned_pre_filter(&self) -> u64 {
90        self.rows_scanned_pre_filter
91    }
92
93    /// Return rows retained after predicate filtering.
94    #[must_use]
95    #[cfg_attr(
96        not(test),
97        expect(
98            dead_code,
99            reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
100        )
101    )]
102    pub(in crate::db) const fn rows_after_predicate(&self) -> u64 {
103        self.rows_after_predicate
104    }
105
106    /// Return rows retained after final projection/materialization.
107    #[must_use]
108    #[cfg_attr(
109        not(test),
110        expect(
111            dead_code,
112            reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
113        )
114    )]
115    pub(in crate::db) const fn rows_after_projection(&self) -> u64 {
116        self.rows_after_projection
117    }
118
119    /// Return rows retained after DISTINCT processing when applicable.
120    #[must_use]
121    #[expect(
122        dead_code,
123        reason = "execution profiling records this for diagnostics consumers before response exposure"
124    )]
125    pub(in crate::db) const fn rows_after_distinct(&self) -> u64 {
126        self.rows_after_distinct
127    }
128
129    /// Return rows submitted to in-memory ordering.
130    #[must_use]
131    #[expect(
132        dead_code,
133        reason = "execution profiling records this for diagnostics consumers before response exposure"
134    )]
135    pub(in crate::db) const fn rows_sorted(&self) -> u64 {
136        self.rows_sorted
137    }
138
139    /// Return number of physical keys yielded by ordered key streams.
140    #[must_use]
141    #[cfg_attr(
142        not(test),
143        expect(
144            dead_code,
145            reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
146        )
147    )]
148    pub(in crate::db) const fn keys_streamed(&self) -> u64 {
149        self.keys_streamed
150    }
151
152    /// Return microseconds spent polling ordered key streams.
153    #[must_use]
154    #[expect(
155        dead_code,
156        reason = "execution profiling records this for diagnostics consumers before response exposure"
157    )]
158    pub(in crate::db) const fn key_stream_micros(&self) -> u64 {
159        self.key_stream_micros
160    }
161
162    /// Return microseconds spent in in-memory ordering.
163    #[must_use]
164    #[expect(
165        dead_code,
166        reason = "execution profiling records this for diagnostics consumers before response exposure"
167    )]
168    pub(in crate::db) const fn ordering_micros(&self) -> u64 {
169        self.ordering_micros
170    }
171
172    /// Return microseconds spent finalizing projection payloads.
173    #[must_use]
174    #[expect(
175        dead_code,
176        reason = "execution profiling records this for diagnostics consumers before response exposure"
177    )]
178    pub(in crate::db) const fn projection_micros(&self) -> u64 {
179        self.projection_micros
180    }
181
182    /// Return microseconds spent in grouped aggregation fold work.
183    #[must_use]
184    #[expect(
185        dead_code,
186        reason = "execution profiling records this for diagnostics consumers before response exposure"
187    )]
188    pub(in crate::db) const fn aggregation_micros(&self) -> u64 {
189        self.aggregation_micros
190    }
191}
192
193#[cfg_attr(
194    doc,
195    doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
196)]
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum ExecutionAccessPathVariant {
199    ByKey,
200    ByKeys,
201    KeyRange,
202    IndexPrefix,
203    IndexMultiLookup,
204    IndexRange,
205    FullScan,
206    Union,
207    Intersection,
208}
209
210#[cfg_attr(
211    doc,
212    doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
213)]
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct ExecutionTrace {
216    pub(crate) access_path_variant: ExecutionAccessPathVariant,
217    pub(crate) direction: OrderDirection,
218    pub(crate) optimization: Option<ExecutionOptimization>,
219    pub(crate) keys_scanned: u64,
220    pub(crate) rows_materialized: u64,
221    pub(crate) rows_returned: u64,
222    pub(crate) execution_time_micros: u64,
223    pub(crate) index_only: bool,
224    pub(crate) continuation_applied: bool,
225    pub(crate) index_predicate_applied: bool,
226    pub(crate) index_predicate_keys_rejected: u64,
227    pub(crate) distinct_keys_deduped: u64,
228    pub(crate) execution_stats: Option<ExecutionStats>,
229}
230
231#[cfg_attr(
232    doc,
233    doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
234)]
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub struct ExecutionMetrics {
237    pub(crate) rows_scanned: u64,
238    pub(crate) rows_materialized: u64,
239    pub(crate) execution_time_micros: u64,
240    pub(crate) index_only: bool,
241}
242
243impl ExecutionTrace {
244    /// Build one trace payload from an executor-projected access shape.
245    #[must_use]
246    pub(crate) const fn new_from_variant(
247        access_path_variant: ExecutionAccessPathVariant,
248        direction: OrderDirection,
249        continuation_applied: bool,
250    ) -> Self {
251        Self {
252            access_path_variant,
253            direction,
254            optimization: None,
255            keys_scanned: 0,
256            rows_materialized: 0,
257            rows_returned: 0,
258            execution_time_micros: 0,
259            index_only: false,
260            continuation_applied,
261            index_predicate_applied: false,
262            index_predicate_keys_rejected: 0,
263            distinct_keys_deduped: 0,
264            execution_stats: None,
265        }
266    }
267
268    /// Apply one finalized path outcome to this trace snapshot.
269    #[expect(clippy::too_many_arguments)]
270    pub(in crate::db) fn set_path_outcome(
271        &mut self,
272        optimization: Option<ExecutionOptimization>,
273        keys_scanned: usize,
274        rows_materialized: usize,
275        rows_returned: usize,
276        execution_time_micros: u64,
277        index_only: bool,
278        index_predicate_applied: bool,
279        index_predicate_keys_rejected: u64,
280        distinct_keys_deduped: u64,
281    ) {
282        self.optimization = optimization;
283        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
284        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
285        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
286        self.execution_time_micros = execution_time_micros;
287        self.index_only = index_only;
288        self.index_predicate_applied = index_predicate_applied;
289        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
290        self.distinct_keys_deduped = distinct_keys_deduped;
291        debug_assert_eq!(
292            self.keys_scanned,
293            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
294            "execution trace keys_scanned must match rows_scanned metrics input",
295        );
296    }
297
298    /// Attach optional operator-level execution stats to this trace.
299    pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
300        self.execution_stats = stats;
301    }
302
303    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
304    #[must_use]
305    pub const fn metrics(&self) -> ExecutionMetrics {
306        ExecutionMetrics {
307            rows_scanned: self.keys_scanned,
308            rows_materialized: self.rows_materialized,
309            execution_time_micros: self.execution_time_micros,
310            index_only: self.index_only,
311        }
312    }
313
314    /// Return the coarse executed access-path variant.
315    #[must_use]
316    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
317        self.access_path_variant
318    }
319
320    /// Return executed order direction.
321    #[must_use]
322    pub const fn direction(&self) -> OrderDirection {
323        self.direction
324    }
325
326    /// Return selected optimization, if any.
327    #[must_use]
328    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
329        self.optimization
330    }
331
332    /// Return number of keys scanned.
333    #[must_use]
334    pub const fn keys_scanned(&self) -> u64 {
335        self.keys_scanned
336    }
337
338    /// Return number of rows materialized.
339    #[must_use]
340    pub const fn rows_materialized(&self) -> u64 {
341        self.rows_materialized
342    }
343
344    /// Return number of rows returned.
345    #[must_use]
346    pub const fn rows_returned(&self) -> u64 {
347        self.rows_returned
348    }
349
350    /// Return execution time in microseconds.
351    #[must_use]
352    pub const fn execution_time_micros(&self) -> u64 {
353        self.execution_time_micros
354    }
355
356    /// Return whether execution remained index-only.
357    #[must_use]
358    pub const fn index_only(&self) -> bool {
359        self.index_only
360    }
361
362    /// Return whether continuation was applied.
363    #[must_use]
364    pub const fn continuation_applied(&self) -> bool {
365        self.continuation_applied
366    }
367
368    /// Return whether index predicate pushdown was applied.
369    #[must_use]
370    pub const fn index_predicate_applied(&self) -> bool {
371        self.index_predicate_applied
372    }
373
374    /// Return number of keys rejected by index predicate pushdown.
375    #[must_use]
376    pub const fn index_predicate_keys_rejected(&self) -> u64 {
377        self.index_predicate_keys_rejected
378    }
379
380    /// Return number of deduplicated keys under DISTINCT processing.
381    #[must_use]
382    pub const fn distinct_keys_deduped(&self) -> u64 {
383        self.distinct_keys_deduped
384    }
385
386    /// Return optional operator-level execution stats for this trace.
387    #[must_use]
388    #[cfg_attr(
389        not(test),
390        expect(
391            dead_code,
392            reason = "execution stats are an internal diagnostics/testing surface"
393        )
394    )]
395    pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
396        self.execution_stats
397    }
398}
399
400impl ExecutionMetrics {
401    /// Return number of rows scanned.
402    #[must_use]
403    pub const fn rows_scanned(&self) -> u64 {
404        self.rows_scanned
405    }
406
407    /// Return number of rows materialized.
408    #[must_use]
409    pub const fn rows_materialized(&self) -> u64 {
410        self.rows_materialized
411    }
412
413    /// Return execution time in microseconds.
414    #[must_use]
415    pub const fn execution_time_micros(&self) -> u64 {
416        self.execution_time_micros
417    }
418
419    /// Return whether execution remained index-only.
420    #[must_use]
421    pub const fn index_only(&self) -> bool {
422        self.index_only
423    }
424}