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::{AccessPath, AccessPlan},
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    IndexRange,
25    FullScan,
26    Union,
27    Intersection,
28}
29
30///
31/// ExecutionOptimization
32///
33/// Canonical load optimization selected by execution, if any.
34///
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ExecutionOptimization {
38    PrimaryKey,
39    SecondaryOrderPushdown,
40    IndexRangeLimitPushdown,
41}
42
43///
44/// ExecutionTrace
45///
46/// Structured, opt-in load execution introspection snapshot.
47/// Captures plan-shape and execution decisions without changing semantics.
48///
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ExecutionTrace {
52    pub access_path_variant: ExecutionAccessPathVariant,
53    pub direction: OrderDirection,
54    pub optimization: Option<ExecutionOptimization>,
55    pub keys_scanned: u64,
56    pub rows_materialized: u64,
57    pub rows_returned: u64,
58    pub execution_time_micros: u64,
59    pub index_only: bool,
60    pub continuation_applied: bool,
61    pub index_predicate_applied: bool,
62    pub index_predicate_keys_rejected: u64,
63    pub distinct_keys_deduped: u64,
64}
65
66///
67/// ExecutionMetrics
68///
69/// Compact execution metrics projection derived from one `ExecutionTrace`.
70/// This surface is intentionally small and stable for pre-EXPLAIN observability.
71///
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct ExecutionMetrics {
75    pub rows_scanned: u64,
76    pub rows_materialized: u64,
77    pub execution_time_micros: u64,
78    pub index_only: bool,
79}
80
81impl ExecutionTrace {
82    /// Build one trace payload from canonical access shape and runtime direction.
83    #[must_use]
84    pub(in crate::db) fn new<K>(
85        access: &AccessPlan<K>,
86        direction: Direction,
87        continuation_applied: bool,
88    ) -> Self {
89        Self {
90            access_path_variant: access_path_variant(access),
91            direction: execution_order_direction(direction),
92            optimization: None,
93            keys_scanned: 0,
94            rows_materialized: 0,
95            rows_returned: 0,
96            execution_time_micros: 0,
97            index_only: false,
98            continuation_applied,
99            index_predicate_applied: false,
100            index_predicate_keys_rejected: 0,
101            distinct_keys_deduped: 0,
102        }
103    }
104
105    /// Apply one finalized path outcome to this trace snapshot.
106    #[expect(clippy::too_many_arguments)]
107    pub(in crate::db) fn set_path_outcome(
108        &mut self,
109        optimization: Option<ExecutionOptimization>,
110        keys_scanned: usize,
111        rows_materialized: usize,
112        rows_returned: usize,
113        execution_time_micros: u64,
114        index_only: bool,
115        index_predicate_applied: bool,
116        index_predicate_keys_rejected: u64,
117        distinct_keys_deduped: u64,
118    ) {
119        self.optimization = optimization;
120        self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
121        self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
122        self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
123        self.execution_time_micros = execution_time_micros;
124        self.index_only = index_only;
125        self.index_predicate_applied = index_predicate_applied;
126        self.index_predicate_keys_rejected = index_predicate_keys_rejected;
127        self.distinct_keys_deduped = distinct_keys_deduped;
128        debug_assert_eq!(
129            self.keys_scanned,
130            u64::try_from(keys_scanned).unwrap_or(u64::MAX),
131            "execution trace keys_scanned must match rows_scanned metrics input",
132        );
133    }
134
135    /// Return compact execution metrics for pre-EXPLAIN observability surfaces.
136    #[must_use]
137    pub const fn metrics(&self) -> ExecutionMetrics {
138        ExecutionMetrics {
139            rows_scanned: self.keys_scanned,
140            rows_materialized: self.rows_materialized,
141            execution_time_micros: self.execution_time_micros,
142            index_only: self.index_only,
143        }
144    }
145}
146
147fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
148    match access {
149        AccessPlan::Path(path) => match path.as_ref() {
150            AccessPath::ByKey(_) => ExecutionAccessPathVariant::ByKey,
151            AccessPath::ByKeys(_) => ExecutionAccessPathVariant::ByKeys,
152            AccessPath::KeyRange { .. } => ExecutionAccessPathVariant::KeyRange,
153            AccessPath::IndexPrefix { .. } => ExecutionAccessPathVariant::IndexPrefix,
154            AccessPath::IndexRange { .. } => ExecutionAccessPathVariant::IndexRange,
155            AccessPath::FullScan => ExecutionAccessPathVariant::FullScan,
156        },
157        AccessPlan::Union(_) => ExecutionAccessPathVariant::Union,
158        AccessPlan::Intersection(_) => ExecutionAccessPathVariant::Intersection,
159    }
160}
161
162const fn execution_order_direction(direction: Direction) -> OrderDirection {
163    match direction {
164        Direction::Asc => OrderDirection::Asc,
165        Direction::Desc => OrderDirection::Desc,
166    }
167}
168
169///
170/// TESTS
171///
172
173#[cfg(test)]
174mod tests {
175    use crate::db::{
176        access::AccessPlan,
177        diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
178        direction::Direction,
179    };
180
181    #[test]
182    fn execution_trace_metrics_projection_exposes_requested_surface() {
183        let access = AccessPlan::by_key(11u64);
184        let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
185        trace.set_path_outcome(
186            Some(ExecutionOptimization::PrimaryKey),
187            5,
188            3,
189            2,
190            42,
191            true,
192            true,
193            7,
194            9,
195        );
196
197        let metrics = trace.metrics();
198        assert_eq!(
199            metrics,
200            ExecutionMetrics {
201                rows_scanned: 5,
202                rows_materialized: 3,
203                execution_time_micros: 42,
204                index_only: true,
205            },
206            "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
207        );
208        assert_eq!(
209            trace.rows_returned, 2,
210            "trace should preserve returned-row counters independently from materialization counters",
211        );
212    }
213}