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, GroupHavingExpr,
17 GroupHavingSpec, GroupHavingValueExpr, 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#[expect(clippy::large_enum_variant)]
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub enum ExplainGrouping {
184 None,
185 Grouped {
186 strategy: &'static str,
187 fallback_reason: Option<&'static str>,
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, Debug, Eq, PartialEq)]
203pub struct ExplainGroupField {
204 pub(crate) slot_index: usize,
205 pub(crate) field: String,
206}
207
208impl ExplainGroupField {
209 #[must_use]
211 pub const fn slot_index(&self) -> usize {
212 self.slot_index
213 }
214
215 #[must_use]
217 pub const fn field(&self) -> &str {
218 self.field.as_str()
219 }
220}
221
222#[derive(Clone, Debug, Eq, PartialEq)]
229pub struct ExplainGroupAggregate {
230 pub(crate) kind: AggregateKind,
231 pub(crate) target_field: Option<String>,
232 pub(crate) distinct: bool,
233}
234
235impl ExplainGroupAggregate {
236 #[must_use]
238 pub const fn kind(&self) -> AggregateKind {
239 self.kind
240 }
241
242 #[must_use]
244 pub fn target_field(&self) -> Option<&str> {
245 self.target_field.as_deref()
246 }
247
248 #[must_use]
250 pub const fn distinct(&self) -> bool {
251 self.distinct
252 }
253}
254
255#[derive(Clone, Debug, Eq, PartialEq)]
262pub struct ExplainGroupHaving {
263 pub(crate) expr: ExplainGroupHavingExpr,
264}
265
266impl ExplainGroupHaving {
267 #[must_use]
269 pub const fn expr(&self) -> &ExplainGroupHavingExpr {
270 &self.expr
271 }
272}
273
274#[derive(Clone, Debug, Eq, PartialEq)]
282pub enum ExplainGroupHavingExpr {
283 Compare {
284 left: ExplainGroupHavingValueExpr,
285 op: CompareOp,
286 right: ExplainGroupHavingValueExpr,
287 },
288 And(Vec<Self>),
289}
290
291#[derive(Clone, Debug, Eq, PartialEq)]
299pub enum ExplainGroupHavingValueExpr {
300 GroupField {
301 slot_index: usize,
302 field: String,
303 },
304 AggregateIndex {
305 index: usize,
306 },
307 Literal(Value),
308 FunctionCall {
309 function: String,
310 args: Vec<Self>,
311 },
312 Binary {
313 op: String,
314 left: Box<Self>,
315 right: Box<Self>,
316 },
317}
318
319#[derive(Clone, Debug, Eq, PartialEq)]
326pub enum ExplainOrderPushdown {
327 MissingModelContext,
328 EligibleSecondaryIndex {
329 index: &'static str,
330 prefix_len: usize,
331 },
332 Rejected(SecondaryOrderPushdownRejection),
333}
334
335#[derive(Clone, Debug, Eq, PartialEq)]
343pub enum ExplainAccessPath {
344 ByKey {
345 key: Value,
346 },
347 ByKeys {
348 keys: Vec<Value>,
349 },
350 KeyRange {
351 start: Value,
352 end: Value,
353 },
354 IndexPrefix {
355 name: &'static str,
356 fields: Vec<&'static str>,
357 prefix_len: usize,
358 values: Vec<Value>,
359 },
360 IndexMultiLookup {
361 name: &'static str,
362 fields: Vec<&'static str>,
363 values: Vec<Value>,
364 },
365 IndexRange {
366 name: &'static str,
367 fields: Vec<&'static str>,
368 prefix_len: usize,
369 prefix: Vec<Value>,
370 lower: Bound<Value>,
371 upper: Bound<Value>,
372 },
373 FullScan,
374 Union(Vec<Self>),
375 Intersection(Vec<Self>),
376}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
386pub enum ExplainPredicate {
387 None,
388 True,
389 False,
390 And(Vec<Self>),
391 Or(Vec<Self>),
392 Not(Box<Self>),
393 Compare {
394 field: String,
395 op: CompareOp,
396 value: Value,
397 coercion: CoercionSpec,
398 },
399 CompareFields {
400 left_field: String,
401 op: CompareOp,
402 right_field: String,
403 coercion: CoercionSpec,
404 },
405 IsNull {
406 field: String,
407 },
408 IsNotNull {
409 field: String,
410 },
411 IsMissing {
412 field: String,
413 },
414 IsEmpty {
415 field: String,
416 },
417 IsNotEmpty {
418 field: String,
419 },
420 TextContains {
421 field: String,
422 value: Value,
423 },
424 TextContainsCi {
425 field: String,
426 value: Value,
427 },
428}
429
430#[derive(Clone, Debug, Eq, PartialEq)]
437pub enum ExplainOrderBy {
438 None,
439 Fields(Vec<ExplainOrder>),
440}
441
442#[derive(Clone, Debug, Eq, PartialEq)]
449pub struct ExplainOrder {
450 pub(crate) field: String,
451 pub(crate) direction: OrderDirection,
452}
453
454impl ExplainOrder {
455 #[must_use]
457 pub const fn field(&self) -> &str {
458 self.field.as_str()
459 }
460
461 #[must_use]
463 pub const fn direction(&self) -> OrderDirection {
464 self.direction
465 }
466}
467
468#[derive(Clone, Debug, Eq, PartialEq)]
475pub enum ExplainPagination {
476 None,
477 Page { limit: Option<u32>, offset: u32 },
478}
479
480#[derive(Clone, Debug, Eq, PartialEq)]
487pub enum ExplainDeleteLimit {
488 None,
489 Limit { max_rows: u32 },
490 Window { limit: Option<u32>, offset: u32 },
491}
492
493impl AccessPlannedQuery {
494 #[must_use]
496 #[cfg(test)]
497 pub(crate) fn explain(&self) -> ExplainPlan {
498 self.explain_inner(None)
499 }
500
501 #[must_use]
507 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
508 self.explain_inner(Some(model))
509 }
510
511 pub(in crate::db::query::explain) fn explain_inner(
512 &self,
513 model: Option<&EntityModel>,
514 ) -> ExplainPlan {
515 let (logical, grouping) = match &self.logical {
517 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
518 LogicalPlan::Grouped(logical) => {
519 let grouped_strategy = grouped_plan_strategy(self).expect(
520 "grouped logical explain projection requires planner-owned grouped strategy",
521 );
522
523 (
524 &logical.scalar,
525 ExplainGrouping::Grouped {
526 strategy: grouped_strategy.code(),
527 fallback_reason: grouped_strategy
528 .fallback_reason()
529 .map(GroupedPlanFallbackReason::code),
530 group_fields: logical
531 .group
532 .group_fields
533 .iter()
534 .map(|field_slot| ExplainGroupField {
535 slot_index: field_slot.index(),
536 field: field_slot.field().to_string(),
537 })
538 .collect(),
539 aggregates: logical
540 .group
541 .aggregates
542 .iter()
543 .map(|aggregate| ExplainGroupAggregate {
544 kind: aggregate.kind,
545 target_field: aggregate.target_field.clone(),
546 distinct: aggregate.distinct,
547 })
548 .collect(),
549 having: explain_group_having(
550 logical.having.as_ref(),
551 logical.having_expr.as_ref(),
552 ),
553 max_groups: logical.group.execution.max_groups(),
554 max_group_bytes: logical.group.execution.max_group_bytes(),
555 },
556 )
557 }
558 };
559
560 explain_scalar_inner(logical, grouping, model, &self.access)
562 }
563}
564
565fn explain_group_having(
566 having: Option<&GroupHavingSpec>,
567 having_expr: Option<&GroupHavingExpr>,
568) -> Option<ExplainGroupHaving> {
569 let expr = match having_expr {
570 Some(expr) => explain_group_having_expr(expr),
571 None => explain_group_having_expr(&GroupHavingExpr::from_legacy_spec(having?)),
572 };
573
574 Some(ExplainGroupHaving { expr })
575}
576
577fn explain_group_having_expr(expr: &GroupHavingExpr) -> ExplainGroupHavingExpr {
578 match expr {
579 GroupHavingExpr::Compare { left, op, right } => ExplainGroupHavingExpr::Compare {
580 left: explain_group_having_value_expr(left),
581 op: *op,
582 right: explain_group_having_value_expr(right),
583 },
584 GroupHavingExpr::And(children) => {
585 ExplainGroupHavingExpr::And(children.iter().map(explain_group_having_expr).collect())
586 }
587 }
588}
589
590fn explain_group_having_value_expr(expr: &GroupHavingValueExpr) -> ExplainGroupHavingValueExpr {
591 match expr {
592 GroupHavingValueExpr::GroupField(field_slot) => ExplainGroupHavingValueExpr::GroupField {
593 slot_index: field_slot.index(),
594 field: field_slot.field().to_string(),
595 },
596 GroupHavingValueExpr::AggregateIndex(index) => {
597 ExplainGroupHavingValueExpr::AggregateIndex { index: *index }
598 }
599 GroupHavingValueExpr::Literal(value) => ExplainGroupHavingValueExpr::Literal(value.clone()),
600 GroupHavingValueExpr::FunctionCall { function, args } => {
601 ExplainGroupHavingValueExpr::FunctionCall {
602 function: function.sql_label().to_string(),
603 args: args.iter().map(explain_group_having_value_expr).collect(),
604 }
605 }
606 GroupHavingValueExpr::Binary { op, left, right } => ExplainGroupHavingValueExpr::Binary {
607 op: explain_group_having_binary_op_label(*op).to_string(),
608 left: Box::new(explain_group_having_value_expr(left)),
609 right: Box::new(explain_group_having_value_expr(right)),
610 },
611 }
612}
613
614const fn explain_group_having_binary_op_label(
615 op: crate::db::query::plan::expr::BinaryOp,
616) -> &'static str {
617 match op {
618 crate::db::query::plan::expr::BinaryOp::Add => "+",
619 crate::db::query::plan::expr::BinaryOp::Sub => "-",
620 crate::db::query::plan::expr::BinaryOp::Mul => "*",
621 crate::db::query::plan::expr::BinaryOp::Div => "/",
622 #[cfg(test)]
623 crate::db::query::plan::expr::BinaryOp::And => "AND",
624 #[cfg(test)]
625 crate::db::query::plan::expr::BinaryOp::Eq => "=",
626 }
627}
628
629fn explain_scalar_inner<K>(
630 logical: &ScalarPlan,
631 grouping: ExplainGrouping,
632 model: Option<&EntityModel>,
633 access: &AccessPlan<K>,
634) -> ExplainPlan
635where
636 K: FieldValue,
637{
638 let predicate_model = logical.predicate.clone();
640 let predicate = match &predicate_model {
641 Some(predicate) => ExplainPredicate::from_predicate(predicate),
642 None => ExplainPredicate::None,
643 };
644
645 let order_by = explain_order(logical.order.as_ref());
647 let order_pushdown = explain_order_pushdown(model);
648 let page = explain_page(logical.page.as_ref());
649 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
650
651 ExplainPlan {
653 mode: logical.mode,
654 access: ExplainAccessPath::from_access_plan(access),
655 predicate,
656 predicate_model,
657 order_by,
658 distinct: logical.distinct,
659 grouping,
660 order_pushdown,
661 page,
662 delete_limit,
663 consistency: logical.consistency,
664 }
665}
666
667const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
668 let _ = model;
669
670 ExplainOrderPushdown::MissingModelContext
672}
673
674impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
675 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
676 Self::from(PushdownSurfaceEligibility::from(&value))
677 }
678}
679
680impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
681 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
682 match value {
683 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
684 Self::EligibleSecondaryIndex { index, prefix_len }
685 }
686 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
687 }
688 }
689}
690
691impl ExplainPredicate {
692 pub(in crate::db) fn from_predicate(predicate: &Predicate) -> Self {
693 match predicate {
694 Predicate::True => Self::True,
695 Predicate::False => Self::False,
696 Predicate::And(children) => {
697 Self::And(children.iter().map(Self::from_predicate).collect())
698 }
699 Predicate::Or(children) => {
700 Self::Or(children.iter().map(Self::from_predicate).collect())
701 }
702 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
703 Predicate::Compare(compare) => Self::from_compare(compare),
704 Predicate::CompareFields(compare) => Self::CompareFields {
705 left_field: compare.left_field().to_string(),
706 op: compare.op(),
707 right_field: compare.right_field().to_string(),
708 coercion: compare.coercion().clone(),
709 },
710 Predicate::IsNull { field } => Self::IsNull {
711 field: field.clone(),
712 },
713 Predicate::IsNotNull { field } => Self::IsNotNull {
714 field: field.clone(),
715 },
716 Predicate::IsMissing { field } => Self::IsMissing {
717 field: field.clone(),
718 },
719 Predicate::IsEmpty { field } => Self::IsEmpty {
720 field: field.clone(),
721 },
722 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
723 field: field.clone(),
724 },
725 Predicate::TextContains { field, value } => Self::TextContains {
726 field: field.clone(),
727 value: value.clone(),
728 },
729 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
730 field: field.clone(),
731 value: value.clone(),
732 },
733 }
734 }
735
736 fn from_compare(compare: &ComparePredicate) -> Self {
737 Self::Compare {
738 field: compare.field.clone(),
739 op: compare.op,
740 value: compare.value.clone(),
741 coercion: compare.coercion.clone(),
742 }
743 }
744}
745
746fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
747 let Some(order) = order else {
748 return ExplainOrderBy::None;
749 };
750
751 if order.fields.is_empty() {
752 return ExplainOrderBy::None;
753 }
754
755 ExplainOrderBy::Fields(
756 order
757 .fields
758 .iter()
759 .map(|(field, direction)| ExplainOrder {
760 field: field.clone(),
761 direction: *direction,
762 })
763 .collect(),
764 )
765}
766
767const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
768 match page {
769 Some(page) => ExplainPagination::Page {
770 limit: page.limit,
771 offset: page.offset,
772 },
773 None => ExplainPagination::None,
774 }
775}
776
777const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
778 match limit {
779 Some(limit) if limit.offset == 0 => match limit.limit {
780 Some(max_rows) => ExplainDeleteLimit::Limit { max_rows },
781 None => ExplainDeleteLimit::Window {
782 limit: None,
783 offset: 0,
784 },
785 },
786 Some(limit) => ExplainDeleteLimit::Window {
787 limit: limit.limit,
788 offset: limit.offset,
789 },
790 None => ExplainDeleteLimit::None,
791 }
792}
793
794fn write_logical_explain_json(explain: &ExplainPlan, out: &mut String) {
795 let mut object = JsonWriter::begin_object(out);
796 object.field_with("mode", |out| write_query_mode_json(explain.mode(), out));
797 object.field_with("access", |out| write_access_json(explain.access(), out));
798 object.field_value_debug("predicate", explain.predicate());
799 object.field_value_debug("order_by", explain.order_by());
800 object.field_bool("distinct", explain.distinct());
801 object.field_value_debug("grouping", explain.grouping());
802 object.field_value_debug("order_pushdown", explain.order_pushdown());
803 object.field_with("page", |out| write_pagination_json(explain.page(), out));
804 object.field_with("delete_limit", |out| {
805 write_delete_limit_json(explain.delete_limit(), out);
806 });
807 object.field_value_debug("consistency", &explain.consistency());
808 object.finish();
809}
810
811fn write_query_mode_json(mode: QueryMode, out: &mut String) {
812 let mut object = JsonWriter::begin_object(out);
813 match mode {
814 QueryMode::Load(spec) => {
815 object.field_str("type", "Load");
816 match spec.limit() {
817 Some(limit) => object.field_u64("limit", u64::from(limit)),
818 None => object.field_null("limit"),
819 }
820 object.field_u64("offset", u64::from(spec.offset()));
821 }
822 QueryMode::Delete(spec) => {
823 object.field_str("type", "Delete");
824 match spec.limit() {
825 Some(limit) => object.field_u64("limit", u64::from(limit)),
826 None => object.field_null("limit"),
827 }
828 }
829 }
830 object.finish();
831}
832
833fn write_pagination_json(page: &ExplainPagination, out: &mut String) {
834 let mut object = JsonWriter::begin_object(out);
835 match page {
836 ExplainPagination::None => {
837 object.field_str("type", "None");
838 }
839 ExplainPagination::Page { limit, offset } => {
840 object.field_str("type", "Page");
841 match limit {
842 Some(limit) => object.field_u64("limit", u64::from(*limit)),
843 None => object.field_null("limit"),
844 }
845 object.field_u64("offset", u64::from(*offset));
846 }
847 }
848 object.finish();
849}
850
851fn write_delete_limit_json(limit: &ExplainDeleteLimit, out: &mut String) {
852 let mut object = JsonWriter::begin_object(out);
853 match limit {
854 ExplainDeleteLimit::None => {
855 object.field_str("type", "None");
856 }
857 ExplainDeleteLimit::Limit { max_rows } => {
858 object.field_str("type", "Limit");
859 object.field_u64("max_rows", u64::from(*max_rows));
860 }
861 ExplainDeleteLimit::Window { limit, offset } => {
862 object.field_str("type", "Window");
863 object.field_with("limit", |out| match limit {
864 Some(limit) => out.push_str(&limit.to_string()),
865 None => out.push_str("null"),
866 });
867 object.field_u64("offset", u64::from(*offset));
868 }
869 }
870 object.finish();
871}