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