1use super::{
4 AccessPath, AccessPlan, DeleteLimitSpec, LogicalPlan, OrderDirection, OrderSpec, PageSpec,
5};
6use crate::{
7 db::query::{
8 QueryMode, ReadConsistency,
9 predicate::{CompareOp, ComparePredicate, Predicate, coercion::CoercionSpec, normalize},
10 },
11 traits::FieldValue,
12 value::Value,
13};
14
15#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct ExplainPlan {
23 pub mode: QueryMode,
24 pub access: ExplainAccessPath,
25 pub predicate: ExplainPredicate,
26 pub order_by: ExplainOrderBy,
27 pub page: ExplainPagination,
28 pub delete_limit: ExplainDeleteLimit,
29 pub consistency: ReadConsistency,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum ExplainAccessPath {
38 ByKey {
39 key: Value,
40 },
41 ByKeys {
42 keys: Vec<Value>,
43 },
44 KeyRange {
45 start: Value,
46 end: Value,
47 },
48 IndexPrefix {
49 name: &'static str,
50 fields: Vec<&'static str>,
51 prefix_len: usize,
52 values: Vec<Value>,
53 },
54 FullScan,
55 Union(Vec<Self>),
56 Intersection(Vec<Self>),
57}
58
59#[derive(Clone, Debug, Eq, PartialEq)]
64pub enum ExplainPredicate {
65 None,
66 True,
67 False,
68 And(Vec<Self>),
69 Or(Vec<Self>),
70 Not(Box<Self>),
71 Compare {
72 field: String,
73 op: CompareOp,
74 value: Value,
75 coercion: CoercionSpec,
76 },
77 IsNull {
78 field: String,
79 },
80 IsMissing {
81 field: String,
82 },
83 IsEmpty {
84 field: String,
85 },
86 IsNotEmpty {
87 field: String,
88 },
89 TextContains {
90 field: String,
91 value: Value,
92 },
93 TextContainsCi {
94 field: String,
95 value: Value,
96 },
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum ExplainOrderBy {
105 None,
106 Fields(Vec<ExplainOrder>),
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct ExplainOrder {
115 pub field: String,
116 pub direction: OrderDirection,
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
124pub enum ExplainPagination {
125 None,
126 Page { limit: Option<u32>, offset: u32 },
127}
128
129#[derive(Clone, Debug, Eq, PartialEq)]
134pub enum ExplainDeleteLimit {
135 None,
136 Limit { max_rows: u32 },
137}
138
139impl<K> LogicalPlan<K>
140where
141 K: FieldValue,
142{
143 #[must_use]
145 pub fn explain(&self) -> ExplainPlan {
146 let predicate = match &self.predicate {
147 Some(predicate) => ExplainPredicate::from_predicate(&normalize(predicate)),
148 None => ExplainPredicate::None,
149 };
150
151 let order_by = explain_order(self.order.as_ref());
152 let page = explain_page(self.page.as_ref());
153 let delete_limit = explain_delete_limit(self.delete_limit.as_ref());
154
155 ExplainPlan {
156 mode: self.mode,
157 access: ExplainAccessPath::from_access_plan(&self.access),
158 predicate,
159 order_by,
160 page,
161 delete_limit,
162 consistency: self.consistency,
163 }
164 }
165}
166
167impl ExplainAccessPath {
168 fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
169 where
170 K: FieldValue,
171 {
172 match access {
173 AccessPlan::Path(path) => Self::from_path(path),
174 AccessPlan::Union(children) => {
175 Self::Union(children.iter().map(Self::from_access_plan).collect())
176 }
177 AccessPlan::Intersection(children) => {
178 Self::Intersection(children.iter().map(Self::from_access_plan).collect())
179 }
180 }
181 }
182
183 fn from_path<K>(path: &AccessPath<K>) -> Self
184 where
185 K: FieldValue,
186 {
187 match path {
188 AccessPath::ByKey(key) => Self::ByKey {
189 key: key.to_value(),
190 },
191 AccessPath::ByKeys(keys) => Self::ByKeys {
192 keys: keys.iter().map(FieldValue::to_value).collect(),
193 },
194 AccessPath::KeyRange { start, end } => Self::KeyRange {
195 start: start.to_value(),
196 end: end.to_value(),
197 },
198 AccessPath::IndexPrefix { index, values } => Self::IndexPrefix {
199 name: index.name,
200 fields: index.fields.to_vec(),
201 prefix_len: values.len(),
202 values: values.clone(),
203 },
204 AccessPath::FullScan => Self::FullScan,
205 }
206 }
207}
208
209impl ExplainPredicate {
210 fn from_predicate(predicate: &Predicate) -> Self {
211 match predicate {
212 Predicate::True => Self::True,
213 Predicate::False => Self::False,
214 Predicate::And(children) => {
215 Self::And(children.iter().map(Self::from_predicate).collect())
216 }
217 Predicate::Or(children) => {
218 Self::Or(children.iter().map(Self::from_predicate).collect())
219 }
220 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
221 Predicate::Compare(compare) => Self::from_compare(compare),
222 Predicate::IsNull { field } => Self::IsNull {
223 field: field.clone(),
224 },
225 Predicate::IsMissing { field } => Self::IsMissing {
226 field: field.clone(),
227 },
228 Predicate::IsEmpty { field } => Self::IsEmpty {
229 field: field.clone(),
230 },
231 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
232 field: field.clone(),
233 },
234 Predicate::TextContains { field, value } => Self::TextContains {
235 field: field.clone(),
236 value: value.clone(),
237 },
238 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
239 field: field.clone(),
240 value: value.clone(),
241 },
242 }
243 }
244
245 fn from_compare(compare: &ComparePredicate) -> Self {
246 Self::Compare {
247 field: compare.field.clone(),
248 op: compare.op,
249 value: compare.value.clone(),
250 coercion: compare.coercion.clone(),
251 }
252 }
253}
254
255fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
256 let Some(order) = order else {
257 return ExplainOrderBy::None;
258 };
259
260 if order.fields.is_empty() {
261 return ExplainOrderBy::None;
262 }
263
264 ExplainOrderBy::Fields(
265 order
266 .fields
267 .iter()
268 .map(|(field, direction)| ExplainOrder {
269 field: field.clone(),
270 direction: *direction,
271 })
272 .collect(),
273 )
274}
275
276const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
277 match page {
278 Some(page) => ExplainPagination::Page {
279 limit: page.limit,
280 offset: page.offset,
281 },
282 None => ExplainPagination::None,
283 }
284}
285
286const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
287 match limit {
288 Some(limit) => ExplainDeleteLimit::Limit {
289 max_rows: limit.max_rows,
290 },
291 None => ExplainDeleteLimit::None,
292 }
293}
294
295#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::db::query::intent::{KeyAccess, access_plan_from_keys_value};
303 use crate::db::query::plan::{AccessPath, LogicalPlan};
304 use crate::db::query::predicate::Predicate;
305 use crate::db::query::{FieldRef, LoadSpec, QueryMode, ReadConsistency};
306 use crate::model::index::IndexModel;
307 use crate::types::Ulid;
308 use crate::value::Value;
309
310 #[test]
311 fn explain_is_deterministic_for_same_query() {
312 let predicate = FieldRef::new("id").eq(Ulid::default());
313 let mut plan: LogicalPlan<Value> =
314 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
315 plan.predicate = Some(predicate);
316
317 assert_eq!(plan.explain(), plan.explain());
318 }
319
320 #[test]
321 fn explain_is_deterministic_for_equivalent_predicates() {
322 let id = Ulid::default();
323
324 let predicate_a = Predicate::And(vec![
325 FieldRef::new("id").eq(id),
326 FieldRef::new("other").eq(Value::Text("x".to_string())),
327 ]);
328 let predicate_b = Predicate::And(vec![
329 FieldRef::new("other").eq(Value::Text("x".to_string())),
330 FieldRef::new("id").eq(id),
331 ]);
332
333 let mut plan_a: LogicalPlan<Value> =
334 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
335 plan_a.predicate = Some(predicate_a);
336
337 let mut plan_b: LogicalPlan<Value> =
338 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
339 plan_b.predicate = Some(predicate_b);
340
341 assert_eq!(plan_a.explain(), plan_b.explain());
342 }
343
344 #[test]
345 fn explain_is_deterministic_for_by_keys() {
346 let a = Ulid::from_u128(1);
347 let b = Ulid::from_u128(2);
348
349 let access_a = access_plan_from_keys_value(&KeyAccess::Many(vec![a, b, a]));
350 let access_b = access_plan_from_keys_value(&KeyAccess::Many(vec![b, a]));
351
352 let plan_a: LogicalPlan<Value> = LogicalPlan {
353 mode: QueryMode::Load(LoadSpec::new()),
354 access: access_a,
355 predicate: None,
356 order: None,
357 delete_limit: None,
358 page: None,
359 consistency: ReadConsistency::MissingOk,
360 };
361 let plan_b: LogicalPlan<Value> = LogicalPlan {
362 mode: QueryMode::Load(LoadSpec::new()),
363 access: access_b,
364 predicate: None,
365 order: None,
366 delete_limit: None,
367 page: None,
368 consistency: ReadConsistency::MissingOk,
369 };
370
371 assert_eq!(plan_a.explain(), plan_b.explain());
372 }
373
374 #[test]
375 fn explain_reports_deterministic_index_choice() {
376 const INDEX_FIELDS: [&str; 1] = ["idx_a"];
377 const INDEX_A: IndexModel =
378 IndexModel::new("explain::idx_a", "explain::store", &INDEX_FIELDS, false);
379 const INDEX_B: IndexModel =
380 IndexModel::new("explain::idx_a_alt", "explain::store", &INDEX_FIELDS, false);
381
382 let mut indexes = [INDEX_B, INDEX_A];
383 indexes.sort_by(|left, right| left.name.cmp(right.name));
384 let chosen = indexes[0];
385
386 let plan: LogicalPlan<Value> = LogicalPlan::new(
387 AccessPath::IndexPrefix {
388 index: chosen,
389 values: vec![Value::Text("alpha".to_string())],
390 },
391 crate::db::query::ReadConsistency::MissingOk,
392 );
393
394 let explain = plan.explain();
395 match explain.access {
396 ExplainAccessPath::IndexPrefix {
397 name,
398 fields,
399 prefix_len,
400 ..
401 } => {
402 assert_eq!(name, "explain::idx_a");
403 assert_eq!(fields, vec!["idx_a"]);
404 assert_eq!(prefix_len, 1);
405 }
406 _ => panic!("expected index prefix"),
407 }
408 }
409
410 #[test]
411 fn explain_differs_for_semantic_changes() {
412 let plan_a: LogicalPlan<Value> = LogicalPlan::new(
413 AccessPath::ByKey(Value::Ulid(Ulid::from_u128(1))),
414 ReadConsistency::MissingOk,
415 );
416 let plan_b: LogicalPlan<Value> =
417 LogicalPlan::new(AccessPath::<Value>::FullScan, ReadConsistency::MissingOk);
418
419 assert_ne!(plan_a.explain(), plan_b.explain());
420 }
421}