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, LogicalPlan,
18 OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan, grouped_plan_strategy,
19 },
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(crate) mode: QueryMode,
37 pub(crate) access: ExplainAccessPath,
38 pub(crate) predicate: ExplainPredicate,
39 predicate_model: Option<Predicate>,
40 pub(crate) order_by: ExplainOrderBy,
41 pub(crate) distinct: bool,
42 pub(crate) grouping: ExplainGrouping,
43 pub(crate) order_pushdown: ExplainOrderPushdown,
44 pub(crate) page: ExplainPagination,
45 pub(crate) delete_limit: ExplainDeleteLimit,
46 pub(crate) consistency: MissingRowPolicy,
47}
48
49impl ExplainPlan {
50 #[must_use]
52 pub const fn mode(&self) -> QueryMode {
53 self.mode
54 }
55
56 #[must_use]
58 pub const fn access(&self) -> &ExplainAccessPath {
59 &self.access
60 }
61
62 #[must_use]
64 pub const fn predicate(&self) -> &ExplainPredicate {
65 &self.predicate
66 }
67
68 #[must_use]
70 pub const fn order_by(&self) -> &ExplainOrderBy {
71 &self.order_by
72 }
73
74 #[must_use]
76 pub const fn distinct(&self) -> bool {
77 self.distinct
78 }
79
80 #[must_use]
82 pub const fn grouping(&self) -> &ExplainGrouping {
83 &self.grouping
84 }
85
86 #[must_use]
88 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
89 &self.order_pushdown
90 }
91
92 #[must_use]
94 pub const fn page(&self) -> &ExplainPagination {
95 &self.page
96 }
97
98 #[must_use]
100 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
101 &self.delete_limit
102 }
103
104 #[must_use]
106 pub const fn consistency(&self) -> MissingRowPolicy {
107 self.consistency
108 }
109}
110
111impl ExplainPlan {
112 #[must_use]
116 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
117 if let Some(predicate) = &self.predicate_model {
118 debug_assert_eq!(
119 self.predicate,
120 ExplainPredicate::from_predicate(predicate),
121 "explain predicate surface drifted from canonical predicate model"
122 );
123 Some(predicate)
124 } else {
125 debug_assert!(
126 matches!(self.predicate, ExplainPredicate::None),
127 "missing canonical predicate model requires ExplainPredicate::None"
128 );
129 None
130 }
131 }
132
133 #[must_use]
138 pub fn render_text_canonical(&self) -> String {
139 format!(
140 concat!(
141 "mode={:?}\n",
142 "access={:?}\n",
143 "predicate={:?}\n",
144 "order_by={:?}\n",
145 "distinct={}\n",
146 "grouping={:?}\n",
147 "order_pushdown={:?}\n",
148 "page={:?}\n",
149 "delete_limit={:?}\n",
150 "consistency={:?}",
151 ),
152 self.mode(),
153 self.access(),
154 self.predicate(),
155 self.order_by(),
156 self.distinct(),
157 self.grouping(),
158 self.order_pushdown(),
159 self.page(),
160 self.delete_limit(),
161 self.consistency(),
162 )
163 }
164
165 #[must_use]
167 pub fn render_json_canonical(&self) -> String {
168 let mut out = String::new();
169 write_logical_explain_json(self, &mut out);
170
171 out
172 }
173}
174
175#[derive(Clone, Debug, Eq, PartialEq)]
182pub enum ExplainGrouping {
183 None,
184 Grouped {
185 strategy: &'static str,
186 fallback_reason: Option<&'static str>,
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, Debug, Eq, PartialEq)]
202pub struct ExplainGroupField {
203 pub(crate) slot_index: usize,
204 pub(crate) field: String,
205}
206
207impl ExplainGroupField {
208 #[must_use]
210 pub const fn slot_index(&self) -> usize {
211 self.slot_index
212 }
213
214 #[must_use]
216 pub const fn field(&self) -> &str {
217 self.field.as_str()
218 }
219}
220
221#[derive(Clone, Debug, Eq, PartialEq)]
228pub struct ExplainGroupAggregate {
229 pub(crate) kind: AggregateKind,
230 pub(crate) target_field: Option<String>,
231 pub(crate) distinct: bool,
232}
233
234impl ExplainGroupAggregate {
235 #[must_use]
237 pub const fn kind(&self) -> AggregateKind {
238 self.kind
239 }
240
241 #[must_use]
243 pub fn target_field(&self) -> Option<&str> {
244 self.target_field.as_deref()
245 }
246
247 #[must_use]
249 pub const fn distinct(&self) -> bool {
250 self.distinct
251 }
252}
253
254#[derive(Clone, Debug, Eq, PartialEq)]
261pub struct ExplainGroupHaving {
262 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
263}
264
265impl ExplainGroupHaving {
266 #[must_use]
268 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
269 self.clauses.as_slice()
270 }
271}
272
273#[derive(Clone, Debug, Eq, PartialEq)]
280pub struct ExplainGroupHavingClause {
281 pub(crate) symbol: ExplainGroupHavingSymbol,
282 pub(crate) op: CompareOp,
283 pub(crate) value: Value,
284}
285
286impl ExplainGroupHavingClause {
287 #[must_use]
289 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
290 &self.symbol
291 }
292
293 #[must_use]
295 pub const fn op(&self) -> CompareOp {
296 self.op
297 }
298
299 #[must_use]
301 pub const fn value(&self) -> &Value {
302 &self.value
303 }
304}
305
306#[derive(Clone, Debug, Eq, PartialEq)]
313pub enum ExplainGroupHavingSymbol {
314 GroupField { slot_index: usize, field: String },
315 AggregateIndex { index: usize },
316}
317
318#[derive(Clone, Debug, Eq, PartialEq)]
325pub enum ExplainOrderPushdown {
326 MissingModelContext,
327 EligibleSecondaryIndex {
328 index: &'static str,
329 prefix_len: usize,
330 },
331 Rejected(SecondaryOrderPushdownRejection),
332}
333
334#[derive(Clone, Debug, Eq, PartialEq)]
342pub enum ExplainAccessPath {
343 ByKey {
344 key: Value,
345 },
346 ByKeys {
347 keys: Vec<Value>,
348 },
349 KeyRange {
350 start: Value,
351 end: Value,
352 },
353 IndexPrefix {
354 name: &'static str,
355 fields: Vec<&'static str>,
356 prefix_len: usize,
357 values: Vec<Value>,
358 },
359 IndexMultiLookup {
360 name: &'static str,
361 fields: Vec<&'static str>,
362 values: Vec<Value>,
363 },
364 IndexRange {
365 name: &'static str,
366 fields: Vec<&'static str>,
367 prefix_len: usize,
368 prefix: Vec<Value>,
369 lower: Bound<Value>,
370 upper: Bound<Value>,
371 },
372 FullScan,
373 Union(Vec<Self>),
374 Intersection(Vec<Self>),
375}
376
377#[derive(Clone, Debug, Eq, PartialEq)]
385pub enum ExplainPredicate {
386 None,
387 True,
388 False,
389 And(Vec<Self>),
390 Or(Vec<Self>),
391 Not(Box<Self>),
392 Compare {
393 field: String,
394 op: CompareOp,
395 value: Value,
396 coercion: CoercionSpec,
397 },
398 CompareFields {
399 left_field: String,
400 op: CompareOp,
401 right_field: String,
402 coercion: CoercionSpec,
403 },
404 IsNull {
405 field: String,
406 },
407 IsNotNull {
408 field: String,
409 },
410 IsMissing {
411 field: String,
412 },
413 IsEmpty {
414 field: String,
415 },
416 IsNotEmpty {
417 field: String,
418 },
419 TextContains {
420 field: String,
421 value: Value,
422 },
423 TextContainsCi {
424 field: String,
425 value: Value,
426 },
427}
428
429#[derive(Clone, Debug, Eq, PartialEq)]
436pub enum ExplainOrderBy {
437 None,
438 Fields(Vec<ExplainOrder>),
439}
440
441#[derive(Clone, Debug, Eq, PartialEq)]
448pub struct ExplainOrder {
449 pub(crate) field: String,
450 pub(crate) direction: OrderDirection,
451}
452
453impl ExplainOrder {
454 #[must_use]
456 pub const fn field(&self) -> &str {
457 self.field.as_str()
458 }
459
460 #[must_use]
462 pub const fn direction(&self) -> OrderDirection {
463 self.direction
464 }
465}
466
467#[derive(Clone, Debug, Eq, PartialEq)]
474pub enum ExplainPagination {
475 None,
476 Page { limit: Option<u32>, offset: u32 },
477}
478
479#[derive(Clone, Debug, Eq, PartialEq)]
486pub enum ExplainDeleteLimit {
487 None,
488 Limit { max_rows: u32 },
489 Window { limit: Option<u32>, offset: u32 },
490}
491
492impl AccessPlannedQuery {
493 #[must_use]
495 #[cfg(test)]
496 pub(crate) fn explain(&self) -> ExplainPlan {
497 self.explain_inner(None)
498 }
499
500 #[must_use]
506 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
507 self.explain_inner(Some(model))
508 }
509
510 pub(in crate::db::query::explain) fn explain_inner(
511 &self,
512 model: Option<&EntityModel>,
513 ) -> ExplainPlan {
514 let (logical, grouping) = match &self.logical {
516 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
517 LogicalPlan::Grouped(logical) => {
518 let grouped_strategy = grouped_plan_strategy(self).expect(
519 "grouped logical explain projection requires planner-owned grouped strategy",
520 );
521
522 (
523 &logical.scalar,
524 ExplainGrouping::Grouped {
525 strategy: grouped_strategy.code(),
526 fallback_reason: grouped_strategy
527 .fallback_reason()
528 .map(GroupedPlanFallbackReason::code),
529 group_fields: logical
530 .group
531 .group_fields
532 .iter()
533 .map(|field_slot| ExplainGroupField {
534 slot_index: field_slot.index(),
535 field: field_slot.field().to_string(),
536 })
537 .collect(),
538 aggregates: logical
539 .group
540 .aggregates
541 .iter()
542 .map(|aggregate| ExplainGroupAggregate {
543 kind: aggregate.kind,
544 target_field: aggregate.target_field.clone(),
545 distinct: aggregate.distinct,
546 })
547 .collect(),
548 having: explain_group_having(logical.having.as_ref()),
549 max_groups: logical.group.execution.max_groups(),
550 max_group_bytes: logical.group.execution.max_group_bytes(),
551 },
552 )
553 }
554 };
555
556 explain_scalar_inner(logical, grouping, model, &self.access)
558 }
559}
560
561fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
562 let having = having?;
563
564 Some(ExplainGroupHaving {
565 clauses: having
566 .clauses()
567 .iter()
568 .map(explain_group_having_clause)
569 .collect(),
570 })
571}
572
573fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
574 ExplainGroupHavingClause {
575 symbol: explain_group_having_symbol(clause.symbol()),
576 op: clause.op(),
577 value: clause.value().clone(),
578 }
579}
580
581fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
582 match symbol {
583 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
584 slot_index: field_slot.index(),
585 field: field_slot.field().to_string(),
586 },
587 GroupHavingSymbol::AggregateIndex(index) => {
588 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
589 }
590 }
591}
592
593fn explain_scalar_inner<K>(
594 logical: &ScalarPlan,
595 grouping: ExplainGrouping,
596 model: Option<&EntityModel>,
597 access: &AccessPlan<K>,
598) -> ExplainPlan
599where
600 K: FieldValue,
601{
602 let predicate_model = logical.predicate.clone();
604 let predicate = match &predicate_model {
605 Some(predicate) => ExplainPredicate::from_predicate(predicate),
606 None => ExplainPredicate::None,
607 };
608
609 let order_by = explain_order(logical.order.as_ref());
611 let order_pushdown = explain_order_pushdown(model);
612 let page = explain_page(logical.page.as_ref());
613 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
614
615 ExplainPlan {
617 mode: logical.mode,
618 access: ExplainAccessPath::from_access_plan(access),
619 predicate,
620 predicate_model,
621 order_by,
622 distinct: logical.distinct,
623 grouping,
624 order_pushdown,
625 page,
626 delete_limit,
627 consistency: logical.consistency,
628 }
629}
630
631const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
632 let _ = model;
633
634 ExplainOrderPushdown::MissingModelContext
636}
637
638impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
639 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
640 Self::from(PushdownSurfaceEligibility::from(&value))
641 }
642}
643
644impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
645 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
646 match value {
647 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
648 Self::EligibleSecondaryIndex { index, prefix_len }
649 }
650 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
651 }
652 }
653}
654
655impl ExplainPredicate {
656 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
657 match predicate {
658 Predicate::True => Self::True,
659 Predicate::False => Self::False,
660 Predicate::And(children) => {
661 Self::And(children.iter().map(Self::from_predicate).collect())
662 }
663 Predicate::Or(children) => {
664 Self::Or(children.iter().map(Self::from_predicate).collect())
665 }
666 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
667 Predicate::Compare(compare) => Self::from_compare(compare),
668 Predicate::CompareFields(compare) => Self::CompareFields {
669 left_field: compare.left_field().to_string(),
670 op: compare.op(),
671 right_field: compare.right_field().to_string(),
672 coercion: compare.coercion().clone(),
673 },
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) if limit.offset == 0 => match limit.limit {
744 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
745 None => ExplainDeleteLimit::Window {
746 limit: None,
747 offset: 0,
748 },
749 },
750 Some(limit) => ExplainDeleteLimit::Window {
751 limit: limit.limit,
752 offset: limit.offset,
753 },
754 None => ExplainDeleteLimit::None,
755 }
756}
757
758fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
759 let mut object = JsonWriter::begin_object(out);
760 object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
761 object.field_with("access", |out| write_access_json(explain.access(), out));
762 object.field_value_debug("predicate", explain.predicate());
763 object.field_value_debug("order_by", explain.order_by());
764 object.field_bool("distinct", explain.distinct());
765 object.field_value_debug("grouping", explain.grouping());
766 object.field_value_debug("order_pushdown", explain.order_pushdown());
767 object.field_with("page", |out| write_pagination_json(explain.page(), out));
768 object.field_with("delete_limit", |out| {
769 write_delete_limit_json(explain.delete_limit(), out);
770 });
771 object.field_value_debug("consistency", &explain.consistency());
772 object.finish();
773}
774
775fn write_query_mode_json(mode: QueryMode, out: &mut String) {
776 let mut object = JsonWriter::begin_object(out);
777 match mode {
778 QueryMode::Load(spec) => {
779 object.field_str("type", "Load");
780 match spec.limit() {
781 Some(limit) => object.field_u64("limit", u64::from(limit)),
782 None => object.field_null("limit"),
783 }
784 object.field_u64("offset", u64::from(spec.offset()));
785 }
786 QueryMode::Delete(spec) => {
787 object.field_str("type", "Delete");
788 match spec.limit() {
789 Some(limit) => object.field_u64("limit", u64::from(limit)),
790 None => object.field_null("limit"),
791 }
792 }
793 }
794 object.finish();
795}
796
797fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
798 let mut object = JsonWriter::begin_object(out);
799 match page {
800 ExplainPagination::None => {
801 object.field_str("type", "None");
802 }
803 ExplainPagination::Page { limit, offset } => {
804 object.field_str("type", "Page");
805 match limit {
806 Some(limit) => object.field_u64("limit", u64::from(*limit)),
807 None => object.field_null("limit"),
808 }
809 object.field_u64("offset", u64::from(*offset));
810 }
811 }
812 object.finish();
813}
814
815fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
816 let mut object = JsonWriter::begin_object(out);
817 match limit {
818 ExplainDeleteLimit::None => {
819 object.field_str("type", "None");
820 }
821 ExplainDeleteLimit::Limit { max_rows } => {
822 object.field_str("type", "Limit");
823 object.field_u64("max_rows", u64::from(*max_rows));
824 }
825 ExplainDeleteLimit::Window { limit, offset } => {
826 object.field_str("type", "Window");
827 object.field_with("limit", |out| match limit {
828 Some(limit) => out.push_str(&limit.to_string()),
829 None => out.push_str("null"),
830 });
831 object.field_u64("offset", u64::from(*offset));
832 }
833 }
834 object.finish();
835}