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