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