1use crate::{
7 db::{
8 access::{
9 AccessPlan, PushdownSurfaceEligibility, SecondaryOrderPushdownEligibility,
10 SecondaryOrderPushdownRejection,
11 },
12 predicate::{CoercionSpec, CompareOp, ComparePredicate, MissingRowPolicy, Predicate},
13 query::{
14 explain::{access_projection::write_access_json, writer::JsonWriter},
15 plan::{
16 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, GroupHavingClause,
17 GroupHavingSpec, GroupHavingSymbol, GroupedPlanStrategyHint, LogicalPlan,
18 OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 grouped_plan_strategy_hint,
20 },
21 },
22 },
23 model::entity::EntityModel,
24 traits::FieldValue,
25 value::Value,
26};
27use std::ops::Bound;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct ExplainPlan {
37 pub(crate) mode: QueryMode,
38 pub(crate) access: ExplainAccessPath,
39 pub(crate) predicate: ExplainPredicate,
40 predicate_model: Option<Predicate>,
41 pub(crate) order_by: ExplainOrderBy,
42 pub(crate) distinct: bool,
43 pub(crate) grouping: ExplainGrouping,
44 pub(crate) order_pushdown: ExplainOrderPushdown,
45 pub(crate) page: ExplainPagination,
46 pub(crate) delete_limit: ExplainDeleteLimit,
47 pub(crate) consistency: MissingRowPolicy,
48}
49
50impl ExplainPlan {
51 #[must_use]
53 pub const fn mode(&self) -> QueryMode {
54 self.mode
55 }
56
57 #[must_use]
59 pub const fn access(&self) -> &ExplainAccessPath {
60 &self.access
61 }
62
63 #[must_use]
65 pub const fn predicate(&self) -> &ExplainPredicate {
66 &self.predicate
67 }
68
69 #[must_use]
71 pub const fn order_by(&self) -> &ExplainOrderBy {
72 &self.order_by
73 }
74
75 #[must_use]
77 pub const fn distinct(&self) -> bool {
78 self.distinct
79 }
80
81 #[must_use]
83 pub const fn grouping(&self) -> &ExplainGrouping {
84 &self.grouping
85 }
86
87 #[must_use]
89 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
90 &self.order_pushdown
91 }
92
93 #[must_use]
95 pub const fn page(&self) -> &ExplainPagination {
96 &self.page
97 }
98
99 #[must_use]
101 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
102 &self.delete_limit
103 }
104
105 #[must_use]
107 pub const fn consistency(&self) -> MissingRowPolicy {
108 self.consistency
109 }
110}
111
112impl ExplainPlan {
113 #[must_use]
117 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
118 if let Some(predicate) = &self.predicate_model {
119 debug_assert_eq!(
120 self.predicate,
121 ExplainPredicate::from_predicate(predicate),
122 "explain predicate surface drifted from canonical predicate model"
123 );
124 Some(predicate)
125 } else {
126 debug_assert!(
127 matches!(self.predicate, ExplainPredicate::None),
128 "missing canonical predicate model requires ExplainPredicate::None"
129 );
130 None
131 }
132 }
133
134 #[must_use]
139 pub fn render_text_canonical(&self) -> String {
140 format!(
141 concat!(
142 "mode={:?}\n",
143 "access={:?}\n",
144 "predicate={:?}\n",
145 "order_by={:?}\n",
146 "distinct={}\n",
147 "grouping={:?}\n",
148 "order_pushdown={:?}\n",
149 "page={:?}\n",
150 "delete_limit={:?}\n",
151 "consistency={:?}",
152 ),
153 self.mode(),
154 self.access(),
155 self.predicate(),
156 self.order_by(),
157 self.distinct(),
158 self.grouping(),
159 self.order_pushdown(),
160 self.page(),
161 self.delete_limit(),
162 self.consistency(),
163 )
164 }
165
166 #[must_use]
168 pub fn render_json_canonical(&self) -> String {
169 let mut out = String::new();
170 write_logical_explain_json(self, &mut out);
171
172 out
173 }
174}
175
176#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum ExplainGrouping {
184 None,
185 Grouped {
186 strategy: ExplainGroupedStrategy,
187 group_fields: Vec<ExplainGroupField>,
188 aggregates: Vec<ExplainGroupAggregate>,
189 having: Option<ExplainGroupHaving>,
190 max_groups: u64,
191 max_group_bytes: u64,
192 },
193}
194
195#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub enum ExplainGroupedStrategy {
203 HashGroup,
204 OrderedGroup,
205}
206
207impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
208 fn from(value: GroupedPlanStrategyHint) -> Self {
209 match value {
210 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
211 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
212 }
213 }
214}
215
216#[derive(Clone, Debug, Eq, PartialEq)]
223pub struct ExplainGroupField {
224 pub(crate) slot_index: usize,
225 pub(crate) field: String,
226}
227
228impl ExplainGroupField {
229 #[must_use]
231 pub const fn slot_index(&self) -> usize {
232 self.slot_index
233 }
234
235 #[must_use]
237 pub const fn field(&self) -> &str {
238 self.field.as_str()
239 }
240}
241
242#[derive(Clone, Debug, Eq, PartialEq)]
249pub struct ExplainGroupAggregate {
250 pub(crate) kind: AggregateKind,
251 pub(crate) target_field: Option<String>,
252 pub(crate) distinct: bool,
253}
254
255impl ExplainGroupAggregate {
256 #[must_use]
258 pub const fn kind(&self) -> AggregateKind {
259 self.kind
260 }
261
262 #[must_use]
264 pub fn target_field(&self) -> Option<&str> {
265 self.target_field.as_deref()
266 }
267
268 #[must_use]
270 pub const fn distinct(&self) -> bool {
271 self.distinct
272 }
273}
274
275#[derive(Clone, Debug, Eq, PartialEq)]
282pub struct ExplainGroupHaving {
283 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
284}
285
286impl ExplainGroupHaving {
287 #[must_use]
289 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
290 self.clauses.as_slice()
291 }
292}
293
294#[derive(Clone, Debug, Eq, PartialEq)]
301pub struct ExplainGroupHavingClause {
302 pub(crate) symbol: ExplainGroupHavingSymbol,
303 pub(crate) op: CompareOp,
304 pub(crate) value: Value,
305}
306
307impl ExplainGroupHavingClause {
308 #[must_use]
310 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
311 &self.symbol
312 }
313
314 #[must_use]
316 pub const fn op(&self) -> CompareOp {
317 self.op
318 }
319
320 #[must_use]
322 pub const fn value(&self) -> &Value {
323 &self.value
324 }
325}
326
327#[derive(Clone, Debug, Eq, PartialEq)]
334pub enum ExplainGroupHavingSymbol {
335 GroupField { slot_index: usize, field: String },
336 AggregateIndex { index: usize },
337}
338
339#[derive(Clone, Debug, Eq, PartialEq)]
346pub enum ExplainOrderPushdown {
347 MissingModelContext,
348 EligibleSecondaryIndex {
349 index: &'static str,
350 prefix_len: usize,
351 },
352 Rejected(SecondaryOrderPushdownRejection),
353}
354
355#[derive(Clone, Debug, Eq, PartialEq)]
363pub enum ExplainAccessPath {
364 ByKey {
365 key: Value,
366 },
367 ByKeys {
368 keys: Vec<Value>,
369 },
370 KeyRange {
371 start: Value,
372 end: Value,
373 },
374 IndexPrefix {
375 name: &'static str,
376 fields: Vec<&'static str>,
377 prefix_len: usize,
378 values: Vec<Value>,
379 },
380 IndexMultiLookup {
381 name: &'static str,
382 fields: Vec<&'static str>,
383 values: Vec<Value>,
384 },
385 IndexRange {
386 name: &'static str,
387 fields: Vec<&'static str>,
388 prefix_len: usize,
389 prefix: Vec<Value>,
390 lower: Bound<Value>,
391 upper: Bound<Value>,
392 },
393 FullScan,
394 Union(Vec<Self>),
395 Intersection(Vec<Self>),
396}
397
398#[derive(Clone, Debug, Eq, PartialEq)]
406pub enum ExplainPredicate {
407 None,
408 True,
409 False,
410 And(Vec<Self>),
411 Or(Vec<Self>),
412 Not(Box<Self>),
413 Compare {
414 field: String,
415 op: CompareOp,
416 value: Value,
417 coercion: CoercionSpec,
418 },
419 IsNull {
420 field: String,
421 },
422 IsNotNull {
423 field: String,
424 },
425 IsMissing {
426 field: String,
427 },
428 IsEmpty {
429 field: String,
430 },
431 IsNotEmpty {
432 field: String,
433 },
434 TextContains {
435 field: String,
436 value: Value,
437 },
438 TextContainsCi {
439 field: String,
440 value: Value,
441 },
442}
443
444#[derive(Clone, Debug, Eq, PartialEq)]
451pub enum ExplainOrderBy {
452 None,
453 Fields(Vec<ExplainOrder>),
454}
455
456#[derive(Clone, Debug, Eq, PartialEq)]
463pub struct ExplainOrder {
464 pub(crate) field: String,
465 pub(crate) direction: OrderDirection,
466}
467
468impl ExplainOrder {
469 #[must_use]
471 pub const fn field(&self) -> &str {
472 self.field.as_str()
473 }
474
475 #[must_use]
477 pub const fn direction(&self) -> OrderDirection {
478 self.direction
479 }
480}
481
482#[derive(Clone, Debug, Eq, PartialEq)]
489pub enum ExplainPagination {
490 None,
491 Page { limit: Option<u32>, offset: u32 },
492}
493
494#[derive(Clone, Debug, Eq, PartialEq)]
501pub enum ExplainDeleteLimit {
502 None,
503 Limit { max_rows: u32 },
504}
505
506impl<K> AccessPlannedQuery<K>
507where
508 K: FieldValue,
509{
510 #[must_use]
512 #[cfg(test)]
513 pub(crate) fn explain(&self) -> ExplainPlan {
514 self.explain_inner(None)
515 }
516
517 #[must_use]
523 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
524 self.explain_inner(Some(model))
525 }
526
527 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
528 let (logical, grouping) = match &self.logical {
530 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
531 LogicalPlan::Grouped(logical) => (
532 &logical.scalar,
533 ExplainGrouping::Grouped {
534 strategy: grouped_plan_strategy_hint(self)
535 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
536 group_fields: logical
537 .group
538 .group_fields
539 .iter()
540 .map(|field_slot| ExplainGroupField {
541 slot_index: field_slot.index(),
542 field: field_slot.field().to_string(),
543 })
544 .collect(),
545 aggregates: logical
546 .group
547 .aggregates
548 .iter()
549 .map(|aggregate| ExplainGroupAggregate {
550 kind: aggregate.kind,
551 target_field: aggregate.target_field.clone(),
552 distinct: aggregate.distinct,
553 })
554 .collect(),
555 having: explain_group_having(logical.having.as_ref()),
556 max_groups: logical.group.execution.max_groups(),
557 max_group_bytes: logical.group.execution.max_group_bytes(),
558 },
559 ),
560 };
561
562 explain_scalar_inner(logical, grouping, model, &self.access)
564 }
565}
566
567fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
568 let having = having?;
569
570 Some(ExplainGroupHaving {
571 clauses: having
572 .clauses()
573 .iter()
574 .map(explain_group_having_clause)
575 .collect(),
576 })
577}
578
579fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
580 ExplainGroupHavingClause {
581 symbol: explain_group_having_symbol(clause.symbol()),
582 op: clause.op(),
583 value: clause.value().clone(),
584 }
585}
586
587fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
588 match symbol {
589 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
590 slot_index: field_slot.index(),
591 field: field_slot.field().to_string(),
592 },
593 GroupHavingSymbol::AggregateIndex(index) => {
594 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
595 }
596 }
597}
598
599fn explain_scalar_inner<K>(
600 logical: &ScalarPlan,
601 grouping: ExplainGrouping,
602 model: Option<&EntityModel>,
603 access: &AccessPlan<K>,
604) -> ExplainPlan
605where
606 K: FieldValue,
607{
608 let predicate_model = logical.predicate.clone();
610 let predicate = match &predicate_model {
611 Some(predicate) => ExplainPredicate::from_predicate(predicate),
612 None => ExplainPredicate::None,
613 };
614
615 let order_by = explain_order(logical.order.as_ref());
617 let order_pushdown = explain_order_pushdown(model);
618 let page = explain_page(logical.page.as_ref());
619 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
620
621 ExplainPlan {
623 mode: logical.mode,
624 access: ExplainAccessPath::from_access_plan(access),
625 predicate,
626 predicate_model,
627 order_by,
628 distinct: logical.distinct,
629 grouping,
630 order_pushdown,
631 page,
632 delete_limit,
633 consistency: logical.consistency,
634 }
635}
636
637const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
638 let _ = model;
639
640 ExplainOrderPushdown::MissingModelContext
642}
643
644impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
645 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
646 Self::from(PushdownSurfaceEligibility::from(&value))
647 }
648}
649
650impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
651 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
652 match value {
653 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
654 Self::EligibleSecondaryIndex { index, prefix_len }
655 }
656 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
657 }
658 }
659}
660
661impl ExplainPredicate {
662 fn from_predicate(predicate: &Predicate) -> Self {
663 match predicate {
664 Predicate::True => Self::True,
665 Predicate::False => Self::False,
666 Predicate::And(children) => {
667 Self::And(children.iter().map(Self::from_predicate).collect())
668 }
669 Predicate::Or(children) => {
670 Self::Or(children.iter().map(Self::from_predicate).collect())
671 }
672 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
673 Predicate::Compare(compare) => Self::from_compare(compare),
674 Predicate::IsNull { field } => Self::IsNull {
675 field: field.clone(),
676 },
677 Predicate::IsNotNull { field } => Self::IsNotNull {
678 field: field.clone(),
679 },
680 Predicate::IsMissing { field } => Self::IsMissing {
681 field: field.clone(),
682 },
683 Predicate::IsEmpty { field } => Self::IsEmpty {
684 field: field.clone(),
685 },
686 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
687 field: field.clone(),
688 },
689 Predicate::TextContains { field, value } => Self::TextContains {
690 field: field.clone(),
691 value: value.clone(),
692 },
693 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
694 field: field.clone(),
695 value: value.clone(),
696 },
697 }
698 }
699
700 fn from_compare(compare: &ComparePredicate) -> Self {
701 Self::Compare {
702 field: compare.field.clone(),
703 op: compare.op,
704 value: compare.value.clone(),
705 coercion: compare.coercion.clone(),
706 }
707 }
708}
709
710fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
711 let Some(order) = order else {
712 return ExplainOrderBy::None;
713 };
714
715 if order.fields.is_empty() {
716 return ExplainOrderBy::None;
717 }
718
719 ExplainOrderBy::Fields(
720 order
721 .fields
722 .iter()
723 .map(|(field, direction)| ExplainOrder {
724 field: field.clone(),
725 direction: *direction,
726 })
727 .collect(),
728 )
729}
730
731const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
732 match page {
733 Some(page) => ExplainPagination::Page {
734 limit: page.limit,
735 offset: page.offset,
736 },
737 None => ExplainPagination::None,
738 }
739}
740
741const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
742 match limit {
743 Some(limit) => ExplainDeleteLimit::Limit {
744 max_rows: limit.max_rows,
745 },
746 None => ExplainDeleteLimit::None,
747 }
748}
749
750fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
751 let mut object = JsonWriter::begin_object(out);
752 object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
753 object.field_with("access", |out| write_access_json(explain.access(), out));
754 object.field_value_debug("predicate", explain.predicate());
755 object.field_value_debug("order_by", explain.order_by());
756 object.field_bool("distinct", explain.distinct());
757 object.field_value_debug("grouping", explain.grouping());
758 object.field_value_debug("order_pushdown", explain.order_pushdown());
759 object.field_with("page", |out| write_pagination_json(explain.page(), out));
760 object.field_with("delete_limit", |out| {
761 write_delete_limit_json(explain.delete_limit(), out);
762 });
763 object.field_value_debug("consistency", &explain.consistency());
764 object.finish();
765}
766
767fn write_query_mode_json(mode: QueryMode, out: &mut String) {
768 let mut object = JsonWriter::begin_object(out);
769 match mode {
770 QueryMode::Load(spec) => {
771 object.field_str("type", "Load");
772 match spec.limit() {
773 Some(limit) => object.field_u64("limit", u64::from(limit)),
774 None => object.field_null("limit"),
775 }
776 object.field_u64("offset", u64::from(spec.offset()));
777 }
778 QueryMode::Delete(spec) => {
779 object.field_str("type", "Delete");
780 match spec.limit() {
781 Some(limit) => object.field_u64("limit", u64::from(limit)),
782 None => object.field_null("limit"),
783 }
784 }
785 }
786 object.finish();
787}
788
789fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
790 let mut object = JsonWriter::begin_object(out);
791 match page {
792 ExplainPagination::None => {
793 object.field_str("type", "None");
794 }
795 ExplainPagination::Page { limit, offset } => {
796 object.field_str("type", "Page");
797 match limit {
798 Some(limit) => object.field_u64("limit", u64::from(*limit)),
799 None => object.field_null("limit"),
800 }
801 object.field_u64("offset", u64::from(*offset));
802 }
803 }
804 object.finish();
805}
806
807fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
808 let mut object = JsonWriter::begin_object(out);
809 match limit {
810 ExplainDeleteLimit::None => {
811 object.field_str("type", "None");
812 }
813 ExplainDeleteLimit::Limit { max_rows } => {
814 object.field_str("type", "Limit");
815 object.field_u64("max_rows", u64::from(*max_rows));
816 }
817 }
818 object.finish();
819}