icydb_core/db/diagnostics/
execution_trace.rs1use crate::db::query::plan::OrderDirection;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum ExecutionOptimization {
18 PrimaryKey,
19 PrimaryKeyTopNSeek,
20 SecondaryOrderPushdown,
21 SecondaryOrderTopNSeek,
22 IndexRangeLimitPushdown,
23}
24
25#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
34pub struct ExecutionStats {
35 rows_scanned_pre_filter: u64,
36 rows_after_predicate: u64,
37 rows_after_projection: u64,
38 rows_after_distinct: u64,
39 rows_sorted: u64,
40 keys_streamed: u64,
41 key_stream_micros: u64,
42 ordering_micros: u64,
43 projection_micros: u64,
44 aggregation_micros: u64,
45}
46
47impl ExecutionStats {
48 #[must_use]
50 #[expect(
51 clippy::too_many_arguments,
52 reason = "diagnostics stats DTO exposes the exact flat trace counter surface"
53 )]
54 pub(in crate::db) const fn new(
55 rows_scanned_pre_filter: u64,
56 rows_after_predicate: u64,
57 rows_after_projection: u64,
58 rows_after_distinct: u64,
59 rows_sorted: u64,
60 keys_streamed: u64,
61 key_stream_micros: u64,
62 ordering_micros: u64,
63 projection_micros: u64,
64 aggregation_micros: u64,
65 ) -> Self {
66 Self {
67 rows_scanned_pre_filter,
68 rows_after_predicate,
69 rows_after_projection,
70 rows_after_distinct,
71 rows_sorted,
72 keys_streamed,
73 key_stream_micros,
74 ordering_micros,
75 projection_micros,
76 aggregation_micros,
77 }
78 }
79
80 #[must_use]
82 #[cfg_attr(
83 not(test),
84 expect(
85 dead_code,
86 reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
87 )
88 )]
89 pub(in crate::db) const fn rows_scanned_pre_filter(&self) -> u64 {
90 self.rows_scanned_pre_filter
91 }
92
93 #[must_use]
95 #[cfg_attr(
96 not(test),
97 expect(
98 dead_code,
99 reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
100 )
101 )]
102 pub(in crate::db) const fn rows_after_predicate(&self) -> u64 {
103 self.rows_after_predicate
104 }
105
106 #[must_use]
108 #[cfg_attr(
109 not(test),
110 expect(
111 dead_code,
112 reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
113 )
114 )]
115 pub(in crate::db) const fn rows_after_projection(&self) -> u64 {
116 self.rows_after_projection
117 }
118
119 #[must_use]
121 #[expect(
122 dead_code,
123 reason = "execution profiling records this for diagnostics consumers before response exposure"
124 )]
125 pub(in crate::db) const fn rows_after_distinct(&self) -> u64 {
126 self.rows_after_distinct
127 }
128
129 #[must_use]
131 #[expect(
132 dead_code,
133 reason = "execution profiling records this for diagnostics consumers before response exposure"
134 )]
135 pub(in crate::db) const fn rows_sorted(&self) -> u64 {
136 self.rows_sorted
137 }
138
139 #[must_use]
141 #[cfg_attr(
142 not(test),
143 expect(
144 dead_code,
145 reason = "execution profiling accessors are consumed by crate tests and diagnostics tooling"
146 )
147 )]
148 pub(in crate::db) const fn keys_streamed(&self) -> u64 {
149 self.keys_streamed
150 }
151
152 #[must_use]
154 #[expect(
155 dead_code,
156 reason = "execution profiling records this for diagnostics consumers before response exposure"
157 )]
158 pub(in crate::db) const fn key_stream_micros(&self) -> u64 {
159 self.key_stream_micros
160 }
161
162 #[must_use]
164 #[expect(
165 dead_code,
166 reason = "execution profiling records this for diagnostics consumers before response exposure"
167 )]
168 pub(in crate::db) const fn ordering_micros(&self) -> u64 {
169 self.ordering_micros
170 }
171
172 #[must_use]
174 #[expect(
175 dead_code,
176 reason = "execution profiling records this for diagnostics consumers before response exposure"
177 )]
178 pub(in crate::db) const fn projection_micros(&self) -> u64 {
179 self.projection_micros
180 }
181
182 #[must_use]
184 #[expect(
185 dead_code,
186 reason = "execution profiling records this for diagnostics consumers before response exposure"
187 )]
188 pub(in crate::db) const fn aggregation_micros(&self) -> u64 {
189 self.aggregation_micros
190 }
191}
192
193#[cfg_attr(
194 doc,
195 doc = "ExecutionAccessPathVariant\n\nCoarse access path shape recorded in execution traces."
196)]
197#[derive(Clone, Copy, Debug, Eq, PartialEq)]
198pub enum ExecutionAccessPathVariant {
199 ByKey,
200 ByKeys,
201 KeyRange,
202 IndexPrefix,
203 IndexMultiLookup,
204 IndexRange,
205 FullScan,
206 Union,
207 Intersection,
208}
209
210#[cfg_attr(
211 doc,
212 doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
213)]
214#[derive(Clone, Copy, Debug, Eq, PartialEq)]
215pub struct ExecutionTrace {
216 pub(crate) access_path_variant: ExecutionAccessPathVariant,
217 pub(crate) direction: OrderDirection,
218 pub(crate) optimization: Option<ExecutionOptimization>,
219 pub(crate) keys_scanned: u64,
220 pub(crate) rows_materialized: u64,
221 pub(crate) rows_returned: u64,
222 pub(crate) execution_time_micros: u64,
223 pub(crate) index_only: bool,
224 pub(crate) continuation_applied: bool,
225 pub(crate) index_predicate_applied: bool,
226 pub(crate) index_predicate_keys_rejected: u64,
227 pub(crate) distinct_keys_deduped: u64,
228 pub(crate) execution_stats: Option<ExecutionStats>,
229}
230
231#[cfg_attr(
232 doc,
233 doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
234)]
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub struct ExecutionMetrics {
237 pub(crate) rows_scanned: u64,
238 pub(crate) rows_materialized: u64,
239 pub(crate) execution_time_micros: u64,
240 pub(crate) index_only: bool,
241}
242
243impl ExecutionTrace {
244 #[must_use]
246 pub(crate) const fn new_from_variant(
247 access_path_variant: ExecutionAccessPathVariant,
248 direction: OrderDirection,
249 continuation_applied: bool,
250 ) -> Self {
251 Self {
252 access_path_variant,
253 direction,
254 optimization: None,
255 keys_scanned: 0,
256 rows_materialized: 0,
257 rows_returned: 0,
258 execution_time_micros: 0,
259 index_only: false,
260 continuation_applied,
261 index_predicate_applied: false,
262 index_predicate_keys_rejected: 0,
263 distinct_keys_deduped: 0,
264 execution_stats: None,
265 }
266 }
267
268 #[expect(clippy::too_many_arguments)]
270 pub(in crate::db) fn set_path_outcome(
271 &mut self,
272 optimization: Option<ExecutionOptimization>,
273 keys_scanned: usize,
274 rows_materialized: usize,
275 rows_returned: usize,
276 execution_time_micros: u64,
277 index_only: bool,
278 index_predicate_applied: bool,
279 index_predicate_keys_rejected: u64,
280 distinct_keys_deduped: u64,
281 ) {
282 self.optimization = optimization;
283 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
284 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
285 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
286 self.execution_time_micros = execution_time_micros;
287 self.index_only = index_only;
288 self.index_predicate_applied = index_predicate_applied;
289 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
290 self.distinct_keys_deduped = distinct_keys_deduped;
291 debug_assert_eq!(
292 self.keys_scanned,
293 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
294 "execution trace keys_scanned must match rows_scanned metrics input",
295 );
296 }
297
298 pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
300 self.execution_stats = stats;
301 }
302
303 #[must_use]
305 pub const fn metrics(&self) -> ExecutionMetrics {
306 ExecutionMetrics {
307 rows_scanned: self.keys_scanned,
308 rows_materialized: self.rows_materialized,
309 execution_time_micros: self.execution_time_micros,
310 index_only: self.index_only,
311 }
312 }
313
314 #[must_use]
316 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
317 self.access_path_variant
318 }
319
320 #[must_use]
322 pub const fn direction(&self) -> OrderDirection {
323 self.direction
324 }
325
326 #[must_use]
328 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
329 self.optimization
330 }
331
332 #[must_use]
334 pub const fn keys_scanned(&self) -> u64 {
335 self.keys_scanned
336 }
337
338 #[must_use]
340 pub const fn rows_materialized(&self) -> u64 {
341 self.rows_materialized
342 }
343
344 #[must_use]
346 pub const fn rows_returned(&self) -> u64 {
347 self.rows_returned
348 }
349
350 #[must_use]
352 pub const fn execution_time_micros(&self) -> u64 {
353 self.execution_time_micros
354 }
355
356 #[must_use]
358 pub const fn index_only(&self) -> bool {
359 self.index_only
360 }
361
362 #[must_use]
364 pub const fn continuation_applied(&self) -> bool {
365 self.continuation_applied
366 }
367
368 #[must_use]
370 pub const fn index_predicate_applied(&self) -> bool {
371 self.index_predicate_applied
372 }
373
374 #[must_use]
376 pub const fn index_predicate_keys_rejected(&self) -> u64 {
377 self.index_predicate_keys_rejected
378 }
379
380 #[must_use]
382 pub const fn distinct_keys_deduped(&self) -> u64 {
383 self.distinct_keys_deduped
384 }
385
386 #[must_use]
388 #[cfg_attr(
389 not(test),
390 expect(
391 dead_code,
392 reason = "execution stats are an internal diagnostics/testing surface"
393 )
394 )]
395 pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
396 self.execution_stats
397 }
398}
399
400impl ExecutionMetrics {
401 #[must_use]
403 pub const fn rows_scanned(&self) -> u64 {
404 self.rows_scanned
405 }
406
407 #[must_use]
409 pub const fn rows_materialized(&self) -> u64 {
410 self.rows_materialized
411 }
412
413 #[must_use]
415 pub const fn execution_time_micros(&self) -> u64 {
416 self.execution_time_micros
417 }
418
419 #[must_use]
421 pub const fn index_only(&self) -> bool {
422 self.index_only
423 }
424}