1mod planner;
4#[cfg(test)]
5mod tests;
6pub(crate) mod validate;
7
8use crate::{
9 db::{
10 access::{
11 AccessPath, AccessPlan, PushdownApplicability, SecondaryOrderPushdownEligibility,
12 SecondaryOrderPushdownRejection,
13 },
14 contracts::{PredicateExecutionModel, ReadConsistency},
15 cursor::CursorBoundary,
16 direction::Direction,
17 query::explain::ExplainAccessPath,
18 },
19 error::InternalError,
20 model::entity::EntityModel,
21 traits::{EntityKind, EntityValue},
22 value::Value,
23};
24use std::ops::{Bound, Deref, DerefMut};
25
26pub(in crate::db) use crate::db::query::fingerprint::canonical;
27pub use validate::PlanError;
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum QueryMode {
39 Load(LoadSpec),
40 Delete(DeleteSpec),
41}
42
43impl QueryMode {
44 #[must_use]
46 pub const fn is_load(&self) -> bool {
47 match self {
48 Self::Load(_) => true,
49 Self::Delete(_) => false,
50 }
51 }
52
53 #[must_use]
55 pub const fn is_delete(&self) -> bool {
56 match self {
57 Self::Delete(_) => true,
58 Self::Load(_) => false,
59 }
60 }
61}
62
63#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub struct LoadSpec {
71 pub limit: Option<u32>,
72 pub offset: u32,
73}
74
75impl LoadSpec {
76 #[must_use]
78 pub const fn new() -> Self {
79 Self {
80 limit: None,
81 offset: 0,
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
94pub struct DeleteSpec {
95 pub limit: Option<u32>,
96}
97
98impl DeleteSpec {
99 #[must_use]
101 pub const fn new() -> Self {
102 Self { limit: None }
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub enum OrderDirection {
112 Asc,
113 Desc,
114}
115
116impl OrderDirection {
117 #[must_use]
119 pub(in crate::db) const fn as_direction(self) -> Direction {
120 match self {
121 Self::Asc => Direction::Asc,
122 Self::Desc => Direction::Desc,
123 }
124 }
125
126 #[must_use]
128 pub(in crate::db) const fn from_direction(direction: Direction) -> Self {
129 match direction {
130 Direction::Asc => Self::Asc,
131 Direction::Desc => Self::Desc,
132 }
133 }
134}
135
136#[derive(Clone, Debug, Eq, PartialEq)]
142pub(crate) struct OrderSpec {
143 pub(crate) fields: Vec<(String, OrderDirection)>,
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub(crate) struct DeleteLimitSpec {
153 pub max_rows: u32,
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
162pub(crate) struct PageSpec {
163 pub limit: Option<u32>,
164 pub offset: u32,
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
176#[allow(dead_code)]
177pub(crate) enum GroupAggregateKind {
178 Count,
179 Exists,
180 Min,
181 Max,
182 First,
183 Last,
184}
185
186#[derive(Clone, Debug, Eq, PartialEq)]
194pub(crate) struct GroupAggregateSpec {
195 pub(crate) kind: GroupAggregateKind,
196 pub(crate) target_field: Option<String>,
197}
198
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
207pub(crate) struct GroupedExecutionConfig {
208 pub(crate) max_groups: u64,
209 pub(crate) max_group_bytes: u64,
210}
211
212impl GroupedExecutionConfig {
213 #[must_use]
215 pub(crate) const fn with_hard_limits(max_groups: u64, max_group_bytes: u64) -> Self {
216 Self {
217 max_groups,
218 max_group_bytes,
219 }
220 }
221
222 #[must_use]
224 pub(crate) const fn unbounded() -> Self {
225 Self::with_hard_limits(u64::MAX, u64::MAX)
226 }
227
228 #[must_use]
230 pub(crate) const fn max_groups(&self) -> u64 {
231 self.max_groups
232 }
233
234 #[must_use]
236 pub(crate) const fn max_group_bytes(&self) -> u64 {
237 self.max_group_bytes
238 }
239}
240
241impl Default for GroupedExecutionConfig {
242 fn default() -> Self {
243 Self::unbounded()
244 }
245}
246
247#[derive(Clone, Debug, Eq, PartialEq)]
255pub(crate) struct GroupSpec {
256 pub(crate) group_fields: Vec<String>,
257 pub(crate) aggregates: Vec<GroupAggregateSpec>,
258 pub(crate) execution: GroupedExecutionConfig,
259}
260
261#[derive(Clone, Debug, Eq, PartialEq)]
281pub(crate) struct LogicalPlan {
282 pub(crate) mode: QueryMode,
284
285 pub(crate) predicate: Option<PredicateExecutionModel>,
287
288 pub(crate) order: Option<OrderSpec>,
290
291 pub(crate) distinct: bool,
293
294 pub(crate) delete_limit: Option<DeleteLimitSpec>,
296
297 pub(crate) page: Option<PageSpec>,
299
300 pub(crate) consistency: ReadConsistency,
302}
303
304#[derive(Clone, Debug, Eq, PartialEq)]
311pub(crate) struct AccessPlannedQuery<K> {
312 pub(crate) logical: LogicalPlan,
313 pub(crate) access: AccessPlan<K>,
314}
315
316#[derive(Clone, Debug, Eq, PartialEq)]
324pub(crate) struct GroupedPlan<K> {
325 pub(crate) base: AccessPlannedQuery<K>,
326 pub(crate) group: GroupSpec,
327}
328
329impl<K> AccessPlannedQuery<K> {
330 #[must_use]
332 pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
333 Self { logical, access }
334 }
335
336 #[must_use]
338 pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
339 (self.logical, self.access)
340 }
341
342 #[cfg(test)]
346 pub(crate) fn new(
347 access: crate::db::access::AccessPath<K>,
348 consistency: ReadConsistency,
349 ) -> Self {
350 Self {
351 logical: LogicalPlan {
352 mode: QueryMode::Load(LoadSpec::new()),
353 predicate: None,
354 order: None,
355 distinct: false,
356 delete_limit: None,
357 page: None,
358 consistency,
359 },
360 access: AccessPlan::path(access),
361 }
362 }
363
364 pub(in crate::db) fn cursor_boundary_from_entity<E>(
366 &self,
367 entity: &E,
368 ) -> Result<CursorBoundary, InternalError>
369 where
370 E: EntityKind<Key = K> + EntityValue,
371 {
372 let Some(order) = self.order.as_ref() else {
373 return Err(InternalError::query_executor_invariant(
374 "cannot build cursor boundary without ordering",
375 ));
376 };
377
378 Ok(CursorBoundary::from_ordered_entity(entity, order))
379 }
380}
381
382impl<K> GroupedPlan<K> {
383 #[must_use]
385 #[allow(dead_code)]
386 pub(crate) const fn from_parts(base: AccessPlannedQuery<K>, group: GroupSpec) -> Self {
387 Self { base, group }
388 }
389}
390
391impl<K> Deref for AccessPlannedQuery<K> {
392 type Target = LogicalPlan;
393
394 fn deref(&self) -> &Self::Target {
395 &self.logical
396 }
397}
398
399impl<K> DerefMut for AccessPlannedQuery<K> {
400 fn deref_mut(&mut self) -> &mut Self::Target {
401 &mut self.logical
402 }
403}
404
405fn order_fields_as_direction_refs(
406 order_fields: &[(String, OrderDirection)],
407) -> Vec<(&str, Direction)> {
408 order_fields
409 .iter()
410 .map(|(field, direction)| (field.as_str(), direction.as_direction()))
411 .collect()
412}
413
414fn applicability_from_eligibility(
415 eligibility: SecondaryOrderPushdownEligibility,
416) -> PushdownApplicability {
417 match eligibility {
418 SecondaryOrderPushdownEligibility::Rejected(
419 SecondaryOrderPushdownRejection::NoOrderBy
420 | SecondaryOrderPushdownRejection::AccessPathNotSingleIndexPrefix,
421 ) => PushdownApplicability::NotApplicable,
422 other => PushdownApplicability::Applicable(other),
423 }
424}
425
426fn match_secondary_order_pushdown_core(
428 model: &EntityModel,
429 order_fields: &[(&str, Direction)],
430 index_name: &'static str,
431 index_fields: &[&'static str],
432 prefix_len: usize,
433) -> SecondaryOrderPushdownEligibility {
434 let Some((last_field, last_direction)) = order_fields.last() else {
435 return SecondaryOrderPushdownEligibility::Rejected(
436 SecondaryOrderPushdownRejection::NoOrderBy,
437 );
438 };
439 if *last_field != model.primary_key.name {
440 return SecondaryOrderPushdownEligibility::Rejected(
441 SecondaryOrderPushdownRejection::MissingPrimaryKeyTieBreak {
442 field: model.primary_key.name.to_string(),
443 },
444 );
445 }
446
447 let expected_direction = *last_direction;
448 for (field, direction) in order_fields {
449 if *direction != expected_direction {
450 return SecondaryOrderPushdownEligibility::Rejected(
451 SecondaryOrderPushdownRejection::MixedDirectionNotEligible {
452 field: (*field).to_string(),
453 },
454 );
455 }
456 }
457
458 let actual_non_pk_len = order_fields.len().saturating_sub(1);
459 let matches_expected_suffix = actual_non_pk_len
460 == index_fields.len().saturating_sub(prefix_len)
461 && order_fields
462 .iter()
463 .take(actual_non_pk_len)
464 .map(|(field, _)| *field)
465 .zip(index_fields.iter().skip(prefix_len).copied())
466 .all(|(actual, expected)| actual == expected);
467 let matches_expected_full = actual_non_pk_len == index_fields.len()
468 && order_fields
469 .iter()
470 .take(actual_non_pk_len)
471 .map(|(field, _)| *field)
472 .zip(index_fields.iter().copied())
473 .all(|(actual, expected)| actual == expected);
474 if matches_expected_suffix || matches_expected_full {
475 return SecondaryOrderPushdownEligibility::Eligible {
476 index: index_name,
477 prefix_len,
478 };
479 }
480
481 SecondaryOrderPushdownEligibility::Rejected(
482 SecondaryOrderPushdownRejection::OrderFieldsDoNotMatchIndex {
483 index: index_name,
484 prefix_len,
485 expected_suffix: index_fields
486 .iter()
487 .skip(prefix_len)
488 .map(|field| (*field).to_string())
489 .collect(),
490 expected_full: index_fields
491 .iter()
492 .map(|field| (*field).to_string())
493 .collect(),
494 actual: order_fields
495 .iter()
496 .take(actual_non_pk_len)
497 .map(|(field, _)| (*field).to_string())
498 .collect(),
499 },
500 )
501}
502
503fn assess_secondary_order_pushdown_for_applicable_shape(
505 model: &EntityModel,
506 order_fields: &[(&str, Direction)],
507 index_name: &'static str,
508 index_fields: &[&'static str],
509 prefix_len: usize,
510) -> SecondaryOrderPushdownEligibility {
511 match_secondary_order_pushdown_core(model, order_fields, index_name, index_fields, prefix_len)
512}
513
514fn assess_secondary_order_pushdown_for_plan<K>(
516 model: &EntityModel,
517 order_fields: Option<&[(&str, Direction)]>,
518 access_plan: &AccessPlan<K>,
519) -> SecondaryOrderPushdownEligibility {
520 let Some(order_fields) = order_fields else {
521 return SecondaryOrderPushdownEligibility::Rejected(
522 SecondaryOrderPushdownRejection::NoOrderBy,
523 );
524 };
525 if order_fields.is_empty() {
526 return SecondaryOrderPushdownEligibility::Rejected(
527 SecondaryOrderPushdownRejection::NoOrderBy,
528 );
529 }
530
531 let Some(access) = access_plan.as_path() else {
532 if let Some((index, prefix_len)) = access_plan.first_index_range_details() {
533 return SecondaryOrderPushdownEligibility::Rejected(
534 SecondaryOrderPushdownRejection::AccessPathIndexRangeUnsupported {
535 index: index.name,
536 prefix_len,
537 },
538 );
539 }
540
541 return SecondaryOrderPushdownEligibility::Rejected(
542 SecondaryOrderPushdownRejection::AccessPathNotSingleIndexPrefix,
543 );
544 };
545 if let Some((index, values)) = access.as_index_prefix() {
546 if values.len() > index.fields.len() {
547 return SecondaryOrderPushdownEligibility::Rejected(
548 SecondaryOrderPushdownRejection::InvalidIndexPrefixBounds {
549 prefix_len: values.len(),
550 index_field_len: index.fields.len(),
551 },
552 );
553 }
554
555 return assess_secondary_order_pushdown_for_applicable_shape(
556 model,
557 order_fields,
558 index.name,
559 index.fields,
560 values.len(),
561 );
562 }
563 if let Some((index, prefix_len)) = access.index_range_details() {
564 return SecondaryOrderPushdownEligibility::Rejected(
565 SecondaryOrderPushdownRejection::AccessPathIndexRangeUnsupported {
566 index: index.name,
567 prefix_len,
568 },
569 );
570 }
571
572 SecondaryOrderPushdownEligibility::Rejected(
573 SecondaryOrderPushdownRejection::AccessPathNotSingleIndexPrefix,
574 )
575}
576
577pub(crate) fn assess_secondary_order_pushdown<K>(
579 model: &EntityModel,
580 plan: &AccessPlannedQuery<K>,
581) -> SecondaryOrderPushdownEligibility {
582 let order_fields = plan
583 .order
584 .as_ref()
585 .map(|order| order_fields_as_direction_refs(&order.fields));
586
587 assess_secondary_order_pushdown_for_plan(model, order_fields.as_deref(), &plan.access)
588}
589
590pub(in crate::db) fn derive_secondary_pushdown_applicability_validated<K>(
592 model: &EntityModel,
593 plan: &AccessPlannedQuery<K>,
594) -> PushdownApplicability {
595 debug_assert!(
596 !matches!(plan.order.as_ref(), Some(order) if order.fields.is_empty()),
597 "validated plan must not contain an empty ORDER BY specification",
598 );
599
600 applicability_from_eligibility(assess_secondary_order_pushdown(model, plan))
601}
602
603#[cfg(test)]
604pub(crate) fn assess_secondary_order_pushdown_if_applicable<K>(
606 model: &EntityModel,
607 plan: &AccessPlannedQuery<K>,
608) -> PushdownApplicability {
609 derive_secondary_pushdown_applicability_validated(model, plan)
610}
611
612#[cfg(test)]
615pub(crate) fn assess_secondary_order_pushdown_if_applicable_validated<K>(
616 model: &EntityModel,
617 plan: &AccessPlannedQuery<K>,
618) -> PushdownApplicability {
619 derive_secondary_pushdown_applicability_validated(model, plan)
620}
621
622pub(crate) use planner::{PlannerError, plan_access};
623
624pub(crate) trait AccessPlanProjection<K> {
632 type Output;
633
634 fn by_key(&mut self, key: &K) -> Self::Output;
635 fn by_keys(&mut self, keys: &[K]) -> Self::Output;
636 fn key_range(&mut self, start: &K, end: &K) -> Self::Output;
637 fn index_prefix(
638 &mut self,
639 index_name: &'static str,
640 index_fields: &[&'static str],
641 prefix_len: usize,
642 values: &[Value],
643 ) -> Self::Output;
644 fn index_range(
645 &mut self,
646 index_name: &'static str,
647 index_fields: &[&'static str],
648 prefix_len: usize,
649 prefix: &[Value],
650 lower: &Bound<Value>,
651 upper: &Bound<Value>,
652 ) -> Self::Output;
653 fn full_scan(&mut self) -> Self::Output;
654 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output;
655 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output;
656}
657
658pub(crate) fn project_access_plan<K, P>(plan: &AccessPlan<K>, projection: &mut P) -> P::Output
660where
661 P: AccessPlanProjection<K>,
662{
663 plan.project(projection)
664}
665
666impl<K> AccessPlan<K> {
667 fn project<P>(&self, projection: &mut P) -> P::Output
669 where
670 P: AccessPlanProjection<K>,
671 {
672 match self {
673 Self::Path(path) => path.project(projection),
674 Self::Union(children) => {
675 let children = children
676 .iter()
677 .map(|child| child.project(projection))
678 .collect();
679 projection.union(children)
680 }
681 Self::Intersection(children) => {
682 let children = children
683 .iter()
684 .map(|child| child.project(projection))
685 .collect();
686 projection.intersection(children)
687 }
688 }
689 }
690}
691
692impl<K> AccessPath<K> {
693 fn project<P>(&self, projection: &mut P) -> P::Output
695 where
696 P: AccessPlanProjection<K>,
697 {
698 match self {
699 Self::ByKey(key) => projection.by_key(key),
700 Self::ByKeys(keys) => projection.by_keys(keys),
701 Self::KeyRange { start, end } => projection.key_range(start, end),
702 Self::IndexPrefix { index, values } => {
703 projection.index_prefix(index.name, index.fields, values.len(), values)
704 }
705 Self::IndexRange { spec } => projection.index_range(
706 spec.index().name,
707 spec.index().fields,
708 spec.prefix_values().len(),
709 spec.prefix_values(),
710 spec.lower(),
711 spec.upper(),
712 ),
713 Self::FullScan => projection.full_scan(),
714 }
715 }
716}
717
718pub(crate) fn project_explain_access_path<P>(
719 access: &ExplainAccessPath,
720 projection: &mut P,
721) -> P::Output
722where
723 P: AccessPlanProjection<Value>,
724{
725 match access {
726 ExplainAccessPath::ByKey { key } => projection.by_key(key),
727 ExplainAccessPath::ByKeys { keys } => projection.by_keys(keys),
728 ExplainAccessPath::KeyRange { start, end } => projection.key_range(start, end),
729 ExplainAccessPath::IndexPrefix {
730 name,
731 fields,
732 prefix_len,
733 values,
734 } => projection.index_prefix(name, fields, *prefix_len, values),
735 ExplainAccessPath::IndexRange {
736 name,
737 fields,
738 prefix_len,
739 prefix,
740 lower,
741 upper,
742 } => projection.index_range(name, fields, *prefix_len, prefix, lower, upper),
743 ExplainAccessPath::FullScan => projection.full_scan(),
744 ExplainAccessPath::Union(children) => {
745 let children = children
746 .iter()
747 .map(|child| project_explain_access_path(child, projection))
748 .collect();
749 projection.union(children)
750 }
751 ExplainAccessPath::Intersection(children) => {
752 let children = children
753 .iter()
754 .map(|child| project_explain_access_path(child, projection))
755 .collect();
756 projection.intersection(children)
757 }
758 }
759}
760
761#[cfg(test)]
766mod access_projection_tests {
767 use super::*;
768 use crate::{model::index::IndexModel, value::Value};
769
770 const TEST_INDEX_FIELDS: [&str; 2] = ["group", "rank"];
771 const TEST_INDEX: IndexModel = IndexModel::new(
772 "tests::group_rank",
773 "tests::store",
774 &TEST_INDEX_FIELDS,
775 false,
776 );
777
778 #[derive(Default)]
779 struct AccessPlanEventProjection {
780 events: Vec<&'static str>,
781 union_child_counts: Vec<usize>,
782 intersection_child_counts: Vec<usize>,
783 seen_index: Option<(&'static str, usize, usize, usize)>,
784 }
785
786 impl AccessPlanProjection<u64> for AccessPlanEventProjection {
787 type Output = ();
788
789 fn by_key(&mut self, _key: &u64) -> Self::Output {
790 self.events.push("by_key");
791 }
792
793 fn by_keys(&mut self, keys: &[u64]) -> Self::Output {
794 self.events.push("by_keys");
795 assert_eq!(keys, [2, 3].as_slice());
796 }
797
798 fn key_range(&mut self, start: &u64, end: &u64) -> Self::Output {
799 self.events.push("key_range");
800 assert_eq!((*start, *end), (4, 9));
801 }
802
803 fn index_prefix(
804 &mut self,
805 index_name: &'static str,
806 index_fields: &[&'static str],
807 prefix_len: usize,
808 values: &[Value],
809 ) -> Self::Output {
810 self.events.push("index_prefix");
811 self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
812 }
813
814 fn index_range(
815 &mut self,
816 index_name: &'static str,
817 index_fields: &[&'static str],
818 prefix_len: usize,
819 prefix: &[Value],
820 lower: &Bound<Value>,
821 upper: &Bound<Value>,
822 ) -> Self::Output {
823 self.events.push("index_range");
824 self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
825 assert_eq!(lower, &Bound::Included(Value::Uint(8)));
826 assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
827 }
828
829 fn full_scan(&mut self) -> Self::Output {
830 self.events.push("full_scan");
831 }
832
833 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
834 self.events.push("union");
835 self.union_child_counts.push(children.len());
836 }
837
838 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
839 self.events.push("intersection");
840 self.intersection_child_counts.push(children.len());
841 }
842 }
843
844 #[test]
845 fn project_access_plan_walks_canonical_access_variants() {
846 let plan: AccessPlan<u64> = AccessPlan::Union(vec![
847 AccessPlan::path(AccessPath::ByKey(1)),
848 AccessPlan::path(AccessPath::ByKeys(vec![2, 3])),
849 AccessPlan::path(AccessPath::KeyRange { start: 4, end: 9 }),
850 AccessPlan::path(AccessPath::IndexPrefix {
851 index: TEST_INDEX,
852 values: vec![Value::Uint(7)],
853 }),
854 AccessPlan::path(AccessPath::index_range(
855 TEST_INDEX,
856 vec![Value::Uint(7)],
857 Bound::Included(Value::Uint(8)),
858 Bound::Excluded(Value::Uint(12)),
859 )),
860 AccessPlan::Intersection(vec![
861 AccessPlan::path(AccessPath::FullScan),
862 AccessPlan::path(AccessPath::ByKey(11)),
863 ]),
864 ]);
865
866 let mut projection = AccessPlanEventProjection::default();
867 project_access_plan(&plan, &mut projection);
868
869 assert_eq!(projection.union_child_counts, vec![6]);
870 assert_eq!(projection.intersection_child_counts, vec![2]);
871 assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
872 assert!(
873 projection.events.contains(&"by_key"),
874 "projection must visit by-key variants"
875 );
876 assert!(
877 projection.events.contains(&"by_keys"),
878 "projection must visit by-keys variants"
879 );
880 assert!(
881 projection.events.contains(&"key_range"),
882 "projection must visit key-range variants"
883 );
884 assert!(
885 projection.events.contains(&"index_prefix"),
886 "projection must visit index-prefix variants"
887 );
888 assert!(
889 projection.events.contains(&"index_range"),
890 "projection must visit index-range variants"
891 );
892 assert!(
893 projection.events.contains(&"full_scan"),
894 "projection must visit full-scan variants"
895 );
896 }
897
898 #[derive(Default)]
899 struct ExplainAccessEventProjection {
900 events: Vec<&'static str>,
901 union_child_counts: Vec<usize>,
902 intersection_child_counts: Vec<usize>,
903 seen_index: Option<(&'static str, usize, usize, usize)>,
904 }
905
906 impl AccessPlanProjection<Value> for ExplainAccessEventProjection {
907 type Output = ();
908
909 fn by_key(&mut self, key: &Value) -> Self::Output {
910 self.events.push("by_key");
911 assert_eq!(key, &Value::Uint(10));
912 }
913
914 fn by_keys(&mut self, keys: &[Value]) -> Self::Output {
915 self.events.push("by_keys");
916 assert_eq!(keys, [Value::Uint(20), Value::Uint(30)].as_slice());
917 }
918
919 fn key_range(&mut self, start: &Value, end: &Value) -> Self::Output {
920 self.events.push("key_range");
921 assert_eq!((start, end), (&Value::Uint(40), &Value::Uint(90)));
922 }
923
924 fn index_prefix(
925 &mut self,
926 index_name: &'static str,
927 index_fields: &[&'static str],
928 prefix_len: usize,
929 values: &[Value],
930 ) -> Self::Output {
931 self.events.push("index_prefix");
932 self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
933 }
934
935 fn index_range(
936 &mut self,
937 index_name: &'static str,
938 index_fields: &[&'static str],
939 prefix_len: usize,
940 prefix: &[Value],
941 lower: &Bound<Value>,
942 upper: &Bound<Value>,
943 ) -> Self::Output {
944 self.events.push("index_range");
945 self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
946 assert_eq!(lower, &Bound::Included(Value::Uint(8)));
947 assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
948 }
949
950 fn full_scan(&mut self) -> Self::Output {
951 self.events.push("full_scan");
952 }
953
954 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
955 self.events.push("union");
956 self.union_child_counts.push(children.len());
957 }
958
959 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
960 self.events.push("intersection");
961 self.intersection_child_counts.push(children.len());
962 }
963 }
964
965 #[test]
966 fn project_explain_access_path_walks_canonical_access_variants() {
967 let access = ExplainAccessPath::Union(vec![
968 ExplainAccessPath::ByKey {
969 key: Value::Uint(10),
970 },
971 ExplainAccessPath::ByKeys {
972 keys: vec![Value::Uint(20), Value::Uint(30)],
973 },
974 ExplainAccessPath::KeyRange {
975 start: Value::Uint(40),
976 end: Value::Uint(90),
977 },
978 ExplainAccessPath::IndexPrefix {
979 name: TEST_INDEX.name,
980 fields: vec!["group", "rank"],
981 prefix_len: 1,
982 values: vec![Value::Uint(7)],
983 },
984 ExplainAccessPath::IndexRange {
985 name: TEST_INDEX.name,
986 fields: vec!["group", "rank"],
987 prefix_len: 1,
988 prefix: vec![Value::Uint(7)],
989 lower: Bound::Included(Value::Uint(8)),
990 upper: Bound::Excluded(Value::Uint(12)),
991 },
992 ExplainAccessPath::Intersection(vec![
993 ExplainAccessPath::FullScan,
994 ExplainAccessPath::ByKey {
995 key: Value::Uint(10),
996 },
997 ]),
998 ]);
999
1000 let mut projection = ExplainAccessEventProjection::default();
1001 project_explain_access_path(&access, &mut projection);
1002
1003 assert_eq!(projection.union_child_counts, vec![6]);
1004 assert_eq!(projection.intersection_child_counts, vec![2]);
1005 assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
1006 assert!(
1007 projection.events.contains(&"by_key"),
1008 "projection must visit by-key variants"
1009 );
1010 assert!(
1011 projection.events.contains(&"by_keys"),
1012 "projection must visit by-keys variants"
1013 );
1014 assert!(
1015 projection.events.contains(&"key_range"),
1016 "projection must visit key-range variants"
1017 );
1018 assert!(
1019 projection.events.contains(&"index_prefix"),
1020 "projection must visit index-prefix variants"
1021 );
1022 assert!(
1023 projection.events.contains(&"index_range"),
1024 "projection must visit index-range variants"
1025 );
1026 assert!(
1027 projection.events.contains(&"full_scan"),
1028 "projection must visit full-scan variants"
1029 );
1030 }
1031}