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