icydb_core/db/diagnostics/
execution_trace.rs1use crate::db::{
7 access::{AccessPath, AccessPlan},
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 access_path_variant: ExecutionAccessPathVariant,
54 pub direction: OrderDirection,
55 pub optimization: Option<ExecutionOptimization>,
56 pub keys_scanned: u64,
57 pub rows_materialized: u64,
58 pub rows_returned: u64,
59 pub execution_time_micros: u64,
60 pub index_only: bool,
61 pub continuation_applied: bool,
62 pub index_predicate_applied: bool,
63 pub index_predicate_keys_rejected: u64,
64 pub distinct_keys_deduped: u64,
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
75pub struct ExecutionMetrics {
76 pub rows_scanned: u64,
77 pub rows_materialized: u64,
78 pub execution_time_micros: u64,
79 pub 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
148fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
149 match access {
150 AccessPlan::Path(path) => match path.as_ref() {
151 AccessPath::ByKey(_) => ExecutionAccessPathVariant::ByKey,
152 AccessPath::ByKeys(_) => ExecutionAccessPathVariant::ByKeys,
153 AccessPath::KeyRange { .. } => ExecutionAccessPathVariant::KeyRange,
154 AccessPath::IndexPrefix { .. } => ExecutionAccessPathVariant::IndexPrefix,
155 AccessPath::IndexMultiLookup { .. } => ExecutionAccessPathVariant::IndexMultiLookup,
156 AccessPath::IndexRange { .. } => ExecutionAccessPathVariant::IndexRange,
157 AccessPath::FullScan => ExecutionAccessPathVariant::FullScan,
158 },
159 AccessPlan::Union(_) => ExecutionAccessPathVariant::Union,
160 AccessPlan::Intersection(_) => ExecutionAccessPathVariant::Intersection,
161 }
162}
163
164const fn execution_order_direction(direction: Direction) -> OrderDirection {
165 match direction {
166 Direction::Asc => OrderDirection::Asc,
167 Direction::Desc => OrderDirection::Desc,
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use crate::db::{
178 access::AccessPlan,
179 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
180 direction::Direction,
181 };
182
183 #[test]
184 fn execution_trace_metrics_projection_exposes_requested_surface() {
185 let access = AccessPlan::by_key(11u64);
186 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
187 trace.set_path_outcome(
188 Some(ExecutionOptimization::PrimaryKey),
189 5,
190 3,
191 2,
192 42,
193 true,
194 true,
195 7,
196 9,
197 );
198
199 let metrics = trace.metrics();
200 assert_eq!(
201 metrics,
202 ExecutionMetrics {
203 rows_scanned: 5,
204 rows_materialized: 3,
205 execution_time_micros: 42,
206 index_only: true,
207 },
208 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
209 );
210 assert_eq!(
211 trace.rows_returned, 2,
212 "trace should preserve returned-row counters independently from materialization counters",
213 );
214 }
215}