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    access::{AccessPathKind, AccessPlan, AccessPlanDispatch, dispatch_access_plan},
8    direction::Direction,
9    query::plan::OrderDirection,
10};
11
12///
13/// ExecutionAccessPathVariant
14///
15/// Coarse access path shape used by the load execution trace surface.
16///
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum ExecutionAccessPathVariant {
20    ByKey,
21    ByKeys,
22    KeyRange,
23    IndexPrefix,
24    IndexMultiLookup,
25    IndexRange,
26    FullScan,
27    Union,
28    Intersection,
29}
30
31///
32/// ExecutionOptimization
33///
34/// Canonical load optimization selected by execution, if any.
35///
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum ExecutionOptimization {
39    PrimaryKey,
40    PrimaryKeyTopNSeek,
41    SecondaryOrderPushdown,
42    SecondaryOrderTopNSeek,
43    IndexRangeLimitPushdown,
44}
45
46///
47/// ExecutionOptimizationCounter
48///
49/// Canonical test-only optimization counter taxonomy.
50/// This keeps fast-path hit counters aligned with one shared naming surface.
51///
52
53#[cfg(test)]
54#[expect(clippy::enum_variant_names)]
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub(crate) enum ExecutionOptimizationCounter {
57    BytesPrimaryKeyFastPath,
58    BytesStreamFastPath,
59    CoveringExistsFastPath,
60    CoveringCountFastPath,
61    PrimaryKeyCountFastPath,
62    PrimaryKeyCardinalityCountFastPath,
63    CoveringIndexProjectionFastPath,
64    CoveringConstantProjectionFastPath,
65}
66
67///
68/// ExecutionTrace
69///
70/// Structured, opt-in load execution introspection snapshot.
71/// Captures plan-shape and execution decisions without changing semantics.
72///
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct ExecutionTrace {
76    pub(crate) access_path_variant: ExecutionAccessPathVariant,
77    pub(crate) direction: OrderDirection,
78    pub(crate) optimization: Option<ExecutionOptimization>,
79    pub(crate) keys_scanned: u64,
80    pub(crate) rows_materialized: u64,
81    pub(crate) rows_returned: u64,
82    pub(crate) execution_time_micros: u64,
83    pub(crate) index_only: bool,
84    pub(crate) continuation_applied: bool,
85    pub(crate) index_predicate_applied: bool,
86    pub(crate) index_predicate_keys_rejected: u64,
87    pub(crate) distinct_keys_deduped: u64,
88}
89
90///
91/// ExecutionMetrics
92///
93/// Compact execution metrics projection derived from one `ExecutionTrace`.
94/// This surface is intentionally small and stable for pre-EXPLAIN observability.
95///
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct ExecutionMetrics {
99    pub(crate) rows_scanned: u64,
100    pub(crate) rows_materialized: u64,
101    pub(crate) execution_time_micros: u64,
102    pub(crate) index_only: bool,
103}
104
105impl ExecutionTrace {
106    /// Build one trace payload from canonical access shape and runtime direction.
107    #[must_use]
108    pub(in crate::db) fn new<K>(
109        access: &AccessPlan<K>,
110        direction: Direction,
111        continuation_applied: bool,
112    ) -> Self {
113        Self {
114            access_path_variant: access_path_variant(access),
115            direction: execution_order_direction(direction),
116            optimization: None,
117            keys_scanned: 0,
118            rows_materialized: 0,
119            rows_returned: 0,
120            execution_time_micros: 0,
121            index_only: false,
122            continuation_applied,
123            index_predicate_applied: false,
124            index_predicate_keys_rejected: 0,
125            distinct_keys_deduped: 0,
126        }
127    }
128
129    /// Apply one finalized path outcome to this trace snapshot.
130    #[expect(clippy::too_many_arguments)]
131    pub(in crate::db) fn set_path_outcome(
132        &mut self,
133        optimization: Option<ExecutionOptimization>,
134        keys_scanned: usize,
135        rows_materialized: usize,
136        rows_returned: usize,
137        execution_time_micros: u64,
138        index_only: bool,
139        index_predicate_applied: bool,
140        index_predicate_keys_rejected: u64,
141        distinct_keys_deduped: u64,
142    ) {
143        self.optimization = optimization;
144        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
145        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
146        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
147        self.execution_time_micros = execution_time_micros;
148        self.index_only = index_only;
149        self.index_predicate_applied = index_predicate_applied;
150        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
151        self.distinct_keys_deduped = distinct_keys_deduped;
152        debug_assert_eq!(
153            self.keys_scanned,
154            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
155            "execution trace keys_scanned must match rows_scanned metrics input",
156        );
157    }
158
159    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
160    #[must_use]
161    pub const fn metrics(&self) -> ExecutionMetrics {
162        ExecutionMetrics {
163            rows_scanned: self.keys_scanned,
164            rows_materialized: self.rows_materialized,
165            execution_time_micros: self.execution_time_micros,
166            index_only: self.index_only,
167        }
168    }
169
170    /// Return the coarse executed access-path variant.
171    #[must_use]
172    pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
173        self.access_path_variant
174    }
175
176    /// Return executed order direction.
177    #[must_use]
178    pub const fn direction(&self) -> OrderDirection {
179        self.direction
180    }
181
182    /// Return selected optimization, if any.
183    #[must_use]
184    pub const fn optimization(&self) -> Option<ExecutionOptimization> {
185        self.optimization
186    }
187
188    /// Return number of keys scanned.
189    #[must_use]
190    pub const fn keys_scanned(&self) -> u64 {
191        self.keys_scanned
192    }
193
194    /// Return number of rows materialized.
195    #[must_use]
196    pub const fn rows_materialized(&self) -> u64 {
197        self.rows_materialized
198    }
199
200    /// Return number of rows returned.
201    #[must_use]
202    pub const fn rows_returned(&self) -> u64 {
203        self.rows_returned
204    }
205
206    /// Return execution time in microseconds.
207    #[must_use]
208    pub const fn execution_time_micros(&self) -> u64 {
209        self.execution_time_micros
210    }
211
212    /// Return whether execution remained index-only.
213    #[must_use]
214    pub const fn index_only(&self) -> bool {
215        self.index_only
216    }
217
218    /// Return whether continuation was applied.
219    #[must_use]
220    pub const fn continuation_applied(&self) -> bool {
221        self.continuation_applied
222    }
223
224    /// Return whether index predicate pushdown was applied.
225    #[must_use]
226    pub const fn index_predicate_applied(&self) -> bool {
227        self.index_predicate_applied
228    }
229
230    /// Return number of keys rejected by index predicate pushdown.
231    #[must_use]
232    pub const fn index_predicate_keys_rejected(&self) -> u64 {
233        self.index_predicate_keys_rejected
234    }
235
236    /// Return number of deduplicated keys under DISTINCT processing.
237    #[must_use]
238    pub const fn distinct_keys_deduped(&self) -> u64 {
239        self.distinct_keys_deduped
240    }
241}
242
243impl ExecutionMetrics {
244    /// Return number of rows scanned.
245    #[must_use]
246    pub const fn rows_scanned(&self) -> u64 {
247        self.rows_scanned
248    }
249
250    /// Return number of rows materialized.
251    #[must_use]
252    pub const fn rows_materialized(&self) -> u64 {
253        self.rows_materialized
254    }
255
256    /// Return execution time in microseconds.
257    #[must_use]
258    pub const fn execution_time_micros(&self) -> u64 {
259        self.execution_time_micros
260    }
261
262    /// Return whether execution remained index-only.
263    #[must_use]
264    pub const fn index_only(&self) -> bool {
265        self.index_only
266    }
267}
268
269fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
270    match dispatch_access_plan(access) {
271        AccessPlanDispatch::Path(path) => match path.kind() {
272            AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
273            AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
274            AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
275            AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
276            AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
277            AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
278            AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
279        },
280        AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
281        AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
282    }
283}
284
285const fn execution_order_direction(direction: Direction) -> OrderDirection {
286    match direction {
287        Direction::Asc => OrderDirection::Asc,
288        Direction::Desc => OrderDirection::Desc,
289    }
290}
291
292///
293/// TESTS
294///
295
296#[cfg(test)]
297mod tests {
298    use crate::db::{
299        access::AccessPlan,
300        diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
301        direction::Direction,
302    };
303
304    #[test]
305    fn execution_trace_metrics_projection_exposes_requested_surface() {
306        let access = AccessPlan::by_key(11u64);
307        let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
308        trace.set_path_outcome(
309            Some(ExecutionOptimization::PrimaryKey),
310            5,
311            3,
312            2,
313            42,
314            true,
315            true,
316            7,
317            9,
318        );
319
320        let metrics = trace.metrics();
321        assert_eq!(
322            metrics,
323            ExecutionMetrics {
324                rows_scanned: 5,
325                rows_materialized: 3,
326                execution_time_micros: 42,
327                index_only: true,
328            },
329            "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
330        );
331        assert_eq!(
332            trace.rows_returned(),
333            2,
334            "trace should preserve returned-row counters independently from materialization counters",
335        );
336    }
337}