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