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