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, GroupedPlanFallbackReason, GroupedPlanStrategy,
18 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
19 grouped_plan_strategy,
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 fallback_reason: Option<ExplainGroupedFallbackReason>,
188 group_fields: Vec<ExplainGroupField>,
189 aggregates: Vec<ExplainGroupAggregate>,
190 having: Option<ExplainGroupHaving>,
191 max_groups: u64,
192 max_group_bytes: u64,
193 },
194}
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum ExplainGroupedStrategy {
204 HashGroup,
205 OrderedGroup,
206}
207
208impl From<GroupedPlanStrategy> for ExplainGroupedStrategy {
209 fn from(value: GroupedPlanStrategy) -> Self {
210 if value.is_ordered_group() {
211 Self::OrderedGroup
212 } else {
213 Self::HashGroup
214 }
215 }
216}
217
218#[derive(Clone, Copy, Debug, Eq, PartialEq)]
225pub enum ExplainGroupedFallbackReason {
226 DistinctGroupingNotAdmitted,
227 ResidualPredicateBlocksGroupedOrder,
228 AggregateStreamingNotSupported,
229 HavingBlocksGroupedOrder,
230 GroupKeyOrderUnavailable,
231}
232
233impl From<GroupedPlanFallbackReason> for ExplainGroupedFallbackReason {
234 fn from(value: GroupedPlanFallbackReason) -> Self {
235 match value {
236 GroupedPlanFallbackReason::DistinctGroupingNotAdmitted => {
237 Self::DistinctGroupingNotAdmitted
238 }
239 GroupedPlanFallbackReason::ResidualPredicateBlocksGroupedOrder => {
240 Self::ResidualPredicateBlocksGroupedOrder
241 }
242 GroupedPlanFallbackReason::AggregateStreamingNotSupported => {
243 Self::AggregateStreamingNotSupported
244 }
245 GroupedPlanFallbackReason::HavingBlocksGroupedOrder => Self::HavingBlocksGroupedOrder,
246 GroupedPlanFallbackReason::GroupKeyOrderUnavailable => Self::GroupKeyOrderUnavailable,
247 }
248 }
249}
250
251#[derive(Clone, Debug, Eq, PartialEq)]
258pub struct ExplainGroupField {
259 pub(crate) slot_index: usize,
260 pub(crate) field: String,
261}
262
263impl ExplainGroupField {
264 #[must_use]
266 pub const fn slot_index(&self) -> usize {
267 self.slot_index
268 }
269
270 #[must_use]
272 pub const fn field(&self) -> &str {
273 self.field.as_str()
274 }
275}
276
277#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct ExplainGroupAggregate {
285 pub(crate) kind: AggregateKind,
286 pub(crate) target_field: Option<String>,
287 pub(crate) distinct: bool,
288}
289
290impl ExplainGroupAggregate {
291 #[must_use]
293 pub const fn kind(&self) -> AggregateKind {
294 self.kind
295 }
296
297 #[must_use]
299 pub fn target_field(&self) -> Option<&str> {
300 self.target_field.as_deref()
301 }
302
303 #[must_use]
305 pub const fn distinct(&self) -> bool {
306 self.distinct
307 }
308}
309
310#[derive(Clone, Debug, Eq, PartialEq)]
317pub struct ExplainGroupHaving {
318 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
319}
320
321impl ExplainGroupHaving {
322 #[must_use]
324 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
325 self.clauses.as_slice()
326 }
327}
328
329#[derive(Clone, Debug, Eq, PartialEq)]
336pub struct ExplainGroupHavingClause {
337 pub(crate) symbol: ExplainGroupHavingSymbol,
338 pub(crate) op: CompareOp,
339 pub(crate) value: Value,
340}
341
342impl ExplainGroupHavingClause {
343 #[must_use]
345 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
346 &self.symbol
347 }
348
349 #[must_use]
351 pub const fn op(&self) -> CompareOp {
352 self.op
353 }
354
355 #[must_use]
357 pub const fn value(&self) -> &Value {
358 &self.value
359 }
360}
361
362#[derive(Clone, Debug, Eq, PartialEq)]
369pub enum ExplainGroupHavingSymbol {
370 GroupField { slot_index: usize, field: String },
371 AggregateIndex { index: usize },
372}
373
374#[derive(Clone, Debug, Eq, PartialEq)]
381pub enum ExplainOrderPushdown {
382 MissingModelContext,
383 EligibleSecondaryIndex {
384 index: &'static str,
385 prefix_len: usize,
386 },
387 Rejected(SecondaryOrderPushdownRejection),
388}
389
390#[derive(Clone, Debug, Eq, PartialEq)]
398pub enum ExplainAccessPath {
399 ByKey {
400 key: Value,
401 },
402 ByKeys {
403 keys: Vec<Value>,
404 },
405 KeyRange {
406 start: Value,
407 end: Value,
408 },
409 IndexPrefix {
410 name: &'static str,
411 fields: Vec<&'static str>,
412 prefix_len: usize,
413 values: Vec<Value>,
414 },
415 IndexMultiLookup {
416 name: &'static str,
417 fields: Vec<&'static str>,
418 values: Vec<Value>,
419 },
420 IndexRange {
421 name: &'static str,
422 fields: Vec<&'static str>,
423 prefix_len: usize,
424 prefix: Vec<Value>,
425 lower: Bound<Value>,
426 upper: Bound<Value>,
427 },
428 FullScan,
429 Union(Vec<Self>),
430 Intersection(Vec<Self>),
431}
432
433#[derive(Clone, Debug, Eq, PartialEq)]
441pub enum ExplainPredicate {
442 None,
443 True,
444 False,
445 And(Vec<Self>),
446 Or(Vec<Self>),
447 Not(Box<Self>),
448 Compare {
449 field: String,
450 op: CompareOp,
451 value: Value,
452 coercion: CoercionSpec,
453 },
454 IsNull {
455 field: String,
456 },
457 IsNotNull {
458 field: String,
459 },
460 IsMissing {
461 field: String,
462 },
463 IsEmpty {
464 field: String,
465 },
466 IsNotEmpty {
467 field: String,
468 },
469 TextContains {
470 field: String,
471 value: Value,
472 },
473 TextContainsCi {
474 field: String,
475 value: Value,
476 },
477}
478
479#[derive(Clone, Debug, Eq, PartialEq)]
486pub enum ExplainOrderBy {
487 None,
488 Fields(Vec<ExplainOrder>),
489}
490
491#[derive(Clone, Debug, Eq, PartialEq)]
498pub struct ExplainOrder {
499 pub(crate) field: String,
500 pub(crate) direction: OrderDirection,
501}
502
503impl ExplainOrder {
504 #[must_use]
506 pub const fn field(&self) -> &str {
507 self.field.as_str()
508 }
509
510 #[must_use]
512 pub const fn direction(&self) -> OrderDirection {
513 self.direction
514 }
515}
516
517#[derive(Clone, Debug, Eq, PartialEq)]
524pub enum ExplainPagination {
525 None,
526 Page { limit: Option<u32>, offset: u32 },
527}
528
529#[derive(Clone, Debug, Eq, PartialEq)]
536pub enum ExplainDeleteLimit {
537 None,
538 Limit { max_rows: u32 },
539}
540
541impl AccessPlannedQuery {
542 #[must_use]
544 #[cfg(test)]
545 pub(crate) fn explain(&self) -> ExplainPlan {
546 self.explain_inner(None)
547 }
548
549 #[must_use]
555 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
556 self.explain_inner(Some(model))
557 }
558
559 pub(in crate::db::query::explain) fn explain_inner(
560 &self,
561 model: Option<&EntityModel>,
562 ) -> ExplainPlan {
563 let (logical, grouping) = match &self.logical {
565 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
566 LogicalPlan::Grouped(logical) => {
567 let grouped_strategy = grouped_plan_strategy(self).expect(
568 "grouped logical explain projection requires planner-owned grouped strategy",
569 );
570
571 (
572 &logical.scalar,
573 ExplainGrouping::Grouped {
574 strategy: grouped_strategy.into(),
575 fallback_reason: grouped_strategy.fallback_reason().map(Into::into),
576 group_fields: logical
577 .group
578 .group_fields
579 .iter()
580 .map(|field_slot| ExplainGroupField {
581 slot_index: field_slot.index(),
582 field: field_slot.field().to_string(),
583 })
584 .collect(),
585 aggregates: logical
586 .group
587 .aggregates
588 .iter()
589 .map(|aggregate| ExplainGroupAggregate {
590 kind: aggregate.kind,
591 target_field: aggregate.target_field.clone(),
592 distinct: aggregate.distinct,
593 })
594 .collect(),
595 having: explain_group_having(logical.having.as_ref()),
596 max_groups: logical.group.execution.max_groups(),
597 max_group_bytes: logical.group.execution.max_group_bytes(),
598 },
599 )
600 }
601 };
602
603 explain_scalar_inner(logical, grouping, model, &self.access)
605 }
606}
607
608fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
609 let having = having?;
610
611 Some(ExplainGroupHaving {
612 clauses: having
613 .clauses()
614 .iter()
615 .map(explain_group_having_clause)
616 .collect(),
617 })
618}
619
620fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
621 ExplainGroupHavingClause {
622 symbol: explain_group_having_symbol(clause.symbol()),
623 op: clause.op(),
624 value: clause.value().clone(),
625 }
626}
627
628fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
629 match symbol {
630 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
631 slot_index: field_slot.index(),
632 field: field_slot.field().to_string(),
633 },
634 GroupHavingSymbol::AggregateIndex(index) => {
635 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
636 }
637 }
638}
639
640fn explain_scalar_inner<K>(
641 logical: &ScalarPlan,
642 grouping: ExplainGrouping,
643 model: Option<&EntityModel>,
644 access: &AccessPlan<K>,
645) -> ExplainPlan
646where
647 K: FieldValue,
648{
649 let predicate_model = logical.predicate.clone();
651 let predicate = match &predicate_model {
652 Some(predicate) => ExplainPredicate::from_predicate(predicate),
653 None => ExplainPredicate::None,
654 };
655
656 let order_by = explain_order(logical.order.as_ref());
658 let order_pushdown = explain_order_pushdown(model);
659 let page = explain_page(logical.page.as_ref());
660 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
661
662 ExplainPlan {
664 mode: logical.mode,
665 access: ExplainAccessPath::from_access_plan(access),
666 predicate,
667 predicate_model,
668 order_by,
669 distinct: logical.distinct,
670 grouping,
671 order_pushdown,
672 page,
673 delete_limit,
674 consistency: logical.consistency,
675 }
676}
677
678const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
679 let _ = model;
680
681 ExplainOrderPushdown::MissingModelContext
683}
684
685impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
686 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
687 Self::from(PushdownSurfaceEligibility::from(&value))
688 }
689}
690
691impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
692 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
693 match value {
694 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
695 Self::EligibleSecondaryIndex { index, prefix_len }
696 }
697 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
698 }
699 }
700}
701
702impl ExplainPredicate {
703 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
704 match predicate {
705 Predicate::True => Self::True,
706 Predicate::False => Self::False,
707 Predicate::And(children) => {
708 Self::And(children.iter().map(Self::from_predicate).collect())
709 }
710 Predicate::Or(children) => {
711 Self::Or(children.iter().map(Self::from_predicate).collect())
712 }
713 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
714 Predicate::Compare(compare) => Self::from_compare(compare),
715 Predicate::IsNull { field } => Self::IsNull {
716 field: field.clone(),
717 },
718 Predicate::IsNotNull { field } => Self::IsNotNull {
719 field: field.clone(),
720 },
721 Predicate::IsMissing { field } => Self::IsMissing {
722 field: field.clone(),
723 },
724 Predicate::IsEmpty { field } => Self::IsEmpty {
725 field: field.clone(),
726 },
727 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
728 field: field.clone(),
729 },
730 Predicate::TextContains { field, value } => Self::TextContains {
731 field: field.clone(),
732 value: value.clone(),
733 },
734 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
735 field: field.clone(),
736 value: value.clone(),
737 },
738 }
739 }
740
741 fn from_compare(compare: &ComparePredicate) -> Self {
742 Self::Compare {
743 field: compare.field.clone(),
744 op: compare.op,
745 value: compare.value.clone(),
746 coercion: compare.coercion.clone(),
747 }
748 }
749}
750
751fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
752 let Some(order) = order else {
753 return ExplainOrderBy::None;
754 };
755
756 if order.fields.is_empty() {
757 return ExplainOrderBy::None;
758 }
759
760 ExplainOrderBy::Fields(
761 order
762 .fields
763 .iter()
764 .map(|(field, direction)| ExplainOrder {
765 field: field.clone(),
766 direction: *direction,
767 })
768 .collect(),
769 )
770}
771
772const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
773 match page {
774 Some(page) => ExplainPagination::Page {
775 limit: page.limit,
776 offset: page.offset,
777 },
778 None => ExplainPagination::None,
779 }
780}
781
782const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
783 match limit {
784 Some(limit) => ExplainDeleteLimit::Limit {
785 max_rows: limit.max_rows,
786 },
787 None => ExplainDeleteLimit::None,
788 }
789}
790
791fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
792 let mut object = JsonWriter::begin_object(out);
793 object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
794 object.field_with("access", |out| write_access_json(explain.access(), out));
795 object.field_value_debug("predicate", explain.predicate());
796 object.field_value_debug("order_by", explain.order_by());
797 object.field_bool("distinct", explain.distinct());
798 object.field_value_debug("grouping", explain.grouping());
799 object.field_value_debug("order_pushdown", explain.order_pushdown());
800 object.field_with("page", |out| write_pagination_json(explain.page(), out));
801 object.field_with("delete_limit", |out| {
802 write_delete_limit_json(explain.delete_limit(), out);
803 });
804 object.field_value_debug("consistency", &explain.consistency());
805 object.finish();
806}
807
808fn write_query_mode_json(mode: QueryMode, out: &mut String) {
809 let mut object = JsonWriter::begin_object(out);
810 match mode {
811 QueryMode::Load(spec) => {
812 object.field_str("type", "Load");
813 match spec.limit() {
814 Some(limit) => object.field_u64("limit", u64::from(limit)),
815 None => object.field_null("limit"),
816 }
817 object.field_u64("offset", u64::from(spec.offset()));
818 }
819 QueryMode::Delete(spec) => {
820 object.field_str("type", "Delete");
821 match spec.limit() {
822 Some(limit) => object.field_u64("limit", u64::from(limit)),
823 None => object.field_null("limit"),
824 }
825 }
826 }
827 object.finish();
828}
829
830fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
831 let mut object = JsonWriter::begin_object(out);
832 match page {
833 ExplainPagination::None => {
834 object.field_str("type", "None");
835 }
836 ExplainPagination::Page { limit, offset } => {
837 object.field_str("type", "Page");
838 match limit {
839 Some(limit) => object.field_u64("limit", u64::from(*limit)),
840 None => object.field_null("limit"),
841 }
842 object.field_u64("offset", u64::from(*offset));
843 }
844 }
845 object.finish();
846}
847
848fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
849 let mut object = JsonWriter::begin_object(out);
850 match limit {
851 ExplainDeleteLimit::None => {
852 object.field_str("type", "None");
853 }
854 ExplainDeleteLimit::Limit { max_rows } => {
855 object.field_str("type", "Limit");
856 object.field_u64("max_rows", u64::from(*max_rows));
857 }
858 }
859 object.finish();
860}