1use crate::{
7 db::{
8 access::{
9 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10 SecondaryOrderPushdownRejection,
11 },
12 predicate::{
13 CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate, normalize,
14 },
15 query::plan::{
16 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupHavingClause, GroupHavingSpec,
17 GroupHavingSymbol, GroupedPlanStrategyHint, LogicalPlan, OrderDirection, OrderSpec,
18 PageSpec, QueryMode, ScalarPlan, grouped_plan_strategy_hint,
19 },
20 },
21 model::entity::EntityModel,
22 traits::FieldValue,
23 value::Value,
24};
25use std::ops::Bound;
26
27#[derive(Clone, Debug, Eq, PartialEq)]
34pub struct ExplainPlan {
35 pub(crate) mode: QueryMode,
36 pub(crate) access: ExplainAccessPath,
37 pub(crate) predicate: ExplainPredicate,
38 predicate_model: Option<Predicate>,
39 pub(crate) order_by: ExplainOrderBy,
40 pub(crate) distinct: bool,
41 pub(crate) grouping: ExplainGrouping,
42 pub(crate) order_pushdown: ExplainOrderPushdown,
43 pub(crate) page: ExplainPagination,
44 pub(crate) delete_limit: ExplainDeleteLimit,
45 pub(crate) consistency: MissingRowPolicy,
46}
47
48impl ExplainPlan {
49 #[must_use]
51 pub const fn mode(&self) -> QueryMode {
52 self.mode
53 }
54
55 #[must_use]
57 pub const fn access(&self) -> &ExplainAccessPath {
58 &self.access
59 }
60
61 #[must_use]
63 pub const fn predicate(&self) -> &ExplainPredicate {
64 &self.predicate
65 }
66
67 #[must_use]
69 pub const fn order_by(&self) -> &ExplainOrderBy {
70 &self.order_by
71 }
72
73 #[must_use]
75 pub const fn distinct(&self) -> bool {
76 self.distinct
77 }
78
79 #[must_use]
81 pub const fn grouping(&self) -> &ExplainGrouping {
82 &self.grouping
83 }
84
85 #[must_use]
87 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
88 &self.order_pushdown
89 }
90
91 #[must_use]
93 pub const fn page(&self) -> &ExplainPagination {
94 &self.page
95 }
96
97 #[must_use]
99 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
100 &self.delete_limit
101 }
102
103 #[must_use]
105 pub const fn consistency(&self) -> MissingRowPolicy {
106 self.consistency
107 }
108}
109
110impl ExplainPlan {
111 #[must_use]
115 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
116 if let Some(predicate) = &self.predicate_model {
117 debug_assert_eq!(
118 self.predicate,
119 ExplainPredicate::from_predicate(predicate),
120 "explain predicate surface drifted from canonical predicate model"
121 );
122 Some(predicate)
123 } else {
124 debug_assert!(
125 matches!(self.predicate, ExplainPredicate::None),
126 "missing canonical predicate model requires ExplainPredicate::None"
127 );
128 None
129 }
130 }
131}
132
133#[derive(Clone, Debug, Eq, PartialEq)]
140pub enum ExplainGrouping {
141 None,
142 Grouped {
143 strategy: ExplainGroupedStrategy,
144 group_fields: Vec<ExplainGroupField>,
145 aggregates: Vec<ExplainGroupAggregate>,
146 having: Option<ExplainGroupHaving>,
147 max_groups: u64,
148 max_group_bytes: u64,
149 },
150}
151
152#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub enum ExplainGroupedStrategy {
160 HashGroup,
161 OrderedGroup,
162}
163
164impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
165 fn from(value: GroupedPlanStrategyHint) -> Self {
166 match value {
167 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
168 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
169 }
170 }
171}
172
173#[derive(Clone, Debug, Eq, PartialEq)]
180pub struct ExplainGroupField {
181 pub(crate) slot_index: usize,
182 pub(crate) field: String,
183}
184
185impl ExplainGroupField {
186 #[must_use]
188 pub const fn slot_index(&self) -> usize {
189 self.slot_index
190 }
191
192 #[must_use]
194 pub const fn field(&self) -> &str {
195 self.field.as_str()
196 }
197}
198
199#[derive(Clone, Debug, Eq, PartialEq)]
206pub struct ExplainGroupAggregate {
207 pub(crate) kind: AggregateKind,
208 pub(crate) target_field: Option<String>,
209 pub(crate) distinct: bool,
210}
211
212impl ExplainGroupAggregate {
213 #[must_use]
215 pub const fn kind(&self) -> AggregateKind {
216 self.kind
217 }
218
219 #[must_use]
221 pub fn target_field(&self) -> Option<&str> {
222 self.target_field.as_deref()
223 }
224
225 #[must_use]
227 pub const fn distinct(&self) -> bool {
228 self.distinct
229 }
230}
231
232#[derive(Clone, Debug, Eq, PartialEq)]
239pub struct ExplainGroupHaving {
240 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
241}
242
243impl ExplainGroupHaving {
244 #[must_use]
246 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
247 self.clauses.as_slice()
248 }
249}
250
251#[derive(Clone, Debug, Eq, PartialEq)]
258pub struct ExplainGroupHavingClause {
259 pub(crate) symbol: ExplainGroupHavingSymbol,
260 pub(crate) op: CompareOp,
261 pub(crate) value: Value,
262}
263
264impl ExplainGroupHavingClause {
265 #[must_use]
267 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
268 &self.symbol
269 }
270
271 #[must_use]
273 pub const fn op(&self) -> CompareOp {
274 self.op
275 }
276
277 #[must_use]
279 pub const fn value(&self) -> &Value {
280 &self.value
281 }
282}
283
284#[derive(Clone, Debug, Eq, PartialEq)]
291pub enum ExplainGroupHavingSymbol {
292 GroupField { slot_index: usize, field: String },
293 AggregateIndex { index: usize },
294}
295
296#[derive(Clone, Debug, Eq, PartialEq)]
303pub enum ExplainOrderPushdown {
304 MissingModelContext,
305 EligibleSecondaryIndex {
306 index: &'static str,
307 prefix_len: usize,
308 },
309 Rejected(SecondaryOrderPushdownRejection),
310}
311
312#[derive(Clone, Debug, Eq, PartialEq)]
320pub enum ExplainAccessPath {
321 ByKey {
322 key: Value,
323 },
324 ByKeys {
325 keys: Vec<Value>,
326 },
327 KeyRange {
328 start: Value,
329 end: Value,
330 },
331 IndexPrefix {
332 name: &'static str,
333 fields: Vec<&'static str>,
334 prefix_len: usize,
335 values: Vec<Value>,
336 },
337 IndexMultiLookup {
338 name: &'static str,
339 fields: Vec<&'static str>,
340 values: Vec<Value>,
341 },
342 IndexRange {
343 name: &'static str,
344 fields: Vec<&'static str>,
345 prefix_len: usize,
346 prefix: Vec<Value>,
347 lower: Bound<Value>,
348 upper: Bound<Value>,
349 },
350 FullScan,
351 Union(Vec<Self>),
352 Intersection(Vec<Self>),
353}
354
355#[derive(Clone, Debug, Eq, PartialEq)]
363pub enum ExplainPredicate {
364 None,
365 True,
366 False,
367 And(Vec<Self>),
368 Or(Vec<Self>),
369 Not(Box<Self>),
370 Compare {
371 field: String,
372 op: CompareOp,
373 value: Value,
374 coercion: CoercionSpec,
375 },
376 IsNull {
377 field: String,
378 },
379 IsMissing {
380 field: String,
381 },
382 IsEmpty {
383 field: String,
384 },
385 IsNotEmpty {
386 field: String,
387 },
388 TextContains {
389 field: String,
390 value: Value,
391 },
392 TextContainsCi {
393 field: String,
394 value: Value,
395 },
396}
397
398#[derive(Clone, Debug, Eq, PartialEq)]
405pub enum ExplainOrderBy {
406 None,
407 Fields(Vec<ExplainOrder>),
408}
409
410#[derive(Clone, Debug, Eq, PartialEq)]
417pub struct ExplainOrder {
418 pub(crate) field: String,
419 pub(crate) direction: OrderDirection,
420}
421
422impl ExplainOrder {
423 #[must_use]
425 pub const fn field(&self) -> &str {
426 self.field.as_str()
427 }
428
429 #[must_use]
431 pub const fn direction(&self) -> OrderDirection {
432 self.direction
433 }
434}
435
436#[derive(Clone, Debug, Eq, PartialEq)]
443pub enum ExplainPagination {
444 None,
445 Page { limit: Option<u32>, offset: u32 },
446}
447
448#[derive(Clone, Debug, Eq, PartialEq)]
455pub enum ExplainDeleteLimit {
456 None,
457 Limit { max_rows: u32 },
458}
459
460impl<K> AccessPlannedQuery<K>
461where
462 K: FieldValue,
463{
464 #[must_use]
466 #[cfg(test)]
467 pub(crate) fn explain(&self) -> ExplainPlan {
468 self.explain_inner(None)
469 }
470
471 #[must_use]
477 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
478 self.explain_inner(Some(model))
479 }
480
481 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
482 let (logical, grouping) = match &self.logical {
484 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
485 LogicalPlan::Grouped(logical) => (
486 &logical.scalar,
487 ExplainGrouping::Grouped {
488 strategy: grouped_plan_strategy_hint(self)
489 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
490 group_fields: logical
491 .group
492 .group_fields
493 .iter()
494 .map(|field_slot| ExplainGroupField {
495 slot_index: field_slot.index(),
496 field: field_slot.field().to_string(),
497 })
498 .collect(),
499 aggregates: logical
500 .group
501 .aggregates
502 .iter()
503 .map(|aggregate| ExplainGroupAggregate {
504 kind: aggregate.kind,
505 target_field: aggregate.target_field.clone(),
506 distinct: aggregate.distinct,
507 })
508 .collect(),
509 having: explain_group_having(logical.having.as_ref()),
510 max_groups: logical.group.execution.max_groups(),
511 max_group_bytes: logical.group.execution.max_group_bytes(),
512 },
513 ),
514 };
515
516 explain_scalar_inner(logical, grouping, model, &self.access)
518 }
519}
520
521fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
522 let having = having?;
523
524 Some(ExplainGroupHaving {
525 clauses: having
526 .clauses()
527 .iter()
528 .map(explain_group_having_clause)
529 .collect(),
530 })
531}
532
533fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
534 ExplainGroupHavingClause {
535 symbol: explain_group_having_symbol(clause.symbol()),
536 op: clause.op(),
537 value: clause.value().clone(),
538 }
539}
540
541fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
542 match symbol {
543 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
544 slot_index: field_slot.index(),
545 field: field_slot.field().to_string(),
546 },
547 GroupHavingSymbol::AggregateIndex(index) => {
548 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
549 }
550 }
551}
552
553fn explain_scalar_inner<K>(
554 logical: &ScalarPlan,
555 grouping: ExplainGrouping,
556 model: Option<&EntityModel>,
557 access: &AccessPlan<K>,
558) -> ExplainPlan
559where
560 K: FieldValue,
561{
562 let predicate_model = logical.predicate.as_ref().map(normalize);
564 let predicate = match &predicate_model {
565 Some(predicate) => ExplainPredicate::from_predicate(predicate),
566 None => ExplainPredicate::None,
567 };
568
569 let order_by = explain_order(logical.order.as_ref());
571 let order_pushdown = explain_order_pushdown(model);
572 let page = explain_page(logical.page.as_ref());
573 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
574
575 ExplainPlan {
577 mode: logical.mode,
578 access: ExplainAccessPath::from_access_plan(access),
579 predicate,
580 predicate_model,
581 order_by,
582 distinct: logical.distinct,
583 grouping,
584 order_pushdown,
585 page,
586 delete_limit,
587 consistency: logical.consistency,
588 }
589}
590
591const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
592 let _ = model;
593
594 ExplainOrderPushdown::MissingModelContext
596}
597
598impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
599 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
600 Self::from(PushdownSurfaceEligibility::from(&value))
601 }
602}
603
604impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
605 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
606 match value {
607 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
608 Self::EligibleSecondaryIndex { index, prefix_len }
609 }
610 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
611 }
612 }
613}
614
615impl ExplainPredicate {
616 fn from_predicate(predicate: &Predicate) -> Self {
617 match predicate {
618 Predicate::True => Self::True,
619 Predicate::False => Self::False,
620 Predicate::And(children) => {
621 Self::And(children.iter().map(Self::from_predicate).collect())
622 }
623 Predicate::Or(children) => {
624 Self::Or(children.iter().map(Self::from_predicate).collect())
625 }
626 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
627 Predicate::Compare(compare) => Self::from_compare(compare),
628 Predicate::IsNull { field } => Self::IsNull {
629 field: field.clone(),
630 },
631 Predicate::IsMissing { field } => Self::IsMissing {
632 field: field.clone(),
633 },
634 Predicate::IsEmpty { field } => Self::IsEmpty {
635 field: field.clone(),
636 },
637 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
638 field: field.clone(),
639 },
640 Predicate::TextContains { field, value } => Self::TextContains {
641 field: field.clone(),
642 value: value.clone(),
643 },
644 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
645 field: field.clone(),
646 value: value.clone(),
647 },
648 }
649 }
650
651 fn from_compare(compare: &ComparePredicate) -> Self {
652 Self::Compare {
653 field: compare.field.clone(),
654 op: compare.op,
655 value: compare.value.clone(),
656 coercion: compare.coercion.clone(),
657 }
658 }
659}
660
661fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
662 let Some(order) = order else {
663 return ExplainOrderBy::None;
664 };
665
666 if order.fields.is_empty() {
667 return ExplainOrderBy::None;
668 }
669
670 ExplainOrderBy::Fields(
671 order
672 .fields
673 .iter()
674 .map(|(field, direction)| ExplainOrder {
675 field: field.clone(),
676 direction: *direction,
677 })
678 .collect(),
679 )
680}
681
682const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
683 match page {
684 Some(page) => ExplainPagination::Page {
685 limit: page.limit,
686 offset: page.offset,
687 },
688 None => ExplainPagination::None,
689 }
690}
691
692const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
693 match limit {
694 Some(limit) => ExplainDeleteLimit::Limit {
695 max_rows: limit.max_rows,
696 },
697 None => ExplainDeleteLimit::None,
698 }
699}