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