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
81#[cfg_attr(
82    doc,
83    doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
84)]
85#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum ExecutionAccessPathVariant {
87    ByKey,
88    ByKeys,
89    FullScan,
90    IndexBranchSet,
91    IndexMultiLookup,
92    IndexPrefix,
93    IndexRange,
94    KeyRange,
95    Union,
96    Intersection,
97}
98
99#[cfg_attr(
100    doc,
101    doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
102)]
103#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub struct ExecutionTrace {
105    pub(crate) access_path_variant: ExecutionAccessPathVariant,
106    pub(crate) direction: OrderDirection,
107    pub(crate) optimization: Option<ExecutionOptimization>,
108    pub(crate) keys_scanned: u64,
109    pub(crate) rows_materialized: u64,
110    pub(crate) rows_returned: u64,
111    pub(crate) execution_time_micros: u64,
112    pub(crate) index_only: bool,
113    pub(crate) continuation_applied: bool,
114    pub(crate) index_predicate_applied: bool,
115    pub(crate) index_predicate_keys_rejected: u64,
116    pub(crate) distinct_keys_deduped: u64,
117    pub(crate) execution_stats: Option<ExecutionStats>,
118}
119
120#[cfg_attr(
121    doc,
122    doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
123)]
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
125pub struct ExecutionMetrics {
126    pub(crate) rows_scanned: u64,
127    pub(crate) rows_materialized: u64,
128    pub(crate) execution_time_micros: u64,
129    pub(crate) index_only: bool,
130}
131
132impl ExecutionTrace {
133    /// Build one trace payload from an executor-projected access shape.
134    #[must_use]
135    pub(in crate::db) const fn new_from_variant(
136        access_path_variant: ExecutionAccessPathVariant,
137        direction: OrderDirection,
138        continuation_applied: bool,
139    ) -> Self {
140        Self {
141            access_path_variant,
142            direction,
143            optimization: None,
144            keys_scanned: 0,
145            rows_materialized: 0,
146            rows_returned: 0,
147            execution_time_micros: 0,
148            index_only: false,
149            continuation_applied,
150            index_predicate_applied: false,
151            index_predicate_keys_rejected: 0,
152            distinct_keys_deduped: 0,
153            execution_stats: None,
154        }
155    }
156
157    /// Apply one finalized path outcome to this trace snapshot.
158    #[expect(clippy::too_many_arguments)]
159    pub(in crate::db) fn set_path_outcome(
160        &mut self,
161        optimization: Option<ExecutionOptimization>,
162        keys_scanned: usize,
163        rows_materialized: usize,
164        rows_returned: usize,
165        execution_time_micros: u64,
166        index_only: bool,
167        index_predicate_applied: bool,
168        index_predicate_keys_rejected: u64,
169        distinct_keys_deduped: u64,
170    ) {
171        self.optimization = optimization;
172        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
173        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
174        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
175        self.execution_time_micros = execution_time_micros;
176        self.index_only = index_only;
177        self.index_predicate_applied = index_predicate_applied;
178        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
179        self.distinct_keys_deduped = distinct_keys_deduped;
180        debug_assert_eq!(
181            self.keys_scanned,
182            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
183            "execution trace keys_scanned must match rows_scanned metrics input",
184        );
185    }
186
187    /// Attach optional operator-level execution stats to this trace.
188    pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
189        self.execution_stats = stats;
190    }
191
192    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
193    #[must_use]
194    pub const fn metrics(&self) -> ExecutionMetrics {
195        ExecutionMetrics {
196            rows_scanned: self.keys_scanned,
197            rows_materialized: self.rows_materialized,
198            execution_time_micros: self.execution_time_micros,
199            index_only: self.index_only,
200        }
201    }
202
203    /// Return the coarse executed access-path variant.
204    #[must_use]
205    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
206        self.access_path_variant
207    }
208
209    /// Return executed order direction.
210    #[must_use]
211    pub const fn direction(&self) -> OrderDirection {
212        self.direction
213    }
214
215    /// Return selected optimization, if any.
216    #[must_use]
217    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
218        self.optimization
219    }
220
221    /// Return number of keys scanned.
222    #[must_use]
223    pub const fn keys_scanned(&self) -> u64 {
224        self.keys_scanned
225    }
226
227    /// Return number of rows materialized.
228    #[must_use]
229    pub const fn rows_materialized(&self) -> u64 {
230        self.rows_materialized
231    }
232
233    /// Return number of rows returned.
234    #[must_use]
235    pub const fn rows_returned(&self) -> u64 {
236        self.rows_returned
237    }
238
239    /// Return execution time in microseconds.
240    #[must_use]
241    pub const fn execution_time_micros(&self) -> u64 {
242        self.execution_time_micros
243    }
244
245    /// Return whether execution remained index-only.
246    #[must_use]
247    pub const fn index_only(&self) -> bool {
248        self.index_only
249    }
250
251    /// Return whether continuation was applied.
252    #[must_use]
253    pub const fn continuation_applied(&self) -> bool {
254        self.continuation_applied
255    }
256
257    /// Return whether index predicate pushdown was applied.
258    #[must_use]
259    pub const fn index_predicate_applied(&self) -> bool {
260        self.index_predicate_applied
261    }
262
263    /// Return number of keys rejected by index predicate pushdown.
264    #[must_use]
265    pub const fn index_predicate_keys_rejected(&self) -> u64 {
266        self.index_predicate_keys_rejected
267    }
268
269    /// Return number of deduplicated keys under DISTINCT processing.
270    #[must_use]
271    pub const fn distinct_keys_deduped(&self) -> u64 {
272        self.distinct_keys_deduped
273    }
274}
275
276impl ExecutionMetrics {
277    /// Return number of rows scanned.
278    #[must_use]
279    pub const fn rows_scanned(&self) -> u64 {
280        self.rows_scanned
281    }
282
283    /// Return number of rows materialized.
284    #[must_use]
285    pub const fn rows_materialized(&self) -> u64 {
286        self.rows_materialized
287    }
288
289    /// Return execution time in microseconds.
290    #[must_use]
291    pub const fn execution_time_micros(&self) -> u64 {
292        self.execution_time_micros
293    }
294
295    /// Return whether execution remained index-only.
296    #[must_use]
297    pub const fn index_only(&self) -> bool {
298        self.index_only
299    }
300}