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 AccessPlanProjection, AccessPlannedQuery, AggregateKind, DeleteLimitSpec,
17 GroupHavingClause, GroupHavingSpec, GroupHavingSymbol, GroupedPlanStrategyHint,
18 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 grouped_plan_strategy_hint_for_plan, project_access_plan,
20 },
21 },
22 model::entity::EntityModel,
23 traits::FieldValue,
24 value::Value,
25};
26use std::ops::Bound;
27
28#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct ExplainPlan {
36 pub mode: QueryMode,
37 pub access: ExplainAccessPath,
38 pub predicate: ExplainPredicate,
39 predicate_model: Option<Predicate>,
40 pub order_by: ExplainOrderBy,
41 pub distinct: bool,
42 pub grouping: ExplainGrouping,
43 pub order_pushdown: ExplainOrderPushdown,
44 pub page: ExplainPagination,
45 pub delete_limit: ExplainDeleteLimit,
46 pub consistency: MissingRowPolicy,
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum ExplainAggregateTerminalRoute {
57 Standard,
58 IndexSeekFirst { fetch: usize },
59 IndexSeekLast { fetch: usize },
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ExplainAggregateTerminalPlan {
70 pub query: ExplainPlan,
71 pub terminal: AggregateKind,
72 pub route: ExplainAggregateTerminalRoute,
73 pub execution: ExplainExecutionDescriptor,
74}
75
76#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum ExplainExecutionOrderingSource {
84 AccessOrder,
85 Materialized,
86 IndexSeekFirst { fetch: usize },
87 IndexSeekLast { fetch: usize },
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
98pub struct ExplainExecutionDescriptor {
99 pub access_strategy: ExplainAccessPath,
100 pub covering_projection: bool,
101 pub aggregation: AggregateKind,
102 pub ordering_source: ExplainExecutionOrderingSource,
103 pub limit: Option<u32>,
104 pub cursor: bool,
105}
106
107impl ExplainAggregateTerminalPlan {
108 #[must_use]
109 pub(in crate::db) const fn new(
110 query: ExplainPlan,
111 terminal: AggregateKind,
112 execution: ExplainExecutionDescriptor,
113 ) -> Self {
114 let route = execution.route();
115
116 Self {
117 query,
118 terminal,
119 route,
120 execution,
121 }
122 }
123}
124
125impl ExplainExecutionDescriptor {
126 #[must_use]
127 pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
128 match self.ordering_source {
129 ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
130 ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
131 }
132 ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
133 ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
134 }
135 ExplainExecutionOrderingSource::AccessOrder
136 | ExplainExecutionOrderingSource::Materialized => {
137 ExplainAggregateTerminalRoute::Standard
138 }
139 }
140 }
141}
142
143impl ExplainPlan {
144 #[must_use]
148 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
149 if let Some(predicate) = &self.predicate_model {
150 debug_assert_eq!(
151 self.predicate,
152 ExplainPredicate::from_predicate(predicate),
153 "explain predicate surface drifted from canonical predicate model"
154 );
155 Some(predicate)
156 } else {
157 debug_assert!(
158 matches!(self.predicate, ExplainPredicate::None),
159 "missing canonical predicate model requires ExplainPredicate::None"
160 );
161 None
162 }
163 }
164}
165
166#[derive(Clone, Debug, Eq, PartialEq)]
173pub enum ExplainGrouping {
174 None,
175 Grouped {
176 strategy: ExplainGroupedStrategy,
177 group_fields: Vec<ExplainGroupField>,
178 aggregates: Vec<ExplainGroupAggregate>,
179 having: Option<ExplainGroupHaving>,
180 max_groups: u64,
181 max_group_bytes: u64,
182 },
183}
184
185#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub enum ExplainGroupedStrategy {
192 HashGroup,
193 OrderedGroup,
194}
195
196impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
197 fn from(value: GroupedPlanStrategyHint) -> Self {
198 match value {
199 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
200 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
201 }
202 }
203}
204
205#[derive(Clone, Debug, Eq, PartialEq)]
212pub struct ExplainGroupField {
213 pub slot_index: usize,
214 pub field: String,
215}
216
217#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct ExplainGroupAggregate {
225 pub kind: AggregateKind,
226 pub target_field: Option<String>,
227 pub distinct: bool,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
237pub struct ExplainGroupHaving {
238 pub clauses: Vec<ExplainGroupHavingClause>,
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct ExplainGroupHavingClause {
249 pub symbol: ExplainGroupHavingSymbol,
250 pub op: CompareOp,
251 pub value: Value,
252}
253
254#[derive(Clone, Debug, Eq, PartialEq)]
261pub enum ExplainGroupHavingSymbol {
262 GroupField { slot_index: usize, field: String },
263 AggregateIndex { index: usize },
264}
265
266#[derive(Clone, Debug, Eq, PartialEq)]
273pub enum ExplainOrderPushdown {
274 MissingModelContext,
275 EligibleSecondaryIndex {
276 index: &'static str,
277 prefix_len: usize,
278 },
279 Rejected(SecondaryOrderPushdownRejection),
280}
281
282#[derive(Clone, Debug, Eq, PartialEq)]
289pub enum ExplainAccessPath {
290 ByKey {
291 key: Value,
292 },
293 ByKeys {
294 keys: Vec<Value>,
295 },
296 KeyRange {
297 start: Value,
298 end: Value,
299 },
300 IndexPrefix {
301 name: &'static str,
302 fields: Vec<&'static str>,
303 prefix_len: usize,
304 values: Vec<Value>,
305 },
306 IndexRange {
307 name: &'static str,
308 fields: Vec<&'static str>,
309 prefix_len: usize,
310 prefix: Vec<Value>,
311 lower: Bound<Value>,
312 upper: Bound<Value>,
313 },
314 FullScan,
315 Union(Vec<Self>),
316 Intersection(Vec<Self>),
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
326pub enum ExplainPredicate {
327 None,
328 True,
329 False,
330 And(Vec<Self>),
331 Or(Vec<Self>),
332 Not(Box<Self>),
333 Compare {
334 field: String,
335 op: CompareOp,
336 value: Value,
337 coercion: CoercionSpec,
338 },
339 IsNull {
340 field: String,
341 },
342 IsMissing {
343 field: String,
344 },
345 IsEmpty {
346 field: String,
347 },
348 IsNotEmpty {
349 field: String,
350 },
351 TextContains {
352 field: String,
353 value: Value,
354 },
355 TextContainsCi {
356 field: String,
357 value: Value,
358 },
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
367pub enum ExplainOrderBy {
368 None,
369 Fields(Vec<ExplainOrder>),
370}
371
372#[derive(Clone, Debug, Eq, PartialEq)]
378pub struct ExplainOrder {
379 pub field: String,
380 pub direction: OrderDirection,
381}
382
383#[derive(Clone, Debug, Eq, PartialEq)]
389pub enum ExplainPagination {
390 None,
391 Page { limit: Option<u32>, offset: u32 },
392}
393
394#[derive(Clone, Debug, Eq, PartialEq)]
400pub enum ExplainDeleteLimit {
401 None,
402 Limit { max_rows: u32 },
403}
404
405impl<K> AccessPlannedQuery<K>
406where
407 K: FieldValue,
408{
409 #[must_use]
411 pub(crate) fn explain(&self) -> ExplainPlan {
412 self.explain_inner(None)
413 }
414
415 #[must_use]
421 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
422 self.explain_inner(Some(model))
423 }
424
425 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
426 let (logical, grouping) = match &self.logical {
428 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
429 LogicalPlan::Grouped(logical) => (
430 &logical.scalar,
431 ExplainGrouping::Grouped {
432 strategy: grouped_plan_strategy_hint_for_plan(self)
433 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
434 group_fields: logical
435 .group
436 .group_fields
437 .iter()
438 .map(|field_slot| ExplainGroupField {
439 slot_index: field_slot.index(),
440 field: field_slot.field().to_string(),
441 })
442 .collect(),
443 aggregates: logical
444 .group
445 .aggregates
446 .iter()
447 .map(|aggregate| ExplainGroupAggregate {
448 kind: aggregate.kind,
449 target_field: aggregate.target_field.clone(),
450 distinct: aggregate.distinct,
451 })
452 .collect(),
453 having: explain_group_having(logical.having.as_ref()),
454 max_groups: logical.group.execution.max_groups(),
455 max_group_bytes: logical.group.execution.max_group_bytes(),
456 },
457 ),
458 };
459
460 explain_scalar_inner(logical, grouping, model, &self.access)
462 }
463}
464
465fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
466 let having = having?;
467
468 Some(ExplainGroupHaving {
469 clauses: having
470 .clauses()
471 .iter()
472 .map(explain_group_having_clause)
473 .collect(),
474 })
475}
476
477fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
478 ExplainGroupHavingClause {
479 symbol: explain_group_having_symbol(clause.symbol()),
480 op: clause.op(),
481 value: clause.value().clone(),
482 }
483}
484
485fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
486 match symbol {
487 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
488 slot_index: field_slot.index(),
489 field: field_slot.field().to_string(),
490 },
491 GroupHavingSymbol::AggregateIndex(index) => {
492 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
493 }
494 }
495}
496
497fn explain_scalar_inner<K>(
498 logical: &ScalarPlan,
499 grouping: ExplainGrouping,
500 model: Option<&EntityModel>,
501 access: &AccessPlan<K>,
502) -> ExplainPlan
503where
504 K: FieldValue,
505{
506 let predicate_model = logical.predicate.as_ref().map(normalize);
508 let predicate = match &predicate_model {
509 Some(predicate) => ExplainPredicate::from_predicate(predicate),
510 None => ExplainPredicate::None,
511 };
512
513 let order_by = explain_order(logical.order.as_ref());
515 let order_pushdown = explain_order_pushdown(model);
516 let page = explain_page(logical.page.as_ref());
517 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
518
519 ExplainPlan {
521 mode: logical.mode,
522 access: ExplainAccessPath::from_access_plan(access),
523 predicate,
524 predicate_model,
525 order_by,
526 distinct: logical.distinct,
527 grouping,
528 order_pushdown,
529 page,
530 delete_limit,
531 consistency: logical.consistency,
532 }
533}
534
535const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
536 let _ = model;
537
538 ExplainOrderPushdown::MissingModelContext
540}
541
542impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
543 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
544 Self::from(PushdownSurfaceEligibility::from(&value))
545 }
546}
547
548impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
549 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
550 match value {
551 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
552 Self::EligibleSecondaryIndex { index, prefix_len }
553 }
554 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
555 }
556 }
557}
558
559struct ExplainAccessProjection;
560
561impl<K> AccessPlanProjection<K> for ExplainAccessProjection
562where
563 K: FieldValue,
564{
565 type Output = ExplainAccessPath;
566
567 fn by_key(&mut self, key: &K) -> Self::Output {
568 ExplainAccessPath::ByKey {
569 key: key.to_value(),
570 }
571 }
572
573 fn by_keys(&mut self, keys: &[K]) -> Self::Output {
574 ExplainAccessPath::ByKeys {
575 keys: keys.iter().map(FieldValue::to_value).collect(),
576 }
577 }
578
579 fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
580 ExplainAccessPath::KeyRange {
581 start: start.to_value(),
582 end: end.to_value(),
583 }
584 }
585
586 fn index_prefix(
587 &mut self,
588 index_name: &'static str,
589 index_fields: &[&'static str],
590 prefix_len: usize,
591 values: &[Value],
592 ) -> Self::Output {
593 ExplainAccessPath::IndexPrefix {
594 name: index_name,
595 fields: index_fields.to_vec(),
596 prefix_len,
597 values: values.to_vec(),
598 }
599 }
600
601 fn index_range(
602 &mut self,
603 index_name: &'static str,
604 index_fields: &[&'static str],
605 prefix_len: usize,
606 prefix: &[Value],
607 lower: &Bound<Value>,
608 upper: &Bound<Value>,
609 ) -> Self::Output {
610 ExplainAccessPath::IndexRange {
611 name: index_name,
612 fields: index_fields.to_vec(),
613 prefix_len,
614 prefix: prefix.to_vec(),
615 lower: lower.clone(),
616 upper: upper.clone(),
617 }
618 }
619
620 fn full_scan(&mut self) -> Self::Output {
621 ExplainAccessPath::FullScan
622 }
623
624 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
625 ExplainAccessPath::Union(children)
626 }
627
628 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
629 ExplainAccessPath::Intersection(children)
630 }
631}
632
633impl ExplainAccessPath {
634 pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
635 where
636 K: FieldValue,
637 {
638 let mut projection = ExplainAccessProjection;
639 project_access_plan(access, &mut projection)
640 }
641}
642
643impl ExplainPredicate {
644 fn from_predicate(predicate: &Predicate) -> Self {
645 match predicate {
646 Predicate::True => Self::True,
647 Predicate::False => Self::False,
648 Predicate::And(children) => {
649 Self::And(children.iter().map(Self::from_predicate).collect())
650 }
651 Predicate::Or(children) => {
652 Self::Or(children.iter().map(Self::from_predicate).collect())
653 }
654 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
655 Predicate::Compare(compare) => Self::from_compare(compare),
656 Predicate::IsNull { field } => Self::IsNull {
657 field: field.clone(),
658 },
659 Predicate::IsMissing { field } => Self::IsMissing {
660 field: field.clone(),
661 },
662 Predicate::IsEmpty { field } => Self::IsEmpty {
663 field: field.clone(),
664 },
665 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
666 field: field.clone(),
667 },
668 Predicate::TextContains { field, value } => Self::TextContains {
669 field: field.clone(),
670 value: value.clone(),
671 },
672 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
673 field: field.clone(),
674 value: value.clone(),
675 },
676 }
677 }
678
679 fn from_compare(compare: &ComparePredicate) -> Self {
680 Self::Compare {
681 field: compare.field.clone(),
682 op: compare.op,
683 value: compare.value.clone(),
684 coercion: compare.coercion.clone(),
685 }
686 }
687}
688
689fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
690 let Some(order) = order else {
691 return ExplainOrderBy::None;
692 };
693
694 if order.fields.is_empty() {
695 return ExplainOrderBy::None;
696 }
697
698 ExplainOrderBy::Fields(
699 order
700 .fields
701 .iter()
702 .map(|(field, direction)| ExplainOrder {
703 field: field.clone(),
704 direction: *direction,
705 })
706 .collect(),
707 )
708}
709
710const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
711 match page {
712 Some(page) => ExplainPagination::Page {
713 limit: page.limit,
714 offset: page.offset,
715 },
716 None => ExplainPagination::None,
717 }
718}
719
720const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
721 match limit {
722 Some(limit) => ExplainDeleteLimit::Limit {
723 max_rows: limit.max_rows,
724 },
725 None => ExplainDeleteLimit::None,
726 }
727}
728
729#[cfg(test)]
734mod tests;