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#[cfg(test)]
54#[expect(clippy::enum_variant_names)]
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub(crate) enum ExecutionOptimizationCounter {
57 BytesPrimaryKeyFastPath,
58 BytesStreamFastPath,
59 CoveringExistsFastPath,
60 CoveringCountFastPath,
61 PrimaryKeyCountFastPath,
62 PrimaryKeyCardinalityCountFastPath,
63 CoveringIndexProjectionFastPath,
64 CoveringConstantProjectionFastPath,
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct ExecutionTrace {
76 pub(crate) access_path_variant: ExecutionAccessPathVariant,
77 pub(crate) direction: OrderDirection,
78 pub(crate) optimization: Option<ExecutionOptimization>,
79 pub(crate) keys_scanned: u64,
80 pub(crate) rows_materialized: u64,
81 pub(crate) rows_returned: u64,
82 pub(crate) execution_time_micros: u64,
83 pub(crate) index_only: bool,
84 pub(crate) continuation_applied: bool,
85 pub(crate) index_predicate_applied: bool,
86 pub(crate) index_predicate_keys_rejected: u64,
87 pub(crate) distinct_keys_deduped: u64,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct ExecutionMetrics {
99 pub(crate) rows_scanned: u64,
100 pub(crate) rows_materialized: u64,
101 pub(crate) execution_time_micros: u64,
102 pub(crate) index_only: bool,
103}
104
105impl ExecutionTrace {
106 #[must_use]
108 pub(in crate::db) fn new<K>(
109 access: &AccessPlan<K>,
110 direction: Direction,
111 continuation_applied: bool,
112 ) -> Self {
113 Self {
114 access_path_variant: access_path_variant(access),
115 direction: execution_order_direction(direction),
116 optimization: None,
117 keys_scanned: 0,
118 rows_materialized: 0,
119 rows_returned: 0,
120 execution_time_micros: 0,
121 index_only: false,
122 continuation_applied,
123 index_predicate_applied: false,
124 index_predicate_keys_rejected: 0,
125 distinct_keys_deduped: 0,
126 }
127 }
128
129 #[expect(clippy::too_many_arguments)]
131 pub(in crate::db) fn set_path_outcome(
132 &mut self,
133 optimization: Option<ExecutionOptimization>,
134 keys_scanned: usize,
135 rows_materialized: usize,
136 rows_returned: usize,
137 execution_time_micros: u64,
138 index_only: bool,
139 index_predicate_applied: bool,
140 index_predicate_keys_rejected: u64,
141 distinct_keys_deduped: u64,
142 ) {
143 self.optimization = optimization;
144 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
145 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
146 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
147 self.execution_time_micros = execution_time_micros;
148 self.index_only = index_only;
149 self.index_predicate_applied = index_predicate_applied;
150 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
151 self.distinct_keys_deduped = distinct_keys_deduped;
152 debug_assert_eq!(
153 self.keys_scanned,
154 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
155 "execution trace keys_scanned must match rows_scanned metrics input",
156 );
157 }
158
159 #[must_use]
161 pub const fn metrics(&self) -> ExecutionMetrics {
162 ExecutionMetrics {
163 rows_scanned: self.keys_scanned,
164 rows_materialized: self.rows_materialized,
165 execution_time_micros: self.execution_time_micros,
166 index_only: self.index_only,
167 }
168 }
169
170 #[must_use]
172 pub const fn access_path_variant(&self) -> ExecutionAccessPathVariant {
173 self.access_path_variant
174 }
175
176 #[must_use]
178 pub const fn direction(&self) -> OrderDirection {
179 self.direction
180 }
181
182 #[must_use]
184 pub const fn optimization(&self) -> Option<ExecutionOptimization> {
185 self.optimization
186 }
187
188 #[must_use]
190 pub const fn keys_scanned(&self) -> u64 {
191 self.keys_scanned
192 }
193
194 #[must_use]
196 pub const fn rows_materialized(&self) -> u64 {
197 self.rows_materialized
198 }
199
200 #[must_use]
202 pub const fn rows_returned(&self) -> u64 {
203 self.rows_returned
204 }
205
206 #[must_use]
208 pub const fn execution_time_micros(&self) -> u64 {
209 self.execution_time_micros
210 }
211
212 #[must_use]
214 pub const fn index_only(&self) -> bool {
215 self.index_only
216 }
217
218 #[must_use]
220 pub const fn continuation_applied(&self) -> bool {
221 self.continuation_applied
222 }
223
224 #[must_use]
226 pub const fn index_predicate_applied(&self) -> bool {
227 self.index_predicate_applied
228 }
229
230 #[must_use]
232 pub const fn index_predicate_keys_rejected(&self) -> u64 {
233 self.index_predicate_keys_rejected
234 }
235
236 #[must_use]
238 pub const fn distinct_keys_deduped(&self) -> u64 {
239 self.distinct_keys_deduped
240 }
241}
242
243impl ExecutionMetrics {
244 #[must_use]
246 pub const fn rows_scanned(&self) -> u64 {
247 self.rows_scanned
248 }
249
250 #[must_use]
252 pub const fn rows_materialized(&self) -> u64 {
253 self.rows_materialized
254 }
255
256 #[must_use]
258 pub const fn execution_time_micros(&self) -> u64 {
259 self.execution_time_micros
260 }
261
262 #[must_use]
264 pub const fn index_only(&self) -> bool {
265 self.index_only
266 }
267}
268
269fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
270 match dispatch_access_plan(access) {
271 AccessPlanDispatch::Path(path) => match path.kind() {
272 AccessPathKind::ByKey => ExecutionAccessPathVariant::ByKey,
273 AccessPathKind::ByKeys => ExecutionAccessPathVariant::ByKeys,
274 AccessPathKind::KeyRange => ExecutionAccessPathVariant::KeyRange,
275 AccessPathKind::IndexPrefix => ExecutionAccessPathVariant::IndexPrefix,
276 AccessPathKind::IndexMultiLookup => ExecutionAccessPathVariant::IndexMultiLookup,
277 AccessPathKind::IndexRange => ExecutionAccessPathVariant::IndexRange,
278 AccessPathKind::FullScan => ExecutionAccessPathVariant::FullScan,
279 },
280 AccessPlanDispatch::Union(_) => ExecutionAccessPathVariant::Union,
281 AccessPlanDispatch::Intersection(_) => ExecutionAccessPathVariant::Intersection,
282 }
283}
284
285const fn execution_order_direction(direction: Direction) -> OrderDirection {
286 match direction {
287 Direction::Asc => OrderDirection::Asc,
288 Direction::Desc => OrderDirection::Desc,
289 }
290}
291
292#[cfg(test)]
297mod tests {
298 use crate::db::{
299 access::AccessPlan,
300 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
301 direction::Direction,
302 };
303
304 #[test]
305 fn execution_trace_metrics_projection_exposes_requested_surface() {
306 let access = AccessPlan::by_key(11u64);
307 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
308 trace.set_path_outcome(
309 Some(ExecutionOptimization::PrimaryKey),
310 5,
311 3,
312 2,
313 42,
314 true,
315 true,
316 7,
317 9,
318 );
319
320 let metrics = trace.metrics();
321 assert_eq!(
322 metrics,
323 ExecutionMetrics {
324 rows_scanned: 5,
325 rows_materialized: 3,
326 execution_time_micros: 42,
327 index_only: true,
328 },
329 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
330 );
331 assert_eq!(
332 trace.rows_returned(),
333 2,
334 "trace should preserve returned-row counters independently from materialization counters",
335 );
336 }
337}