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