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