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 PrimaryKeyTopNSeek,
41 SecondaryOrderPushdown,
42 SecondaryOrderTopNSeek,
43 IndexRangeLimitPushdown,
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct ExecutionTrace {
55 pub(crate) access_path_variant: ExecutionAccessPathVariant,
56 pub(crate) direction: OrderDirection,
57 pub(crate) optimization: Option<ExecutionOptimization>,
58 pub(crate) keys_scanned: u64,
59 pub(crate) rows_materialized: u64,
60 pub(crate) rows_returned: u64,
61 pub(crate) execution_time_micros: u64,
62 pub(crate) index_only: bool,
63 pub(crate) continuation_applied: bool,
64 pub(crate) index_predicate_applied: bool,
65 pub(crate) index_predicate_keys_rejected: u64,
66 pub(crate) distinct_keys_deduped: u64,
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77pub struct ExecutionMetrics {
78 pub(crate) rows_scanned: u64,
79 pub(crate) rows_materialized: u64,
80 pub(crate) execution_time_micros: u64,
81 pub(crate) index_only: bool,
82}
83
84impl ExecutionTrace {
85 #[must_use]
87 pub(in crate::db) fn new<K>(
88 access: &AccessPlan<K>,
89 direction: Direction,
90 continuation_applied: bool,
91 ) -> Self {
92 Self {
93 access_path_variant: access_path_variant(access),
94 direction: execution_order_direction(direction),
95 optimization: None,
96 keys_scanned: 0,
97 rows_materialized: 0,
98 rows_returned: 0,
99 execution_time_micros: 0,
100 index_only: false,
101 continuation_applied,
102 index_predicate_applied: false,
103 index_predicate_keys_rejected: 0,
104 distinct_keys_deduped: 0,
105 }
106 }
107
108 #[expect(clippy::too_many_arguments)]
110 pub(in crate::db) fn set_path_outcome(
111 &mut self,
112 optimization: Option<ExecutionOptimization>,
113 keys_scanned: usize,
114 rows_materialized: usize,
115 rows_returned: usize,
116 execution_time_micros: u64,
117 index_only: bool,
118 index_predicate_applied: bool,
119 index_predicate_keys_rejected: u64,
120 distinct_keys_deduped: u64,
121 ) {
122 self.optimization = optimization;
123 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
124 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
125 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
126 self.execution_time_micros = execution_time_micros;
127 self.index_only = index_only;
128 self.index_predicate_applied = index_predicate_applied;
129 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
130 self.distinct_keys_deduped = distinct_keys_deduped;
131 debug_assert_eq!(
132 self.keys_scanned,
133 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
134 "execution trace keys_scanned must match rows_scanned metrics input",
135 );
136 }
137
138 #[must_use]
140 pub const fn metrics(&self) -> ExecutionMetrics {
141 ExecutionMetrics {
142 rows_scanned: self.keys_scanned,
143 rows_materialized: self.rows_materialized,
144 execution_time_micros: self.execution_time_micros,
145 index_only: self.index_only,
146 }
147 }
148
149 #[must_use]
151 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
152 self.access_path_variant
153 }
154
155 #[must_use]
157 pub const fn direction(&self) -> OrderDirection {
158 self.direction
159 }
160
161 #[must_use]
163 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
164 self.optimization
165 }
166
167 #[must_use]
169 pub const fn keys_scanned(&self) -> u64 {
170 self.keys_scanned
171 }
172
173 #[must_use]
175 pub const fn rows_materialized(&self) -> u64 {
176 self.rows_materialized
177 }
178
179 #[must_use]
181 pub const fn rows_returned(&self) -> u64 {
182 self.rows_returned
183 }
184
185 #[must_use]
187 pub const fn execution_time_micros(&self) -> u64 {
188 self.execution_time_micros
189 }
190
191 #[must_use]
193 pub const fn index_only(&self) -> bool {
194 self.index_only
195 }
196
197 #[must_use]
199 pub const fn continuation_applied(&self) -> bool {
200 self.continuation_applied
201 }
202
203 #[must_use]
205 pub const fn index_predicate_applied(&self) -> bool {
206 self.index_predicate_applied
207 }
208
209 #[must_use]
211 pub const fn index_predicate_keys_rejected(&self) -> u64 {
212 self.index_predicate_keys_rejected
213 }
214
215 #[must_use]
217 pub const fn distinct_keys_deduped(&self) -> u64 {
218 self.distinct_keys_deduped
219 }
220}
221
222impl ExecutionMetrics {
223 #[must_use]
225 pub const fn rows_scanned(&self) -> u64 {
226 self.rows_scanned
227 }
228
229 #[must_use]
231 pub const fn rows_materialized(&self) -> u64 {
232 self.rows_materialized
233 }
234
235 #[must_use]
237 pub const fn execution_time_micros(&self) -> u64 {
238 self.execution_time_micros
239 }
240
241 #[must_use]
243 pub const fn index_only(&self) -> bool {
244 self.index_only
245 }
246}
247
248fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
249 match dispatch_access_plan(access) {
250 AccessPlanDispatch::Path(path) => match path.kind() {
251 AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
252 AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
253 AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
254 AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
255 AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
256 AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
257 AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
258 },
259 AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
260 AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
261 }
262}
263
264const fn execution_order_direction(direction: Direction) -> OrderDirection {
265 match direction {
266 Direction::Asc => OrderDirection::Asc,
267 Direction::Desc => OrderDirection::Desc,
268 }
269}
270
271#[cfg(test)]
276mod tests {
277 use crate::db::{
278 access::AccessPlan,
279 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
280 direction::Direction,
281 };
282
283 #[test]
284 fn execution_trace_metrics_projection_exposes_requested_surface() {
285 let access = AccessPlan::by_key(11u64);
286 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
287 trace.set_path_outcome(
288 Some(ExecutionOptimization::PrimaryKey),
289 5,
290 3,
291 2,
292 42,
293 true,
294 true,
295 7,
296 9,
297 );
298
299 let metrics = trace.metrics();
300 assert_eq!(
301 metrics,
302 ExecutionMetrics {
303 rows_scanned: 5,
304 rows_materialized: 3,
305 execution_time_micros: 42,
306 index_only: true,
307 },
308 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
309 );
310 assert_eq!(
311 trace.rows_returned(),
312 2,
313 "trace should preserve returned-row counters independently from materialization counters",
314 );
315 }
316}