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 access::{AccessPathVisitor, visit_explain_access_path},
17 plan::{
18 AccessPlanProjection, AccessPlannedQuery, AggregateKind, DeleteLimitSpec,
19 GroupHavingClause, GroupHavingSpec, GroupHavingSymbol, GroupedPlanStrategyHint,
20 LogicalPlan, OrderDirection, OrderSpec, PageSpec, QueryMode, ScalarPlan,
21 grouped_plan_strategy_hint_for_plan, project_access_plan,
22 },
23 },
24 },
25 model::entity::EntityModel,
26 traits::FieldValue,
27 value::Value,
28};
29use std::{collections::BTreeMap, fmt::Write, 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
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum ExplainAggregateTerminalRoute {
60 Standard,
61 IndexSeekFirst { fetch: usize },
62 IndexSeekLast { fetch: usize },
63}
64
65#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct ExplainAggregateTerminalPlan {
73 pub(crate) query: ExplainPlan,
74 pub(crate) terminal: AggregateKind,
75 pub(crate) route: ExplainAggregateTerminalRoute,
76 pub(crate) execution: ExplainExecutionDescriptor,
77}
78
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
86pub enum ExplainExecutionOrderingSource {
87 AccessOrder,
88 Materialized,
89 IndexSeekFirst { fetch: usize },
90 IndexSeekLast { fetch: usize },
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
99pub enum ExplainExecutionMode {
100 Streaming,
101 Materialized,
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
112pub struct ExplainExecutionDescriptor {
113 pub(crate) access_strategy: ExplainAccessPath,
114 pub(crate) covering_projection: bool,
115 pub(crate) aggregation: AggregateKind,
116 pub(crate) execution_mode: ExplainExecutionMode,
117 pub(crate) ordering_source: ExplainExecutionOrderingSource,
118 pub(crate) limit: Option<u32>,
119 pub(crate) cursor: bool,
120 pub(crate) node_properties: BTreeMap<String, Value>,
121}
122
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub enum ExplainExecutionNodeType {
130 ByKeyLookup,
131 ByKeysLookup,
132 PrimaryKeyRangeScan,
133 IndexPrefixScan,
134 IndexRangeScan,
135 IndexMultiLookup,
136 FullScan,
137 Union,
138 Intersection,
139 IndexPredicatePrefilter,
140 ResidualPredicateFilter,
141 OrderByAccessSatisfied,
142 OrderByMaterializedSort,
143 DistinctPreOrdered,
144 DistinctMaterialized,
145 ProjectionMaterialized,
146 ProjectionIndexOnly,
147 LimitOffset,
148 CursorResume,
149 IndexRangeLimitPushdown,
150 TopNSeek,
151 AggregateCount,
152 AggregateExists,
153 AggregateMin,
154 AggregateMax,
155 AggregateFirst,
156 AggregateLast,
157 AggregateSum,
158 AggregateSeekFirst,
159 AggregateSeekLast,
160 GroupedAggregateHashMaterialized,
161 GroupedAggregateOrderedMaterialized,
162 SecondaryOrderPushdown,
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
172pub struct ExplainExecutionNodeDescriptor {
173 pub(crate) node_type: ExplainExecutionNodeType,
174 pub(crate) execution_mode: ExplainExecutionMode,
175 pub(crate) access_strategy: Option<ExplainAccessPath>,
176 pub(crate) predicate_pushdown: Option<String>,
177 pub(crate) residual_predicate: Option<ExplainPredicate>,
178 pub(crate) projection: Option<String>,
179 pub(crate) ordering_source: Option<ExplainExecutionOrderingSource>,
180 pub(crate) limit: Option<u32>,
181 pub(crate) cursor: Option<bool>,
182 pub(crate) covering_scan: Option<bool>,
183 pub(crate) rows_expected: Option<u64>,
184 pub(crate) children: Vec<Self>,
185 pub(crate) node_properties: BTreeMap<String, Value>,
186}
187
188impl ExplainPlan {
189 #[must_use]
191 pub const fn mode(&self) -> QueryMode {
192 self.mode
193 }
194
195 #[must_use]
197 pub const fn access(&self) -> &ExplainAccessPath {
198 &self.access
199 }
200
201 #[must_use]
203 pub const fn predicate(&self) -> &ExplainPredicate {
204 &self.predicate
205 }
206
207 #[must_use]
209 pub const fn order_by(&self) -> &ExplainOrderBy {
210 &self.order_by
211 }
212
213 #[must_use]
215 pub const fn distinct(&self) -> bool {
216 self.distinct
217 }
218
219 #[must_use]
221 pub const fn grouping(&self) -> &ExplainGrouping {
222 &self.grouping
223 }
224
225 #[must_use]
227 pub const fn order_pushdown(&self) -> &ExplainOrderPushdown {
228 &self.order_pushdown
229 }
230
231 #[must_use]
233 pub const fn page(&self) -> &ExplainPagination {
234 &self.page
235 }
236
237 #[must_use]
239 pub const fn delete_limit(&self) -> &ExplainDeleteLimit {
240 &self.delete_limit
241 }
242
243 #[must_use]
245 pub const fn consistency(&self) -> MissingRowPolicy {
246 self.consistency
247 }
248}
249
250impl ExplainAggregateTerminalPlan {
251 #[must_use]
253 pub const fn query(&self) -> &ExplainPlan {
254 &self.query
255 }
256
257 #[must_use]
259 pub const fn terminal(&self) -> AggregateKind {
260 self.terminal
261 }
262
263 #[must_use]
265 pub const fn route(&self) -> ExplainAggregateTerminalRoute {
266 self.route
267 }
268
269 #[must_use]
271 pub const fn execution(&self) -> &ExplainExecutionDescriptor {
272 &self.execution
273 }
274
275 #[must_use]
276 pub(in crate::db) const fn new(
277 query: ExplainPlan,
278 terminal: AggregateKind,
279 execution: ExplainExecutionDescriptor,
280 ) -> Self {
281 let route = execution.route();
282
283 Self {
284 query,
285 terminal,
286 route,
287 execution,
288 }
289 }
290}
291
292impl ExplainExecutionDescriptor {
293 #[must_use]
295 pub const fn access_strategy(&self) -> &ExplainAccessPath {
296 &self.access_strategy
297 }
298
299 #[must_use]
301 pub const fn covering_projection(&self) -> bool {
302 self.covering_projection
303 }
304
305 #[must_use]
307 pub const fn aggregation(&self) -> AggregateKind {
308 self.aggregation
309 }
310
311 #[must_use]
313 pub const fn execution_mode(&self) -> ExplainExecutionMode {
314 self.execution_mode
315 }
316
317 #[must_use]
319 pub const fn ordering_source(&self) -> ExplainExecutionOrderingSource {
320 self.ordering_source
321 }
322
323 #[must_use]
325 pub const fn limit(&self) -> Option<u32> {
326 self.limit
327 }
328
329 #[must_use]
331 pub const fn cursor(&self) -> bool {
332 self.cursor
333 }
334
335 #[must_use]
337 pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
338 &self.node_properties
339 }
340
341 #[must_use]
342 pub(in crate::db) const fn route(&self) -> ExplainAggregateTerminalRoute {
343 match self.ordering_source {
344 ExplainExecutionOrderingSource::IndexSeekFirst { fetch } => {
345 ExplainAggregateTerminalRoute::IndexSeekFirst { fetch }
346 }
347 ExplainExecutionOrderingSource::IndexSeekLast { fetch } => {
348 ExplainAggregateTerminalRoute::IndexSeekLast { fetch }
349 }
350 ExplainExecutionOrderingSource::AccessOrder
351 | ExplainExecutionOrderingSource::Materialized => {
352 ExplainAggregateTerminalRoute::Standard
353 }
354 }
355 }
356}
357
358impl ExplainAggregateTerminalPlan {
359 #[must_use]
360 pub fn execution_node_descriptor(&self) -> ExplainExecutionNodeDescriptor {
361 ExplainExecutionNodeDescriptor {
362 node_type: aggregate_execution_node_type(self.terminal, self.execution.ordering_source),
363 execution_mode: self.execution.execution_mode,
364 access_strategy: Some(self.execution.access_strategy.clone()),
365 predicate_pushdown: None,
366 residual_predicate: None,
367 projection: None,
368 ordering_source: Some(self.execution.ordering_source),
369 limit: self.execution.limit,
370 cursor: Some(self.execution.cursor),
371 covering_scan: Some(self.execution.covering_projection),
372 rows_expected: None,
373 children: Vec::new(),
374 node_properties: self.execution.node_properties.clone(),
375 }
376 }
377}
378
379const fn aggregate_execution_node_type(
380 terminal: AggregateKind,
381 ordering_source: ExplainExecutionOrderingSource,
382) -> ExplainExecutionNodeType {
383 match ordering_source {
384 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => {
385 ExplainExecutionNodeType::AggregateSeekFirst
386 }
387 ExplainExecutionOrderingSource::IndexSeekLast { .. } => {
388 ExplainExecutionNodeType::AggregateSeekLast
389 }
390 ExplainExecutionOrderingSource::AccessOrder
391 | ExplainExecutionOrderingSource::Materialized => match terminal {
392 AggregateKind::Count => ExplainExecutionNodeType::AggregateCount,
393 AggregateKind::Exists => ExplainExecutionNodeType::AggregateExists,
394 AggregateKind::Min => ExplainExecutionNodeType::AggregateMin,
395 AggregateKind::Max => ExplainExecutionNodeType::AggregateMax,
396 AggregateKind::First => ExplainExecutionNodeType::AggregateFirst,
397 AggregateKind::Last => ExplainExecutionNodeType::AggregateLast,
398 AggregateKind::Sum => ExplainExecutionNodeType::AggregateSum,
399 },
400 }
401}
402
403impl ExplainExecutionNodeType {
404 #[must_use]
405 pub const fn as_str(self) -> &'static str {
406 match self {
407 Self::ByKeyLookup => "ByKeyLookup",
408 Self::ByKeysLookup => "ByKeysLookup",
409 Self::PrimaryKeyRangeScan => "PrimaryKeyRangeScan",
410 Self::IndexPrefixScan => "IndexPrefixScan",
411 Self::IndexRangeScan => "IndexRangeScan",
412 Self::IndexMultiLookup => "IndexMultiLookup",
413 Self::FullScan => "FullScan",
414 Self::Union => "Union",
415 Self::Intersection => "Intersection",
416 Self::IndexPredicatePrefilter => "IndexPredicatePrefilter",
417 Self::ResidualPredicateFilter => "ResidualPredicateFilter",
418 Self::OrderByAccessSatisfied => "OrderByAccessSatisfied",
419 Self::OrderByMaterializedSort => "OrderByMaterializedSort",
420 Self::DistinctPreOrdered => "DistinctPreOrdered",
421 Self::DistinctMaterialized => "DistinctMaterialized",
422 Self::ProjectionMaterialized => "ProjectionMaterialized",
423 Self::ProjectionIndexOnly => "ProjectionIndexOnly",
424 Self::LimitOffset => "LimitOffset",
425 Self::CursorResume => "CursorResume",
426 Self::IndexRangeLimitPushdown => "IndexRangeLimitPushdown",
427 Self::TopNSeek => "TopNSeek",
428 Self::AggregateCount => "AggregateCount",
429 Self::AggregateExists => "AggregateExists",
430 Self::AggregateMin => "AggregateMin",
431 Self::AggregateMax => "AggregateMax",
432 Self::AggregateFirst => "AggregateFirst",
433 Self::AggregateLast => "AggregateLast",
434 Self::AggregateSum => "AggregateSum",
435 Self::AggregateSeekFirst => "AggregateSeekFirst",
436 Self::AggregateSeekLast => "AggregateSeekLast",
437 Self::GroupedAggregateHashMaterialized => "GroupedAggregateHashMaterialized",
438 Self::GroupedAggregateOrderedMaterialized => "GroupedAggregateOrderedMaterialized",
439 Self::SecondaryOrderPushdown => "SecondaryOrderPushdown",
440 }
441 }
442}
443
444impl ExplainExecutionNodeDescriptor {
445 #[must_use]
447 pub const fn node_type(&self) -> ExplainExecutionNodeType {
448 self.node_type
449 }
450
451 #[must_use]
453 pub const fn execution_mode(&self) -> ExplainExecutionMode {
454 self.execution_mode
455 }
456
457 #[must_use]
459 pub const fn access_strategy(&self) -> Option<&ExplainAccessPath> {
460 self.access_strategy.as_ref()
461 }
462
463 #[must_use]
465 pub fn predicate_pushdown(&self) -> Option<&str> {
466 self.predicate_pushdown.as_deref()
467 }
468
469 #[must_use]
471 pub const fn residual_predicate(&self) -> Option<&ExplainPredicate> {
472 self.residual_predicate.as_ref()
473 }
474
475 #[must_use]
477 pub fn projection(&self) -> Option<&str> {
478 self.projection.as_deref()
479 }
480
481 #[must_use]
483 pub const fn ordering_source(&self) -> Option<ExplainExecutionOrderingSource> {
484 self.ordering_source
485 }
486
487 #[must_use]
489 pub const fn limit(&self) -> Option<u32> {
490 self.limit
491 }
492
493 #[must_use]
495 pub const fn cursor(&self) -> Option<bool> {
496 self.cursor
497 }
498
499 #[must_use]
501 pub const fn covering_scan(&self) -> Option<bool> {
502 self.covering_scan
503 }
504
505 #[must_use]
507 pub const fn rows_expected(&self) -> Option<u64> {
508 self.rows_expected
509 }
510
511 #[must_use]
513 pub const fn children(&self) -> &[Self] {
514 self.children.as_slice()
515 }
516
517 #[must_use]
519 pub const fn node_properties(&self) -> &BTreeMap<String, Value> {
520 &self.node_properties
521 }
522
523 #[must_use]
524 pub fn render_text_tree(&self) -> String {
525 let mut lines = Vec::new();
526 self.render_text_tree_into(0, &mut lines);
527 lines.join("\n")
528 }
529
530 #[must_use]
531 pub fn render_json_canonical(&self) -> String {
532 let mut out = String::new();
533 write_execution_node_json(self, &mut out);
534 out
535 }
536
537 #[must_use]
538 pub fn render_text_tree_verbose(&self) -> String {
539 let mut lines = Vec::new();
540 self.render_text_tree_verbose_into(0, &mut lines);
541 lines.join("\n")
542 }
543
544 fn render_text_tree_into(&self, depth: usize, lines: &mut Vec<String>) {
545 let mut line = format!(
546 "{}{} execution_mode={}",
547 " ".repeat(depth),
548 self.node_type.as_str(),
549 execution_mode_label(self.execution_mode)
550 );
551
552 if let Some(access_strategy) = self.access_strategy.as_ref() {
553 let _ = write!(line, " access={}", access_strategy_label(access_strategy));
554 }
555 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
556 let _ = write!(line, " predicate_pushdown={predicate_pushdown}");
557 }
558 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
559 let _ = write!(line, " residual_predicate={residual_predicate:?}");
560 }
561 if let Some(projection) = self.projection.as_ref() {
562 let _ = write!(line, " projection={projection}");
563 }
564 if let Some(ordering_source) = self.ordering_source {
565 let _ = write!(
566 line,
567 " ordering_source={}",
568 ordering_source_label(ordering_source)
569 );
570 }
571 if let Some(limit) = self.limit {
572 let _ = write!(line, " limit={limit}");
573 }
574 if let Some(cursor) = self.cursor {
575 let _ = write!(line, " cursor={cursor}");
576 }
577 if let Some(covering_scan) = self.covering_scan {
578 let _ = write!(line, " covering_scan={covering_scan}");
579 }
580 if let Some(rows_expected) = self.rows_expected {
581 let _ = write!(line, " rows_expected={rows_expected}");
582 }
583 if !self.node_properties.is_empty() {
584 let _ = write!(
585 line,
586 " node_properties={}",
587 render_node_properties(&self.node_properties)
588 );
589 }
590
591 lines.push(line);
592
593 for child in &self.children {
594 child.render_text_tree_into(depth.saturating_add(1), lines);
595 }
596 }
597
598 fn render_text_tree_verbose_into(&self, depth: usize, lines: &mut Vec<String>) {
599 let node_indent = " ".repeat(depth);
601 let field_indent = " ".repeat(depth.saturating_add(1));
602 lines.push(format!(
603 "{}{} execution_mode={}",
604 node_indent,
605 self.node_type.as_str(),
606 execution_mode_label(self.execution_mode)
607 ));
608
609 if let Some(access_strategy) = self.access_strategy.as_ref() {
611 lines.push(format!("{field_indent}access_strategy={access_strategy:?}"));
612 }
613 if let Some(predicate_pushdown) = self.predicate_pushdown.as_ref() {
614 lines.push(format!(
615 "{field_indent}predicate_pushdown={predicate_pushdown}"
616 ));
617 }
618 if let Some(residual_predicate) = self.residual_predicate.as_ref() {
619 lines.push(format!(
620 "{field_indent}residual_predicate={residual_predicate:?}"
621 ));
622 }
623 if let Some(projection) = self.projection.as_ref() {
624 lines.push(format!("{field_indent}projection={projection}"));
625 }
626 if let Some(ordering_source) = self.ordering_source {
627 lines.push(format!(
628 "{}ordering_source={}",
629 field_indent,
630 ordering_source_label(ordering_source)
631 ));
632 }
633 if let Some(limit) = self.limit {
634 lines.push(format!("{field_indent}limit={limit}"));
635 }
636 if let Some(cursor) = self.cursor {
637 lines.push(format!("{field_indent}cursor={cursor}"));
638 }
639 if let Some(covering_scan) = self.covering_scan {
640 lines.push(format!("{field_indent}covering_scan={covering_scan}"));
641 }
642 if let Some(rows_expected) = self.rows_expected {
643 lines.push(format!("{field_indent}rows_expected={rows_expected}"));
644 }
645 if !self.node_properties.is_empty() {
646 lines.push(format!(
647 "{}node_properties={}",
648 field_indent,
649 render_node_properties(&self.node_properties)
650 ));
651 }
652
653 for child in &self.children {
655 child.render_text_tree_verbose_into(depth.saturating_add(1), lines);
656 }
657 }
658}
659
660const fn execution_mode_label(mode: ExplainExecutionMode) -> &'static str {
661 match mode {
662 ExplainExecutionMode::Streaming => "Streaming",
663 ExplainExecutionMode::Materialized => "Materialized",
664 }
665}
666
667fn render_node_properties(node_properties: &BTreeMap<String, Value>) -> String {
668 let mut rendered = String::new();
669 let mut first = true;
670 for (key, value) in node_properties {
671 if first {
672 first = false;
673 } else {
674 rendered.push(',');
675 }
676 let _ = write!(rendered, "{key}={value:?}");
677 }
678 rendered
679}
680
681fn write_execution_node_json(node: &ExplainExecutionNodeDescriptor, out: &mut String) {
682 out.push('{');
683
684 write_json_field_name(out, "node_type");
685 write_json_string(out, node.node_type.as_str());
686 out.push(',');
687
688 write_json_field_name(out, "execution_mode");
689 write_json_string(out, execution_mode_label(node.execution_mode));
690 out.push(',');
691
692 write_json_field_name(out, "access_strategy");
693 match node.access_strategy.as_ref() {
694 Some(access) => write_access_json(access, out),
695 None => out.push_str("null"),
696 }
697 out.push(',');
698
699 write_json_field_name(out, "predicate_pushdown");
700 match node.predicate_pushdown.as_ref() {
701 Some(predicate_pushdown) => write_json_string(out, predicate_pushdown),
702 None => out.push_str("null"),
703 }
704 out.push(',');
705
706 write_json_field_name(out, "residual_predicate");
707 match node.residual_predicate.as_ref() {
708 Some(residual_predicate) => write_json_string(out, &format!("{residual_predicate:?}")),
709 None => out.push_str("null"),
710 }
711 out.push(',');
712
713 write_json_field_name(out, "projection");
714 match node.projection.as_ref() {
715 Some(projection) => write_json_string(out, projection),
716 None => out.push_str("null"),
717 }
718 out.push(',');
719
720 write_json_field_name(out, "ordering_source");
721 match node.ordering_source {
722 Some(ordering_source) => write_json_string(out, ordering_source_label(ordering_source)),
723 None => out.push_str("null"),
724 }
725 out.push(',');
726
727 write_json_field_name(out, "limit");
728 match node.limit {
729 Some(limit) => out.push_str(&limit.to_string()),
730 None => out.push_str("null"),
731 }
732 out.push(',');
733
734 write_json_field_name(out, "cursor");
735 match node.cursor {
736 Some(cursor) => out.push_str(if cursor { "true" } else { "false" }),
737 None => out.push_str("null"),
738 }
739 out.push(',');
740
741 write_json_field_name(out, "covering_scan");
742 match node.covering_scan {
743 Some(covering_scan) => out.push_str(if covering_scan { "true" } else { "false" }),
744 None => out.push_str("null"),
745 }
746 out.push(',');
747
748 write_json_field_name(out, "rows_expected");
749 match node.rows_expected {
750 Some(rows_expected) => out.push_str(&rows_expected.to_string()),
751 None => out.push_str("null"),
752 }
753 out.push(',');
754
755 write_json_field_name(out, "children");
756 out.push('[');
757 for (index, child) in node.children.iter().enumerate() {
758 if index > 0 {
759 out.push(',');
760 }
761 write_execution_node_json(child, out);
762 }
763 out.push(']');
764 out.push(',');
765
766 write_json_field_name(out, "node_properties");
767 write_node_properties_json(&node.node_properties, out);
768
769 out.push('}');
770}
771
772struct ExplainJsonVisitor<'a> {
779 out: &'a mut String,
780}
781
782impl AccessPathVisitor<()> for ExplainJsonVisitor<'_> {
783 fn visit_by_key(&mut self, key: &Value) {
784 self.out.push('{');
785 write_json_field_name(self.out, "type");
786 write_json_string(self.out, "ByKey");
787 self.out.push(',');
788 write_json_field_name(self.out, "key");
789 write_json_string(self.out, &format!("{key:?}"));
790 self.out.push('}');
791 }
792
793 fn visit_by_keys(&mut self, keys: &[Value]) {
794 self.out.push('{');
795 write_json_field_name(self.out, "type");
796 write_json_string(self.out, "ByKeys");
797 self.out.push(',');
798 write_json_field_name(self.out, "keys");
799 write_value_vec_as_debug_json(keys, self.out);
800 self.out.push('}');
801 }
802
803 fn visit_key_range(&mut self, start: &Value, end: &Value) {
804 self.out.push('{');
805 write_json_field_name(self.out, "type");
806 write_json_string(self.out, "KeyRange");
807 self.out.push(',');
808 write_json_field_name(self.out, "start");
809 write_json_string(self.out, &format!("{start:?}"));
810 self.out.push(',');
811 write_json_field_name(self.out, "end");
812 write_json_string(self.out, &format!("{end:?}"));
813 self.out.push('}');
814 }
815
816 fn visit_index_prefix(
817 &mut self,
818 name: &'static str,
819 fields: &[&'static str],
820 prefix_len: usize,
821 values: &[Value],
822 ) {
823 self.out.push('{');
824 write_json_field_name(self.out, "type");
825 write_json_string(self.out, "IndexPrefix");
826 self.out.push(',');
827 write_json_field_name(self.out, "name");
828 write_json_string(self.out, name);
829 self.out.push(',');
830 write_json_field_name(self.out, "fields");
831 write_str_vec_json(fields, self.out);
832 self.out.push(',');
833 write_json_field_name(self.out, "prefix_len");
834 self.out.push_str(&prefix_len.to_string());
835 self.out.push(',');
836 write_json_field_name(self.out, "values");
837 write_value_vec_as_debug_json(values, self.out);
838 self.out.push('}');
839 }
840
841 fn visit_index_multi_lookup(
842 &mut self,
843 name: &'static str,
844 fields: &[&'static str],
845 values: &[Value],
846 ) {
847 self.out.push('{');
848 write_json_field_name(self.out, "type");
849 write_json_string(self.out, "IndexMultiLookup");
850 self.out.push(',');
851 write_json_field_name(self.out, "name");
852 write_json_string(self.out, name);
853 self.out.push(',');
854 write_json_field_name(self.out, "fields");
855 write_str_vec_json(fields, self.out);
856 self.out.push(',');
857 write_json_field_name(self.out, "values");
858 write_value_vec_as_debug_json(values, self.out);
859 self.out.push('}');
860 }
861
862 fn visit_index_range(
863 &mut self,
864 name: &'static str,
865 fields: &[&'static str],
866 prefix_len: usize,
867 prefix: &[Value],
868 lower: &Bound<Value>,
869 upper: &Bound<Value>,
870 ) {
871 self.out.push('{');
872 write_json_field_name(self.out, "type");
873 write_json_string(self.out, "IndexRange");
874 self.out.push(',');
875 write_json_field_name(self.out, "name");
876 write_json_string(self.out, name);
877 self.out.push(',');
878 write_json_field_name(self.out, "fields");
879 write_str_vec_json(fields, self.out);
880 self.out.push(',');
881 write_json_field_name(self.out, "prefix_len");
882 self.out.push_str(&prefix_len.to_string());
883 self.out.push(',');
884 write_json_field_name(self.out, "prefix");
885 write_value_vec_as_debug_json(prefix, self.out);
886 self.out.push(',');
887 write_json_field_name(self.out, "lower");
888 write_json_string(self.out, &format!("{lower:?}"));
889 self.out.push(',');
890 write_json_field_name(self.out, "upper");
891 write_json_string(self.out, &format!("{upper:?}"));
892 self.out.push('}');
893 }
894
895 fn visit_full_scan(&mut self) {
896 self.out.push('{');
897 write_json_field_name(self.out, "type");
898 write_json_string(self.out, "FullScan");
899 self.out.push('}');
900 }
901
902 fn visit_union(&mut self, children: &[ExplainAccessPath]) {
903 self.out.push('{');
904 write_json_field_name(self.out, "type");
905 write_json_string(self.out, "Union");
906 self.out.push(',');
907 write_json_field_name(self.out, "children");
908 self.out.push('[');
909 for (index, child) in children.iter().enumerate() {
910 if index > 0 {
911 self.out.push(',');
912 }
913 visit_explain_access_path(child, self);
914 }
915 self.out.push(']');
916 self.out.push('}');
917 }
918
919 fn visit_intersection(&mut self, children: &[ExplainAccessPath]) {
920 self.out.push('{');
921 write_json_field_name(self.out, "type");
922 write_json_string(self.out, "Intersection");
923 self.out.push(',');
924 write_json_field_name(self.out, "children");
925 self.out.push('[');
926 for (index, child) in children.iter().enumerate() {
927 if index > 0 {
928 self.out.push(',');
929 }
930 visit_explain_access_path(child, self);
931 }
932 self.out.push(']');
933 self.out.push('}');
934 }
935}
936
937fn write_access_json(access: &ExplainAccessPath, out: &mut String) {
938 let mut visitor = ExplainJsonVisitor { out };
939 visit_explain_access_path(access, &mut visitor);
940}
941
942fn write_node_properties_json(node_properties: &BTreeMap<String, Value>, out: &mut String) {
943 out.push('{');
944 for (index, (key, value)) in node_properties.iter().enumerate() {
945 if index > 0 {
946 out.push(',');
947 }
948 write_json_field_name(out, key);
949 write_json_string(out, &format!("{value:?}"));
950 }
951 out.push('}');
952}
953
954fn write_value_vec_as_debug_json(values: &[Value], out: &mut String) {
955 out.push('[');
956 for (index, value) in values.iter().enumerate() {
957 if index > 0 {
958 out.push(',');
959 }
960 write_json_string(out, &format!("{value:?}"));
961 }
962 out.push(']');
963}
964
965fn write_str_vec_json(values: &[&str], out: &mut String) {
966 out.push('[');
967 for (index, value) in values.iter().enumerate() {
968 if index > 0 {
969 out.push(',');
970 }
971 write_json_string(out, value);
972 }
973 out.push(']');
974}
975
976fn write_json_field_name(out: &mut String, key: &str) {
977 write_json_string(out, key);
978 out.push(':');
979}
980
981fn write_json_string(out: &mut String, value: &str) {
982 out.push('"');
983 for ch in value.chars() {
984 match ch {
985 '"' => out.push_str("\\\""),
986 '\\' => out.push_str("\\\\"),
987 '\n' => out.push_str("\\n"),
988 '\r' => out.push_str("\\r"),
989 '\t' => out.push_str("\\t"),
990 '\u{08}' => out.push_str("\\b"),
991 '\u{0C}' => out.push_str("\\f"),
992 _ => out.push(ch),
993 }
994 }
995 out.push('"');
996}
997
998fn access_strategy_label(access: &ExplainAccessPath) -> String {
999 struct ExplainLabelVisitor;
1000
1001 impl AccessPathVisitor<String> for ExplainLabelVisitor {
1002 fn visit_by_key(&mut self, _key: &Value) -> String {
1003 "ByKey".to_string()
1004 }
1005
1006 fn visit_by_keys(&mut self, _keys: &[Value]) -> String {
1007 "ByKeys".to_string()
1008 }
1009
1010 fn visit_key_range(&mut self, _start: &Value, _end: &Value) -> String {
1011 "KeyRange".to_string()
1012 }
1013
1014 fn visit_index_prefix(
1015 &mut self,
1016 name: &'static str,
1017 _fields: &[&'static str],
1018 _prefix_len: usize,
1019 _values: &[Value],
1020 ) -> String {
1021 format!("IndexPrefix({name})")
1022 }
1023
1024 fn visit_index_multi_lookup(
1025 &mut self,
1026 name: &'static str,
1027 _fields: &[&'static str],
1028 _values: &[Value],
1029 ) -> String {
1030 format!("IndexMultiLookup({name})")
1031 }
1032
1033 fn visit_index_range(
1034 &mut self,
1035 name: &'static str,
1036 _fields: &[&'static str],
1037 _prefix_len: usize,
1038 _prefix: &[Value],
1039 _lower: &Bound<Value>,
1040 _upper: &Bound<Value>,
1041 ) -> String {
1042 format!("IndexRange({name})")
1043 }
1044
1045 fn visit_full_scan(&mut self) -> String {
1046 "FullScan".to_string()
1047 }
1048
1049 fn visit_union(&mut self, children: &[ExplainAccessPath]) -> String {
1050 format!("Union({})", children.len())
1051 }
1052
1053 fn visit_intersection(&mut self, children: &[ExplainAccessPath]) -> String {
1054 format!("Intersection({})", children.len())
1055 }
1056 }
1057
1058 let mut visitor = ExplainLabelVisitor;
1059 visit_explain_access_path(access, &mut visitor)
1060}
1061
1062const fn ordering_source_label(ordering_source: ExplainExecutionOrderingSource) -> &'static str {
1063 match ordering_source {
1064 ExplainExecutionOrderingSource::AccessOrder => "AccessOrder",
1065 ExplainExecutionOrderingSource::Materialized => "Materialized",
1066 ExplainExecutionOrderingSource::IndexSeekFirst { .. } => "IndexSeekFirst",
1067 ExplainExecutionOrderingSource::IndexSeekLast { .. } => "IndexSeekLast",
1068 }
1069}
1070
1071impl ExplainPlan {
1072 #[must_use]
1076 pub(crate) fn predicate_model_for_hash(&self) -> Option<&Predicate> {
1077 if let Some(predicate) = &self.predicate_model {
1078 debug_assert_eq!(
1079 self.predicate,
1080 ExplainPredicate::from_predicate(predicate),
1081 "explain predicate surface drifted from canonical predicate model"
1082 );
1083 Some(predicate)
1084 } else {
1085 debug_assert!(
1086 matches!(self.predicate, ExplainPredicate::None),
1087 "missing canonical predicate model requires ExplainPredicate::None"
1088 );
1089 None
1090 }
1091 }
1092}
1093
1094#[derive(Clone, Debug, Eq, PartialEq)]
1101pub enum ExplainGrouping {
1102 None,
1103 Grouped {
1104 strategy: ExplainGroupedStrategy,
1105 group_fields: Vec<ExplainGroupField>,
1106 aggregates: Vec<ExplainGroupAggregate>,
1107 having: Option<ExplainGroupHaving>,
1108 max_groups: u64,
1109 max_group_bytes: u64,
1110 },
1111}
1112
1113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1119pub enum ExplainGroupedStrategy {
1120 HashGroup,
1121 OrderedGroup,
1122}
1123
1124impl From<GroupedPlanStrategyHint> for ExplainGroupedStrategy {
1125 fn from(value: GroupedPlanStrategyHint) -> Self {
1126 match value {
1127 GroupedPlanStrategyHint::HashGroup => Self::HashGroup,
1128 GroupedPlanStrategyHint::OrderedGroup => Self::OrderedGroup,
1129 }
1130 }
1131}
1132
1133#[derive(Clone, Debug, Eq, PartialEq)]
1140pub struct ExplainGroupField {
1141 pub(crate) slot_index: usize,
1142 pub(crate) field: String,
1143}
1144
1145impl ExplainGroupField {
1146 #[must_use]
1148 pub const fn slot_index(&self) -> usize {
1149 self.slot_index
1150 }
1151
1152 #[must_use]
1154 pub const fn field(&self) -> &str {
1155 self.field.as_str()
1156 }
1157}
1158
1159#[derive(Clone, Debug, Eq, PartialEq)]
1166pub struct ExplainGroupAggregate {
1167 pub(crate) kind: AggregateKind,
1168 pub(crate) target_field: Option<String>,
1169 pub(crate) distinct: bool,
1170}
1171
1172impl ExplainGroupAggregate {
1173 #[must_use]
1175 pub const fn kind(&self) -> AggregateKind {
1176 self.kind
1177 }
1178
1179 #[must_use]
1181 pub fn target_field(&self) -> Option<&str> {
1182 self.target_field.as_deref()
1183 }
1184
1185 #[must_use]
1187 pub const fn distinct(&self) -> bool {
1188 self.distinct
1189 }
1190}
1191
1192#[derive(Clone, Debug, Eq, PartialEq)]
1199pub struct ExplainGroupHaving {
1200 pub(crate) clauses: Vec<ExplainGroupHavingClause>,
1201}
1202
1203impl ExplainGroupHaving {
1204 #[must_use]
1206 pub const fn clauses(&self) -> &[ExplainGroupHavingClause] {
1207 self.clauses.as_slice()
1208 }
1209}
1210
1211#[derive(Clone, Debug, Eq, PartialEq)]
1218pub struct ExplainGroupHavingClause {
1219 pub(crate) symbol: ExplainGroupHavingSymbol,
1220 pub(crate) op: CompareOp,
1221 pub(crate) value: Value,
1222}
1223
1224impl ExplainGroupHavingClause {
1225 #[must_use]
1227 pub const fn symbol(&self) -> &ExplainGroupHavingSymbol {
1228 &self.symbol
1229 }
1230
1231 #[must_use]
1233 pub const fn op(&self) -> CompareOp {
1234 self.op
1235 }
1236
1237 #[must_use]
1239 pub const fn value(&self) -> &Value {
1240 &self.value
1241 }
1242}
1243
1244#[derive(Clone, Debug, Eq, PartialEq)]
1251pub enum ExplainGroupHavingSymbol {
1252 GroupField { slot_index: usize, field: String },
1253 AggregateIndex { index: usize },
1254}
1255
1256#[derive(Clone, Debug, Eq, PartialEq)]
1263pub enum ExplainOrderPushdown {
1264 MissingModelContext,
1265 EligibleSecondaryIndex {
1266 index: &'static str,
1267 prefix_len: usize,
1268 },
1269 Rejected(SecondaryOrderPushdownRejection),
1270}
1271
1272#[derive(Clone, Debug, Eq, PartialEq)]
1279pub enum ExplainAccessPath {
1280 ByKey {
1281 key: Value,
1282 },
1283 ByKeys {
1284 keys: Vec<Value>,
1285 },
1286 KeyRange {
1287 start: Value,
1288 end: Value,
1289 },
1290 IndexPrefix {
1291 name: &'static str,
1292 fields: Vec<&'static str>,
1293 prefix_len: usize,
1294 values: Vec<Value>,
1295 },
1296 IndexMultiLookup {
1297 name: &'static str,
1298 fields: Vec<&'static str>,
1299 values: Vec<Value>,
1300 },
1301 IndexRange {
1302 name: &'static str,
1303 fields: Vec<&'static str>,
1304 prefix_len: usize,
1305 prefix: Vec<Value>,
1306 lower: Bound<Value>,
1307 upper: Bound<Value>,
1308 },
1309 FullScan,
1310 Union(Vec<Self>),
1311 Intersection(Vec<Self>),
1312}
1313
1314#[derive(Clone, Debug, Eq, PartialEq)]
1321pub enum ExplainPredicate {
1322 None,
1323 True,
1324 False,
1325 And(Vec<Self>),
1326 Or(Vec<Self>),
1327 Not(Box<Self>),
1328 Compare {
1329 field: String,
1330 op: CompareOp,
1331 value: Value,
1332 coercion: CoercionSpec,
1333 },
1334 IsNull {
1335 field: String,
1336 },
1337 IsMissing {
1338 field: String,
1339 },
1340 IsEmpty {
1341 field: String,
1342 },
1343 IsNotEmpty {
1344 field: String,
1345 },
1346 TextContains {
1347 field: String,
1348 value: Value,
1349 },
1350 TextContainsCi {
1351 field: String,
1352 value: Value,
1353 },
1354}
1355
1356#[derive(Clone, Debug, Eq, PartialEq)]
1362pub enum ExplainOrderBy {
1363 None,
1364 Fields(Vec<ExplainOrder>),
1365}
1366
1367#[derive(Clone, Debug, Eq, PartialEq)]
1374pub struct ExplainOrder {
1375 pub(crate) field: String,
1376 pub(crate) direction: OrderDirection,
1377}
1378
1379impl ExplainOrder {
1380 #[must_use]
1382 pub const fn field(&self) -> &str {
1383 self.field.as_str()
1384 }
1385
1386 #[must_use]
1388 pub const fn direction(&self) -> OrderDirection {
1389 self.direction
1390 }
1391}
1392
1393#[derive(Clone, Debug, Eq, PartialEq)]
1400pub enum ExplainPagination {
1401 None,
1402 Page { limit: Option<u32>, offset: u32 },
1403}
1404
1405#[derive(Clone, Debug, Eq, PartialEq)]
1412pub enum ExplainDeleteLimit {
1413 None,
1414 Limit { max_rows: u32 },
1415}
1416
1417impl<K> AccessPlannedQuery<K>
1418where
1419 K: FieldValue,
1420{
1421 #[must_use]
1423 pub(crate) fn explain(&self) -> ExplainPlan {
1424 self.explain_inner(None)
1425 }
1426
1427 #[must_use]
1433 pub(crate) fn explain_with_model(&self, model: &EntityModel) -> ExplainPlan {
1434 self.explain_inner(Some(model))
1435 }
1436
1437 fn explain_inner(&self, model: Option<&EntityModel>) -> ExplainPlan {
1438 let (logical, grouping) = match &self.logical {
1440 LogicalPlan::Scalar(logical) => (logical, ExplainGrouping::None),
1441 LogicalPlan::Grouped(logical) => (
1442 &logical.scalar,
1443 ExplainGrouping::Grouped {
1444 strategy: grouped_plan_strategy_hint_for_plan(self)
1445 .map_or(ExplainGroupedStrategy::HashGroup, Into::into),
1446 group_fields: logical
1447 .group
1448 .group_fields
1449 .iter()
1450 .map(|field_slot| ExplainGroupField {
1451 slot_index: field_slot.index(),
1452 field: field_slot.field().to_string(),
1453 })
1454 .collect(),
1455 aggregates: logical
1456 .group
1457 .aggregates
1458 .iter()
1459 .map(|aggregate| ExplainGroupAggregate {
1460 kind: aggregate.kind,
1461 target_field: aggregate.target_field.clone(),
1462 distinct: aggregate.distinct,
1463 })
1464 .collect(),
1465 having: explain_group_having(logical.having.as_ref()),
1466 max_groups: logical.group.execution.max_groups(),
1467 max_group_bytes: logical.group.execution.max_group_bytes(),
1468 },
1469 ),
1470 };
1471
1472 explain_scalar_inner(logical, grouping, model, &self.access)
1474 }
1475}
1476
1477fn explain_group_having(having: Option<&GroupHavingSpec>) -> Option<ExplainGroupHaving> {
1478 let having = having?;
1479
1480 Some(ExplainGroupHaving {
1481 clauses: having
1482 .clauses()
1483 .iter()
1484 .map(explain_group_having_clause)
1485 .collect(),
1486 })
1487}
1488
1489fn explain_group_having_clause(clause: &GroupHavingClause) -> ExplainGroupHavingClause {
1490 ExplainGroupHavingClause {
1491 symbol: explain_group_having_symbol(clause.symbol()),
1492 op: clause.op(),
1493 value: clause.value().clone(),
1494 }
1495}
1496
1497fn explain_group_having_symbol(symbol: &GroupHavingSymbol) -> ExplainGroupHavingSymbol {
1498 match symbol {
1499 GroupHavingSymbol::GroupField(field_slot) => ExplainGroupHavingSymbol::GroupField {
1500 slot_index: field_slot.index(),
1501 field: field_slot.field().to_string(),
1502 },
1503 GroupHavingSymbol::AggregateIndex(index) => {
1504 ExplainGroupHavingSymbol::AggregateIndex { index: *index }
1505 }
1506 }
1507}
1508
1509fn explain_scalar_inner<K>(
1510 logical: &ScalarPlan,
1511 grouping: ExplainGrouping,
1512 model: Option<&EntityModel>,
1513 access: &AccessPlan<K>,
1514) -> ExplainPlan
1515where
1516 K: FieldValue,
1517{
1518 let predicate_model = logical.predicate.as_ref().map(normalize);
1520 let predicate = match &predicate_model {
1521 Some(predicate) => ExplainPredicate::from_predicate(predicate),
1522 None => ExplainPredicate::None,
1523 };
1524
1525 let order_by = explain_order(logical.order.as_ref());
1527 let order_pushdown = explain_order_pushdown(model);
1528 let page = explain_page(logical.page.as_ref());
1529 let delete_limit = explain_delete_limit(logical.delete_limit.as_ref());
1530
1531 ExplainPlan {
1533 mode: logical.mode,
1534 access: ExplainAccessPath::from_access_plan(access),
1535 predicate,
1536 predicate_model,
1537 order_by,
1538 distinct: logical.distinct,
1539 grouping,
1540 order_pushdown,
1541 page,
1542 delete_limit,
1543 consistency: logical.consistency,
1544 }
1545}
1546
1547const fn explain_order_pushdown(model: Option<&EntityModel>) -> ExplainOrderPushdown {
1548 let _ = model;
1549
1550 ExplainOrderPushdown::MissingModelContext
1552}
1553
1554impl From<SecondaryOrderPushdownEligibility> for ExplainOrderPushdown {
1555 fn from(value: SecondaryOrderPushdownEligibility) -> Self {
1556 Self::from(PushdownSurfaceEligibility::from(&value))
1557 }
1558}
1559
1560impl From<PushdownSurfaceEligibility<'_>> for ExplainOrderPushdown {
1561 fn from(value: PushdownSurfaceEligibility<'_>) -> Self {
1562 match value {
1563 PushdownSurfaceEligibility::EligibleSecondaryIndex { index, prefix_len } => {
1564 Self::EligibleSecondaryIndex { index, prefix_len }
1565 }
1566 PushdownSurfaceEligibility::Rejected { reason } => Self::Rejected(reason.clone()),
1567 }
1568 }
1569}
1570
1571struct ExplainAccessProjection;
1572
1573impl<K> AccessPlanProjection<K> for ExplainAccessProjection
1574where
1575 K: FieldValue,
1576{
1577 type Output = ExplainAccessPath;
1578
1579 fn by_key(&mut self, key: &K) -> Self::Output {
1580 ExplainAccessPath::ByKey {
1581 key: key.to_value(),
1582 }
1583 }
1584
1585 fn by_keys(&mut self, keys: &[K]) -> Self::Output {
1586 ExplainAccessPath::ByKeys {
1587 keys: keys.iter().map(FieldValue::to_value).collect(),
1588 }
1589 }
1590
1591 fn key_range(&mut self, start: &K, end: &K) -> Self::Output {
1592 ExplainAccessPath::KeyRange {
1593 start: start.to_value(),
1594 end: end.to_value(),
1595 }
1596 }
1597
1598 fn index_prefix(
1599 &mut self,
1600 index_name: &'static str,
1601 index_fields: &[&'static str],
1602 prefix_len: usize,
1603 values: &[Value],
1604 ) -> Self::Output {
1605 ExplainAccessPath::IndexPrefix {
1606 name: index_name,
1607 fields: index_fields.to_vec(),
1608 prefix_len,
1609 values: values.to_vec(),
1610 }
1611 }
1612
1613 fn index_multi_lookup(
1614 &mut self,
1615 index_name: &'static str,
1616 index_fields: &[&'static str],
1617 values: &[Value],
1618 ) -> Self::Output {
1619 ExplainAccessPath::IndexMultiLookup {
1620 name: index_name,
1621 fields: index_fields.to_vec(),
1622 values: values.to_vec(),
1623 }
1624 }
1625
1626 fn index_range(
1627 &mut self,
1628 index_name: &'static str,
1629 index_fields: &[&'static str],
1630 prefix_len: usize,
1631 prefix: &[Value],
1632 lower: &Bound<Value>,
1633 upper: &Bound<Value>,
1634 ) -> Self::Output {
1635 ExplainAccessPath::IndexRange {
1636 name: index_name,
1637 fields: index_fields.to_vec(),
1638 prefix_len,
1639 prefix: prefix.to_vec(),
1640 lower: lower.clone(),
1641 upper: upper.clone(),
1642 }
1643 }
1644
1645 fn full_scan(&mut self) -> Self::Output {
1646 ExplainAccessPath::FullScan
1647 }
1648
1649 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
1650 ExplainAccessPath::Union(children)
1651 }
1652
1653 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
1654 ExplainAccessPath::Intersection(children)
1655 }
1656}
1657
1658impl ExplainAccessPath {
1659 pub(in crate::db) fn from_access_plan<K>(access: &AccessPlan<K>) -> Self
1660 where
1661 K: FieldValue,
1662 {
1663 let mut projection = ExplainAccessProjection;
1664 project_access_plan(access, &mut projection)
1665 }
1666}
1667
1668impl ExplainPredicate {
1669 fn from_predicate(predicate: &Predicate) -> Self {
1670 match predicate {
1671 Predicate::True => Self::True,
1672 Predicate::False => Self::False,
1673 Predicate::And(children) => {
1674 Self::And(children.iter().map(Self::from_predicate).collect())
1675 }
1676 Predicate::Or(children) => {
1677 Self::Or(children.iter().map(Self::from_predicate).collect())
1678 }
1679 Predicate::Not(inner) => Self::Not(Box::new(Self::from_predicate(inner))),
1680 Predicate::Compare(compare) => Self::from_compare(compare),
1681 Predicate::IsNull { field } => Self::IsNull {
1682 field: field.clone(),
1683 },
1684 Predicate::IsMissing { field } => Self::IsMissing {
1685 field: field.clone(),
1686 },
1687 Predicate::IsEmpty { field } => Self::IsEmpty {
1688 field: field.clone(),
1689 },
1690 Predicate::IsNotEmpty { field } => Self::IsNotEmpty {
1691 field: field.clone(),
1692 },
1693 Predicate::TextContains { field, value } => Self::TextContains {
1694 field: field.clone(),
1695 value: value.clone(),
1696 },
1697 Predicate::TextContainsCi { field, value } => Self::TextContainsCi {
1698 field: field.clone(),
1699 value: value.clone(),
1700 },
1701 }
1702 }
1703
1704 fn from_compare(compare: &ComparePredicate) -> Self {
1705 Self::Compare {
1706 field: compare.field.clone(),
1707 op: compare.op,
1708 value: compare.value.clone(),
1709 coercion: compare.coercion.clone(),
1710 }
1711 }
1712}
1713
1714fn explain_order(order: Option<&OrderSpec>) -> ExplainOrderBy {
1715 let Some(order) = order else {
1716 return ExplainOrderBy::None;
1717 };
1718
1719 if order.fields.is_empty() {
1720 return ExplainOrderBy::None;
1721 }
1722
1723 ExplainOrderBy::Fields(
1724 order
1725 .fields
1726 .iter()
1727 .map(|(field, direction)| ExplainOrder {
1728 field: field.clone(),
1729 direction: *direction,
1730 })
1731 .collect(),
1732 )
1733}
1734
1735const fn explain_page(page: Option<&PageSpec>) -> ExplainPagination {
1736 match page {
1737 Some(page) => ExplainPagination::Page {
1738 limit: page.limit,
1739 offset: page.offset,
1740 },
1741 None => ExplainPagination::None,
1742 }
1743}
1744
1745const fn explain_delete_limit(limit: Option<&DeleteLimitSpec>) -> ExplainDeleteLimit {
1746 match limit {
1747 Some(limit) => ExplainDeleteLimit::Limit {
1748 max_rows: limit.max_rows,
1749 },
1750 None => ExplainDeleteLimit::None,
1751 }
1752}
1753
1754#[cfg(test)]
1759mod tests;