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 IsNotNull {
380 field: String,
381 },
382 IsMissing {
383 field: String,
384 },
385 IsEmpty {
386 field: String,
387 },
388 IsNotEmpty {
389 field: String,
390 },
391 TextContains {
392 field: String,
393 value: Value,
394 },
395 TextContainsCi {
396 field: String,
397 value: Value,
398 },
399}
400
401#[derive(Clone, Debug, Eq, PartialEq)]
408pub enum ExplainOrderBy {
409 None,
410 Fields(Vec<ExplainOrder>),
411}
412
413#[derive(Clone, Debug, Eq, PartialEq)]
420pub struct ExplainOrder {
421 pub(crate) field: String,
422 pub(crate) direction: OrderDirection,
423}
424
425impl ExplainOrder {
426 #[must_use]
428 pub const fn field(&self) -> &str {
429 self.field.as_str()
430 }
431
432 #[must_use]
434 pub const fn direction(&self) -> OrderDirection {
435 self.direction
436 }
437}
438
439#[derive(Clone, Debug, Eq, PartialEq)]
446pub enum ExplainPagination {
447 None,
448 Page { limit: Option<u32>, offset: u32 },
449}
450
451#[derive(Clone, Debug, Eq, PartialEq)]
458pub enum ExplainDeleteLimit {
459 None,
460 Limit { max_rows: u32 },
461}
462
463impl<K> AccessPlannedQuery<K>
464where
465 K: FieldValue,
466{
467 #[must_use]
469 #[cfg(test)]
470 pub(crate) fn explain(&self) -> ExplainPlan {
471 self.explain_inner(None)
472 }
473
474 #[must_use]
480 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
481 self.explain_inner(Some(model))
482 }
483
484 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
485 let (logical, grouping) = match &self.logical {
487 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
488 LogicalPlan::Grouped(logical) => (
489 &logical.scalar,
490 ExplainGrouping::Grouped {
491 strategy: grouped_plan_strategy_hint(self)
492 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
493 group_fields: logical
494 .group
495 .group_fields
496 .iter()
497 .map(|field_slot| ExplainGroupField {
498 slot_index: field_slot.index(),
499 field: field_slot.field().to_string(),
500 })
501 .collect(),
502 aggregates: logical
503 .group
504 .aggregates
505 .iter()
506 .map(|aggregate| ExplainGroupAggregate {
507 kind: aggregate.kind,
508 target_field: aggregate.target_field.clone(),
509 distinct: aggregate.distinct,
510 })
511 .collect(),
512 having: explain_group_having(logical.having.as_ref()),
513 max_groups: logical.group.execution.max_groups(),
514 max_group_bytes: logical.group.execution.max_group_bytes(),
515 },
516 ),
517 };
518
519 explain_scalar_inner(logical, grouping, model, &self.access)
521 }
522}
523
524fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
525 let having = having?;
526
527 Some(ExplainGroupHaving {
528 clauses: having
529 .clauses()
530 .iter()
531 .map(explain_group_having_clause)
532 .collect(),
533 })
534}
535
536fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
537 ExplainGroupHavingClause {
538 symbol: explain_group_having_symbol(clause.symbol()),
539 op: clause.op(),
540 value: clause.value().clone(),
541 }
542}
543
544fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
545 match symbol {
546 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
547 slot_index: field_slot.index(),
548 field: field_slot.field().to_string(),
549 },
550 GroupHavingSymbol::AggregateIndex(index) => {
551 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
552 }
553 }
554}
555
556fn explain_scalar_inner<K>(
557 logical: &ScalarPlan,
558 grouping: ExplainGrouping,
559 model: Option<&EntityModel>,
560 access: &AccessPlan<K>,
561) -> ExplainPlan
562where
563 K: FieldValue,
564{
565 let predicate_model = logical.predicate.as_ref().map(normalize);
567 let predicate = match &predicate_model {
568 Some(predicate) => ExplainPredicate::from_predicate(predicate),
569 None => ExplainPredicate::None,
570 };
571
572 let order_by = explain_order(logical.order.as_ref());
574 let order_pushdown = explain_order_pushdown(model);
575 let page = explain_page(logical.page.as_ref());
576 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
577
578 ExplainPlan {
580 mode: logical.mode,
581 access: ExplainAccessPath::from_access_plan(access),
582 predicate,
583 predicate_model,
584 order_by,
585 distinct: logical.distinct,
586 grouping,
587 order_pushdown,
588 page,
589 delete_limit,
590 consistency: logical.consistency,
591 }
592}
593
594const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
595 let _ = model;
596
597 ExplainOrderPushdown::MissingModelContext
599}
600
601impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
602 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
603 Self::from(PushdownSurfaceEligibility::from(&value))
604 }
605}
606
607impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
608 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
609 match value {
610 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
611 Self::EligibleSecondaryIndex { index, prefix_len }
612 }
613 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
614 }
615 }
616}
617
618impl ExplainPredicate {
619 fn from_predicate(predicate: &Predicate) -> Self {
620 match predicate {
621 Predicate::True => Self::True,
622 Predicate::False => Self::False,
623 Predicate::And(children) => {
624 Self::And(children.iter().map(Self::from_predicate).collect())
625 }
626 Predicate::Or(children) => {
627 Self::Or(children.iter().map(Self::from_predicate).collect())
628 }
629 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
630 Predicate::Compare(compare) => Self::from_compare(compare),
631 Predicate::IsNull { field } => Self::IsNull {
632 field: field.clone(),
633 },
634 Predicate::IsNotNull { field } => Self::IsNotNull {
635 field: field.clone(),
636 },
637 Predicate::IsMissing { field } => Self::IsMissing {
638 field: field.clone(),
639 },
640 Predicate::IsEmpty { field } => Self::IsEmpty {
641 field: field.clone(),
642 },
643 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
644 field: field.clone(),
645 },
646 Predicate::TextContains { field, value } => Self::TextContains {
647 field: field.clone(),
648 value: value.clone(),
649 },
650 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
651 field: field.clone(),
652 value: value.clone(),
653 },
654 }
655 }
656
657 fn from_compare(compare: &ComparePredicate) -> Self {
658 Self::Compare {
659 field: compare.field.clone(),
660 op: compare.op,
661 value: compare.value.clone(),
662 coercion: compare.coercion.clone(),
663 }
664 }
665}
666
667fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
668 let Some(order) = order else {
669 return ExplainOrderBy::None;
670 };
671
672 if order.fields.is_empty() {
673 return ExplainOrderBy::None;
674 }
675
676 ExplainOrderBy::Fields(
677 order
678 .fields
679 .iter()
680 .map(|(field, direction)| ExplainOrder {
681 field: field.clone(),
682 direction: *direction,
683 })
684 .collect(),
685 )
686}
687
688const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
689 match page {
690 Some(page) => ExplainPagination::Page {
691 limit: page.limit,
692 offset: page.offset,
693 },
694 None => ExplainPagination::None,
695 }
696}
697
698const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
699 match limit {
700 Some(limit) => ExplainDeleteLimit::Limit {
701 max_rows: limit.max_rows,
702 },
703 None => ExplainDeleteLimit::None,
704 }
705}