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 IndexRange,
25 FullScan,
26 Union,
27 Intersection,
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum ExecutionOptimization {
38 PrimaryKey,
39 SecondaryOrderPushdown,
40 IndexRangeLimitPushdown,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ExecutionTrace {
52 pub access_path_variant: ExecutionAccessPathVariant,
53 pub direction: OrderDirection,
54 pub optimization: Option<ExecutionOptimization>,
55 pub keys_scanned: u64,
56 pub rows_materialized: u64,
57 pub rows_returned: u64,
58 pub execution_time_micros: u64,
59 pub index_only: bool,
60 pub continuation_applied: bool,
61 pub index_predicate_applied: bool,
62 pub index_predicate_keys_rejected: u64,
63 pub distinct_keys_deduped: u64,
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct ExecutionMetrics {
75 pub rows_scanned: u64,
76 pub rows_materialized: u64,
77 pub execution_time_micros: u64,
78 pub index_only: bool,
79}
80
81impl ExecutionTrace {
82 #[must_use]
84 pub(in crate::db) fn new<K>(
85 access: &AccessPlan<K>,
86 direction: Direction,
87 continuation_applied: bool,
88 ) -> Self {
89 Self {
90 access_path_variant: access_path_variant(access),
91 direction: execution_order_direction(direction),
92 optimization: None,
93 keys_scanned: 0,
94 rows_materialized: 0,
95 rows_returned: 0,
96 execution_time_micros: 0,
97 index_only: false,
98 continuation_applied,
99 index_predicate_applied: false,
100 index_predicate_keys_rejected: 0,
101 distinct_keys_deduped: 0,
102 }
103 }
104
105 #[expect(clippy::too_many_arguments)]
107 pub(in crate::db) fn set_path_outcome(
108 &mut self,
109 optimization: Option<ExecutionOptimization>,
110 keys_scanned: usize,
111 rows_materialized: usize,
112 rows_returned: usize,
113 execution_time_micros: u64,
114 index_only: bool,
115 index_predicate_applied: bool,
116 index_predicate_keys_rejected: u64,
117 distinct_keys_deduped: u64,
118 ) {
119 self.optimization = optimization;
120 self.keys_scanned = u64::try_from(keys_scanned).unwrap_or(u64::MAX);
121 self.rows_materialized = u64::try_from(rows_materialized).unwrap_or(u64::MAX);
122 self.rows_returned = u64::try_from(rows_returned).unwrap_or(u64::MAX);
123 self.execution_time_micros = execution_time_micros;
124 self.index_only = index_only;
125 self.index_predicate_applied = index_predicate_applied;
126 self.index_predicate_keys_rejected = index_predicate_keys_rejected;
127 self.distinct_keys_deduped = distinct_keys_deduped;
128 debug_assert_eq!(
129 self.keys_scanned,
130 u64::try_from(keys_scanned).unwrap_or(u64::MAX),
131 "execution trace keys_scanned must match rows_scanned metrics input",
132 );
133 }
134
135 #[must_use]
137 pub const fn metrics(&self) -> ExecutionMetrics {
138 ExecutionMetrics {
139 rows_scanned: self.keys_scanned,
140 rows_materialized: self.rows_materialized,
141 execution_time_micros: self.execution_time_micros,
142 index_only: self.index_only,
143 }
144 }
145}
146
147fn access_path_variant<K>(access: &AccessPlan<K>) -> ExecutionAccessPathVariant {
148 match access {
149 AccessPlan::Path(path) => match path.as_ref() {
150 AccessPath::ByKey(_) => ExecutionAccessPathVariant::ByKey,
151 AccessPath::ByKeys(_) => ExecutionAccessPathVariant::ByKeys,
152 AccessPath::KeyRange { .. } => ExecutionAccessPathVariant::KeyRange,
153 AccessPath::IndexPrefix { .. } => ExecutionAccessPathVariant::IndexPrefix,
154 AccessPath::IndexRange { .. } => ExecutionAccessPathVariant::IndexRange,
155 AccessPath::FullScan => ExecutionAccessPathVariant::FullScan,
156 },
157 AccessPlan::Union(_) => ExecutionAccessPathVariant::Union,
158 AccessPlan::Intersection(_) => ExecutionAccessPathVariant::Intersection,
159 }
160}
161
162const fn execution_order_direction(direction: Direction) -> OrderDirection {
163 match direction {
164 Direction::Asc => OrderDirection::Asc,
165 Direction::Desc => OrderDirection::Desc,
166 }
167}
168
169#[cfg(test)]
174mod tests {
175 use crate::db::{
176 access::AccessPlan,
177 diagnostics::{ExecutionMetrics, ExecutionOptimization, ExecutionTrace},
178 direction::Direction,
179 };
180
181 #[test]
182 fn execution_trace_metrics_projection_exposes_requested_surface() {
183 let access = AccessPlan::by_key(11u64);
184 let mut trace = ExecutionTrace::new(&access, Direction::Asc, false);
185 trace.set_path_outcome(
186 Some(ExecutionOptimization::PrimaryKey),
187 5,
188 3,
189 2,
190 42,
191 true,
192 true,
193 7,
194 9,
195 );
196
197 let metrics = trace.metrics();
198 assert_eq!(
199 metrics,
200 ExecutionMetrics {
201 rows_scanned: 5,
202 rows_materialized: 3,
203 execution_time_micros: 42,
204 index_only: true,
205 },
206 "metrics projection must expose rows_scanned/rows_materialized/execution_time/index_only",
207 );
208 assert_eq!(
209 trace.rows_returned, 2,
210 "trace should preserve returned-row counters independently from materialization counters",
211 );
212 }
213}