icydb_core/db/diagnostics/
execution_trace.rs1use crate::db::{
7 access::{AccessPathKind, AccessPlan, AccessPlanDispatch, dispatch_access_plan},
8 direction::Direction,
9 query::plan::OrderDirection,
10};
11
12#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum ExecutionOptimization {
39 PrimaryKey,
40 SecondaryOrderPushdown,
41 IndexRangeLimitPushdown,
42}
43
44#[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#[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 #[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 #[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 #[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 #[must_use]
149 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
150 self.access_path_variant
151 }
152
153 #[must_use]
155 pub const fn direction(&self) -> OrderDirection {
156 self.direction
157 }
158
159 #[must_use]
161 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
162 self.optimization
163 }
164
165 #[must_use]
167 pub const fn keys_scanned(&self) -> u64 {
168 self.keys_scanned
169 }
170
171 #[must_use]
173 pub const fn rows_materialized(&self) -> u64 {
174 self.rows_materialized
175 }
176
177 #[must_use]
179 pub const fn rows_returned(&self) -> u64 {
180 self.rows_returned
181 }
182
183 #[must_use]
185 pub const fn execution_time_micros(&self) -> u64 {
186 self.execution_time_micros
187 }
188
189 #[must_use]
191 pub const fn index_only(&self) -> bool {
192 self.index_only
193 }
194
195 #[must_use]
197 pub const fn continuation_applied(&self) -> bool {
198 self.continuation_applied
199 }
200
201 #[must_use]
203 pub const fn index_predicate_applied(&self) -> bool {
204 self.index_predicate_applied
205 }
206
207 #[must_use]
209 pub const fn index_predicate_keys_rejected(&self) -> u64 {
210 self.index_predicate_keys_rejected
211 }
212
213 #[must_use]
215 pub const fn distinct_keys_deduped(&self) -> u64 {
216 self.distinct_keys_deduped
217 }
218}
219
220impl ExecutionMetrics {
221 #[must_use]
223 pub const fn rows_scanned(&self) -> u64 {
224 self.rows_scanned
225 }
226
227 #[must_use]
229 pub const fn rows_materialized(&self) -> u64 {
230 self.rows_materialized
231 }
232
233 #[must_use]
235 pub const fn execution_time_micros(&self) -> u64 {
236 self.execution_time_micros
237 }
238
239 #[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#[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}