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