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 FullScan,
202 IndexBranchSet,
203 IndexMultiLookup,
204 IndexPrefix,
205 IndexRange,
206 KeyRange,
207 Union,
208 Intersection,
209}
210
211#[cfg_attr(
212 doc,
213 doc = "ExecutionTrace\n\nStructured execution trace snapshot for one load path.\nCaptures plan shape and counters without affecting behavior."
214)]
215#[derive(Clone, Copy, Debug, Eq, PartialEq)]
216pub struct ExecutionTrace {
217 pub(crate) access_path_variant: ExecutionAccessPathVariant,
218 pub(crate) direction: OrderDirection,
219 pub(crate) optimization: Option<ExecutionOptimization>,
220 pub(crate) keys_scanned: u64,
221 pub(crate) rows_materialized: u64,
222 pub(crate) rows_returned: u64,
223 pub(crate) execution_time_micros: u64,
224 pub(crate) index_only: bool,
225 pub(crate) continuation_applied: bool,
226 pub(crate) index_predicate_applied: bool,
227 pub(crate) index_predicate_keys_rejected: u64,
228 pub(crate) distinct_keys_deduped: u64,
229 pub(crate) execution_stats: Option<ExecutionStats>,
230}
231
232#[cfg_attr(
233 doc,
234 doc = "ExecutionMetrics\n\nCompact metrics view derived from one `ExecutionTrace`.\nKept small for lightweight observability surfaces."
235)]
236#[derive(Clone, Copy, Debug, Eq, PartialEq)]
237pub struct ExecutionMetrics {
238 pub(crate) rows_scanned: u64,
239 pub(crate) rows_materialized: u64,
240 pub(crate) execution_time_micros: u64,
241 pub(crate) index_only: bool,
242}
243
244impl ExecutionTrace {
245 #[must_use]
247 pub(crate) const fn new_from_variant(
248 access_path_variant: ExecutionAccessPathVariant,
249 direction: OrderDirection,
250 continuation_applied: bool,
251 ) -> Self {
252 Self {
253 access_path_variant,
254 direction,
255 optimization: None,
256 keys_scanned: 0,
257 rows_materialized: 0,
258 rows_returned: 0,
259 execution_time_micros: 0,
260 index_only: false,
261 continuation_applied,
262 index_predicate_applied: false,
263 index_predicate_keys_rejected: 0,
264 distinct_keys_deduped: 0,
265 execution_stats: None,
266 }
267 }
268
269 #[expect(clippy::too_many_arguments)]
271 pub(in crate::db) fn set_path_outcome(
272 &mut self,
273 optimization: Option<ExecutionOptimization>,
274 keys_scanned: usize,
275 rows_materialized: usize,
276 rows_returned: usize,
277 execution_time_micros: u64,
278 index_only: bool,
279 index_predicate_applied: bool,
280 index_predicate_keys_rejected: u64,
281 distinct_keys_deduped: u64,
282 ) {
283 self.optimization = optimization;
284 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
285 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
286 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
287 self.execution_time_micros = execution_time_micros;
288 self.index_only = index_only;
289 self.index_predicate_applied = index_predicate_applied;
290 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
291 self.distinct_keys_deduped = distinct_keys_deduped;
292 debug_assert_eq!(
293 self.keys_scanned,
294 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
295 "execution trace keys_scanned must match rows_scanned metrics input",
296 );
297 }
298
299 pub(in crate::db) const fn set_execution_stats(&mut self, stats: Option<ExecutionStats>) {
301 self.execution_stats = stats;
302 }
303
304 #[must_use]
306 pub const fn metrics(&self) -> ExecutionMetrics {
307 ExecutionMetrics {
308 rows_scanned: self.keys_scanned,
309 rows_materialized: self.rows_materialized,
310 execution_time_micros: self.execution_time_micros,
311 index_only: self.index_only,
312 }
313 }
314
315 #[must_use]
317 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
318 self.access_path_variant
319 }
320
321 #[must_use]
323 pub const fn direction(&self) -> OrderDirection {
324 self.direction
325 }
326
327 #[must_use]
329 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
330 self.optimization
331 }
332
333 #[must_use]
335 pub const fn keys_scanned(&self) -> u64 {
336 self.keys_scanned
337 }
338
339 #[must_use]
341 pub const fn rows_materialized(&self) -> u64 {
342 self.rows_materialized
343 }
344
345 #[must_use]
347 pub const fn rows_returned(&self) -> u64 {
348 self.rows_returned
349 }
350
351 #[must_use]
353 pub const fn execution_time_micros(&self) -> u64 {
354 self.execution_time_micros
355 }
356
357 #[must_use]
359 pub const fn index_only(&self) -> bool {
360 self.index_only
361 }
362
363 #[must_use]
365 pub const fn continuation_applied(&self) -> bool {
366 self.continuation_applied
367 }
368
369 #[must_use]
371 pub const fn index_predicate_applied(&self) -> bool {
372 self.index_predicate_applied
373 }
374
375 #[must_use]
377 pub const fn index_predicate_keys_rejected(&self) -> u64 {
378 self.index_predicate_keys_rejected
379 }
380
381 #[must_use]
383 pub const fn distinct_keys_deduped(&self) -> u64 {
384 self.distinct_keys_deduped
385 }
386
387 #[must_use]
389 #[cfg_attr(
390 not(test),
391 expect(
392 dead_code,
393 reason = "execution stats are an internal diagnostics/testing surface"
394 )
395 )]
396 pub(in crate::db) const fn execution_stats(&self) -> Option<ExecutionStats> {
397 self.execution_stats
398 }
399}
400
401impl ExecutionMetrics {
402 #[must_use]
404 pub const fn rows_scanned(&self) -> u64 {
405 self.rows_scanned
406 }
407
408 #[must_use]
410 pub const fn rows_materialized(&self) -> u64 {
411 self.rows_materialized
412 }
413
414 #[must_use]
416 pub const fn execution_time_micros(&self) -> u64 {
417 self.execution_time_micros
418 }
419
420 #[must_use]
422 pub const fn index_only(&self) -> bool {
423 self.index_only
424 }
425}