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