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    FullScan,
202    IndexBranchSet,
203    IndexMultiLookup,
204    IndexPrefix,
205    IndexRange,
206    KeyRange,
207    Union,
208    Intersection,
209}
210
211#[cfg_attr(
212    doc,
213    doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
214)]
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub struct ExecutionTrace {
217    pub(crate) access_path_variant: ExecutionAccessPathVariant,
218    pub(crate) direction: OrderDirection,
219    pub(crate) optimization: Option<ExecutionOptimization>,
220    pub(crate) keys_scanned: u64,
221    pub(crate) rows_materialized: u64,
222    pub(crate) rows_returned: u64,
223    pub(crate) execution_time_micros: u64,
224    pub(crate) index_only: bool,
225    pub(crate) continuation_applied: bool,
226    pub(crate) index_predicate_applied: bool,
227    pub(crate) index_predicate_keys_rejected: u64,
228    pub(crate) distinct_keys_deduped: u64,
229    pub(crate) execution_stats: Option<ExecutionStats>,
230}
231
232#[cfg_attr(
233    doc,
234    doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
235)]
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub struct ExecutionMetrics {
238    pub(crate) rows_scanned: u64,
239    pub(crate) rows_materialized: u64,
240    pub(crate) execution_time_micros: u64,
241    pub(crate) index_only: bool,
242}
243
244impl ExecutionTrace {
245    /// Build one trace payload from an executor-projected access shape.
246    #[must_use]
247    pub(crate) const fn new_from_variant(
248        access_path_variant: ExecutionAccessPathVariant,
249        direction: OrderDirection,
250        continuation_applied: bool,
251    ) -> Self {
252        Self {
253            access_path_variant,
254            direction,
255            optimization: None,
256            keys_scanned: 0,
257            rows_materialized: 0,
258            rows_returned: 0,
259            execution_time_micros: 0,
260            index_only: false,
261            continuation_applied,
262            index_predicate_applied: false,
263            index_predicate_keys_rejected: 0,
264            distinct_keys_deduped: 0,
265            execution_stats: None,
266        }
267    }
268
269    /// Apply one finalized path outcome to this trace snapshot.
270    #[expect(clippy::too_many_arguments)]
271    pub(in crate::db) fn set_path_outcome(
272        &mut self,
273        optimization: Option<ExecutionOptimization>,
274        keys_scanned: usize,
275        rows_materialized: usize,
276        rows_returned: usize,
277        execution_time_micros: u64,
278        index_only: bool,
279        index_predicate_applied: bool,
280        index_predicate_keys_rejected: u64,
281        distinct_keys_deduped: u64,
282    ) {
283        self.optimization = optimization;
284        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
285        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
286        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
287        self.execution_time_micros = execution_time_micros;
288        self.index_only = index_only;
289        self.index_predicate_applied = index_predicate_applied;
290        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
291        self.distinct_keys_deduped = distinct_keys_deduped;
292        debug_assert_eq!(
293            self.keys_scanned,
294            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
295            "execution trace keys_scanned must match rows_scanned metrics input",
296        );
297    }
298
299    /// Attach optional operator-level execution stats to this trace.
300    pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
301        self.execution_stats = stats;
302    }
303
304    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
305    #[must_use]
306    pub const fn metrics(&self) -> ExecutionMetrics {
307        ExecutionMetrics {
308            rows_scanned: self.keys_scanned,
309            rows_materialized: self.rows_materialized,
310            execution_time_micros: self.execution_time_micros,
311            index_only: self.index_only,
312        }
313    }
314
315    /// Return the coarse executed access-path variant.
316    #[must_use]
317    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
318        self.access_path_variant
319    }
320
321    /// Return executed order direction.
322    #[must_use]
323    pub const fn direction(&self) -> OrderDirection {
324        self.direction
325    }
326
327    /// Return selected optimization, if any.
328    #[must_use]
329    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
330        self.optimization
331    }
332
333    /// Return number of keys scanned.
334    #[must_use]
335    pub const fn keys_scanned(&self) -> u64 {
336        self.keys_scanned
337    }
338
339    /// Return number of rows materialized.
340    #[must_use]
341    pub const fn rows_materialized(&self) -> u64 {
342        self.rows_materialized
343    }
344
345    /// Return number of rows returned.
346    #[must_use]
347    pub const fn rows_returned(&self) -> u64 {
348        self.rows_returned
349    }
350
351    /// Return execution time in microseconds.
352    #[must_use]
353    pub const fn execution_time_micros(&self) -> u64 {
354        self.execution_time_micros
355    }
356
357    /// Return whether execution remained index-only.
358    #[must_use]
359    pub const fn index_only(&self) -> bool {
360        self.index_only
361    }
362
363    /// Return whether continuation was applied.
364    #[must_use]
365    pub const fn continuation_applied(&self) -> bool {
366        self.continuation_applied
367    }
368
369    /// Return whether index predicate pushdown was applied.
370    #[must_use]
371    pub const fn index_predicate_applied(&self) -> bool {
372        self.index_predicate_applied
373    }
374
375    /// Return number of keys rejected by index predicate pushdown.
376    #[must_use]
377    pub const fn index_predicate_keys_rejected(&self) -> u64 {
378        self.index_predicate_keys_rejected
379    }
380
381    /// Return number of deduplicated keys under DISTINCT processing.
382    #[must_use]
383    pub const fn distinct_keys_deduped(&self) -> u64 {
384        self.distinct_keys_deduped
385    }
386
387    /// Return optional operator-level execution stats for this trace.
388    #[must_use]
389    #[cfg_attr(
390        not(test),
391        expect(
392            dead_code,
393            reason = "execution stats are an internal diagnostics/testing surface"
394        )
395    )]
396    pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
397        self.execution_stats
398    }
399}
400
401impl ExecutionMetrics {
402    /// Return number of rows scanned.
403    #[must_use]
404    pub const fn rows_scanned(&self) -> u64 {
405        self.rows_scanned
406    }
407
408    /// Return number of rows materialized.
409    #[must_use]
410    pub const fn rows_materialized(&self) -> u64 {
411        self.rows_materialized
412    }
413
414    /// Return execution time in microseconds.
415    #[must_use]
416    pub const fn execution_time_micros(&self) -> u64 {
417        self.execution_time_micros
418    }
419
420    /// Return whether execution remained index-only.
421    #[must_use]
422    pub const fn index_only(&self) -> bool {
423        self.index_only
424    }
425}