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    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 access_path_variant: ExecutionAccessPathVariant,
54    pub direction: OrderDirection,
55    pub optimization: Option<ExecutionOptimization>,
56    pub keys_scanned: u64,
57    pub rows_materialized: u64,
58    pub rows_returned: u64,
59    pub execution_time_micros: u64,
60    pub index_only: bool,
61    pub continuation_applied: bool,
62    pub index_predicate_applied: bool,
63    pub index_predicate_keys_rejected: u64,
64    pub 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 rows_scanned: u64,
77    pub rows_materialized: u64,
78    pub execution_time_micros: u64,
79    pub 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
148fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
149    match access {
150        AccessPlan::Path(path) => match path.as_ref() {
151            AccessPath::ByKey(_) => ExecutionAccessPathVariant::ByKey,
152            AccessPath::ByKeys(_) => ExecutionAccessPathVariant::ByKeys,
153            AccessPath::KeyRange { .. } => ExecutionAccessPathVariant::KeyRange,
154            AccessPath::IndexPrefix { .. } => ExecutionAccessPathVariant::IndexPrefix,
155            AccessPath::IndexMultiLookup { .. } => ExecutionAccessPathVariant::IndexMultiLookup,
156            AccessPath::IndexRange { .. } => ExecutionAccessPathVariant::IndexRange,
157            AccessPath::FullScan => ExecutionAccessPathVariant::FullScan,
158        },
159        AccessPlan::Union(_) => ExecutionAccessPathVariant::Union,
160        AccessPlan::Intersection(_) => ExecutionAccessPathVariant::Intersection,
161    }
162}
163
164const fn execution_order_direction(direction: Direction) -> OrderDirection {
165    match direction {
166        Direction::Asc => OrderDirection::Asc,
167        Direction::Desc => OrderDirection::Desc,
168    }
169}
170
171///
172/// TESTS
173///
174
175#[cfg(test)]
176mod tests {
177    use crate::db::{
178        access::AccessPlan,
179        diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
180        direction::Direction,
181    };
182
183    #[test]
184    fn execution_trace_metrics_projection_exposes_requested_surface() {
185        let access = AccessPlan::by_key(11u64);
186        let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
187        trace.set_path_outcome(
188            Some(ExecutionOptimization::PrimaryKey),
189            5,
190            3,
191            2,
192            42,
193            true,
194            true,
195            7,
196            9,
197        );
198
199        let metrics = trace.metrics();
200        assert_eq!(
201            metrics,
202            ExecutionMetrics {
203                rows_scanned: 5,
204                rows_materialized: 3,
205                execution_time_micros: 42,
206                index_only: true,
207            },
208            "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
209        );
210        assert_eq!(
211            trace.rows_returned, 2,
212            "trace should preserve returned-row counters independently from materialization counters",
213        );
214    }
215}