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