1mod group;
7mod planner;
8mod pushdown;
9#[cfg(test)]
10mod tests;
11pub(crate) mod validate;
12
13use crate::{
14 db::{
15 access::{AccessPath, AccessPlan},
16 direction::Direction,
17 predicate::{MissingRowPolicy, PredicateExecutionModel},
18 query::explain::ExplainAccessPath,
19 },
20 model::entity::{EntityModel, resolve_field_slot},
21 value::Value,
22};
23use std::ops::Bound;
24#[cfg(test)]
25use std::ops::{Deref, DerefMut};
26
27pub(in crate::db) use group::{GroupedExecutorHandoff, grouped_executor_handoff};
28pub(crate) use pushdown::assess_secondary_order_pushdown_from_parts;
29pub(in crate::db) use pushdown::derive_secondary_pushdown_applicability_validated;
30#[cfg(test)]
31pub(crate) use pushdown::{
32 assess_secondary_order_pushdown, assess_secondary_order_pushdown_if_applicable,
33 assess_secondary_order_pushdown_if_applicable_validated,
34};
35pub use validate::PlanError;
36pub(crate) use validate::{GroupPlanError, validate_group_query_semantics};
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum QueryMode {
48 Load(LoadSpec),
49 Delete(DeleteSpec),
50}
51
52impl QueryMode {
53 #[must_use]
55 pub const fn is_load(&self) -> bool {
56 match self {
57 Self::Load(_) => true,
58 Self::Delete(_) => false,
59 }
60 }
61
62 #[must_use]
64 pub const fn is_delete(&self) -> bool {
65 match self {
66 Self::Delete(_) => true,
67 Self::Load(_) => false,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
79pub struct LoadSpec {
80 pub limit: Option<u32>,
81 pub offset: u32,
82}
83
84impl LoadSpec {
85 #[must_use]
87 pub const fn new() -> Self {
88 Self {
89 limit: None,
90 offset: 0,
91 }
92 }
93}
94
95#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
103pub struct DeleteSpec {
104 pub limit: Option<u32>,
105}
106
107impl DeleteSpec {
108 #[must_use]
110 pub const fn new() -> Self {
111 Self { limit: None }
112 }
113}
114
115#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum OrderDirection {
121 Asc,
122 Desc,
123}
124
125impl OrderDirection {
126 #[must_use]
128 pub(in crate::db) const fn as_direction(self) -> Direction {
129 match self {
130 Self::Asc => Direction::Asc,
131 Self::Desc => Direction::Desc,
132 }
133 }
134
135 #[must_use]
137 pub(in crate::db) const fn from_direction(direction: Direction) -> Self {
138 match direction {
139 Direction::Asc => Self::Asc,
140 Direction::Desc => Self::Desc,
141 }
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
151pub(crate) struct OrderSpec {
152 pub(crate) fields: Vec<(String, OrderDirection)>,
153}
154
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub(crate) struct DeleteLimitSpec {
162 pub max_rows: u32,
163}
164
165#[derive(Clone, Debug, Eq, PartialEq)]
171pub(crate) struct PageSpec {
172 pub limit: Option<u32>,
173 pub offset: u32,
174}
175
176#[allow(dead_code)]
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum AggregateKind {
188 Count,
189 Exists,
190 Min,
191 Max,
192 First,
193 Last,
194}
195
196impl AggregateKind {
197 #[must_use]
199 pub(in crate::db) const fn is_count(self) -> bool {
200 matches!(self, Self::Count)
201 }
202
203 #[must_use]
205 pub(in crate::db) const fn supports_field_targets(self) -> bool {
206 matches!(self, Self::Min | Self::Max)
207 }
208
209 #[must_use]
211 pub(in crate::db) const fn is_extrema(self) -> bool {
212 self.supports_field_targets()
213 }
214
215 #[must_use]
217 pub(in crate::db) const fn supports_terminal_value_projection(self) -> bool {
218 matches!(self, Self::First | Self::Last)
219 }
220
221 #[must_use]
223 pub(in crate::db) const fn requires_decoded_id(self) -> bool {
224 !matches!(self, Self::Count | Self::Exists)
225 }
226
227 #[must_use]
229 pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
230 match self {
231 Self::Min => Some(Direction::Asc),
232 Self::Max => Some(Direction::Desc),
233 Self::Count | Self::Exists | Self::First | Self::Last => None,
234 }
235 }
236
237 #[must_use]
239 pub(in crate::db) const fn materialized_fold_direction(self) -> Direction {
240 match self {
241 Self::Min => Direction::Desc,
242 Self::Count | Self::Exists | Self::Max | Self::First | Self::Last => Direction::Asc,
243 }
244 }
245
246 #[must_use]
248 pub(in crate::db) const fn fingerprint_tag_v1(self) -> u8 {
249 match self {
250 Self::Count => 0x01,
251 Self::Exists => 0x02,
252 Self::Min => 0x03,
253 Self::Max => 0x04,
254 Self::First => 0x05,
255 Self::Last => 0x06,
256 }
257 }
258
259 #[must_use]
261 pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
262 !self.is_count()
263 }
264
265 #[must_use]
267 pub(in crate::db) fn bounded_probe_fetch_hint(
268 self,
269 direction: Direction,
270 offset: usize,
271 page_limit: Option<usize>,
272 ) -> Option<usize> {
273 match self {
274 Self::Exists | Self::First => Some(offset.saturating_add(1)),
275 Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
276 Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
277 Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
278 Self::Count | Self::Min | Self::Max => None,
279 }
280 }
281}
282
283pub(crate) type GroupAggregateKind = AggregateKind;
285
286#[derive(Clone, Debug, Eq, PartialEq)]
295pub(crate) struct GroupAggregateSpec {
296 pub(crate) kind: AggregateKind,
297 pub(crate) target_field: Option<String>,
298}
299
300impl GroupAggregateSpec {
301 #[must_use]
303 pub(crate) const fn kind(&self) -> AggregateKind {
304 self.kind
305 }
306
307 #[must_use]
309 pub(crate) fn target_field(&self) -> Option<&str> {
310 self.target_field.as_deref()
311 }
312}
313
314#[derive(Clone, Debug, Eq, PartialEq)]
323pub(crate) struct FieldSlot {
324 index: usize,
325 field: String,
326}
327
328impl FieldSlot {
329 #[must_use]
331 pub(crate) fn resolve(model: &EntityModel, field: &str) -> Option<Self> {
332 let index = resolve_field_slot(model, field)?;
333 let canonical = model
334 .fields
335 .get(index)
336 .map_or(field, |model_field| model_field.name);
337
338 Some(Self {
339 index,
340 field: canonical.to_string(),
341 })
342 }
343
344 #[cfg(test)]
346 #[must_use]
347 pub(crate) fn from_parts_for_test(index: usize, field: impl Into<String>) -> Self {
348 Self {
349 index,
350 field: field.into(),
351 }
352 }
353
354 #[must_use]
356 pub(crate) const fn index(&self) -> usize {
357 self.index
358 }
359
360 #[must_use]
362 pub(crate) fn field(&self) -> &str {
363 &self.field
364 }
365}
366
367#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub(crate) struct GroupedExecutionConfig {
377 pub(crate) max_groups: u64,
378 pub(crate) max_group_bytes: u64,
379}
380
381impl GroupedExecutionConfig {
382 #[must_use]
384 pub(crate) const fn with_hard_limits(max_groups: u64, max_group_bytes: u64) -> Self {
385 Self {
386 max_groups,
387 max_group_bytes,
388 }
389 }
390
391 #[must_use]
393 pub(crate) const fn unbounded() -> Self {
394 Self::with_hard_limits(u64::MAX, u64::MAX)
395 }
396
397 #[must_use]
399 pub(crate) const fn max_groups(&self) -> u64 {
400 self.max_groups
401 }
402
403 #[must_use]
405 pub(crate) const fn max_group_bytes(&self) -> u64 {
406 self.max_group_bytes
407 }
408}
409
410impl Default for GroupedExecutionConfig {
411 fn default() -> Self {
412 Self::unbounded()
413 }
414}
415
416#[derive(Clone, Debug, Eq, PartialEq)]
425pub(crate) struct GroupSpec {
426 pub(crate) group_fields: Vec<FieldSlot>,
427 pub(crate) aggregates: Vec<GroupAggregateSpec>,
428 pub(crate) execution: GroupedExecutionConfig,
429}
430
431#[derive(Clone, Debug, Eq, PartialEq)]
452pub(crate) struct ScalarPlan {
453 pub(crate) mode: QueryMode,
455
456 pub(crate) predicate: Option<PredicateExecutionModel>,
458
459 pub(crate) order: Option<OrderSpec>,
461
462 pub(crate) distinct: bool,
464
465 pub(crate) delete_limit: Option<DeleteLimitSpec>,
467
468 pub(crate) page: Option<PageSpec>,
470
471 pub(crate) consistency: MissingRowPolicy,
473}
474
475#[derive(Clone, Debug, Eq, PartialEq)]
483pub(crate) struct GroupPlan {
484 pub(crate) scalar: ScalarPlan,
485 pub(crate) group: GroupSpec,
486}
487
488#[derive(Clone, Debug, Eq, PartialEq)]
496pub(crate) enum LogicalPlan {
497 Scalar(ScalarPlan),
498 Grouped(GroupPlan),
499}
500
501impl LogicalPlan {
502 #[must_use]
504 pub(in crate::db) const fn scalar_semantics(&self) -> &ScalarPlan {
505 match self {
506 Self::Scalar(plan) => plan,
507 Self::Grouped(plan) => &plan.scalar,
508 }
509 }
510
511 #[must_use]
513 #[cfg(test)]
514 pub(in crate::db) const fn scalar_semantics_mut(&mut self) -> &mut ScalarPlan {
515 match self {
516 Self::Scalar(plan) => plan,
517 Self::Grouped(plan) => &mut plan.scalar,
518 }
519 }
520}
521
522#[cfg(test)]
523impl Deref for LogicalPlan {
524 type Target = ScalarPlan;
525
526 fn deref(&self) -> &Self::Target {
527 self.scalar_semantics()
528 }
529}
530
531#[cfg(test)]
532impl DerefMut for LogicalPlan {
533 fn deref_mut(&mut self) -> &mut Self::Target {
534 self.scalar_semantics_mut()
535 }
536}
537
538#[derive(Clone, Debug, Eq, PartialEq)]
546pub(crate) struct AccessPlannedQuery<K> {
547 pub(crate) logical: LogicalPlan,
548 pub(crate) access: AccessPlan<K>,
549}
550
551impl<K> AccessPlannedQuery<K> {
552 #[must_use]
554 pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
555 Self { logical, access }
556 }
557
558 #[must_use]
560 pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
561 (self.logical, self.access)
562 }
563
564 #[must_use]
566 pub(in crate::db) fn into_grouped(self, group: GroupSpec) -> Self {
567 let Self { logical, access } = self;
568 let scalar = match logical {
569 LogicalPlan::Scalar(plan) => plan,
570 LogicalPlan::Grouped(plan) => plan.scalar,
571 };
572
573 Self {
574 logical: LogicalPlan::Grouped(GroupPlan { scalar, group }),
575 access,
576 }
577 }
578
579 #[must_use]
581 pub(in crate::db) const fn scalar_plan(&self) -> &ScalarPlan {
582 self.logical.scalar_semantics()
583 }
584
585 #[must_use]
587 pub(in crate::db) const fn grouped_plan(&self) -> Option<&GroupPlan> {
588 match &self.logical {
589 LogicalPlan::Scalar(_) => None,
590 LogicalPlan::Grouped(plan) => Some(plan),
591 }
592 }
593
594 #[must_use]
596 #[cfg(test)]
597 pub(in crate::db) const fn scalar_plan_mut(&mut self) -> &mut ScalarPlan {
598 self.logical.scalar_semantics_mut()
599 }
600
601 #[cfg(test)]
605 pub(crate) fn new(
606 access: crate::db::access::AccessPath<K>,
607 consistency: MissingRowPolicy,
608 ) -> Self {
609 Self {
610 logical: LogicalPlan::Scalar(crate::db::query::plan::ScalarPlan {
611 mode: QueryMode::Load(LoadSpec::new()),
612 predicate: None,
613 order: None,
614 distinct: false,
615 delete_limit: None,
616 page: None,
617 consistency,
618 }),
619 access: AccessPlan::path(access),
620 }
621 }
622}
623
624#[cfg(test)]
625impl<K> Deref for AccessPlannedQuery<K> {
626 type Target = ScalarPlan;
627
628 fn deref(&self) -> &Self::Target {
629 self.scalar_plan()
630 }
631}
632
633#[cfg(test)]
634impl<K> DerefMut for AccessPlannedQuery<K> {
635 fn deref_mut(&mut self) -> &mut Self::Target {
636 self.scalar_plan_mut()
637 }
638}
639
640pub(crate) use planner::{PlannerError, plan_access};
641
642pub(crate) trait AccessPlanProjection<K> {
650 type Output;
651
652 fn by_key(&mut self, key: &K) -> Self::Output;
653 fn by_keys(&mut self, keys: &[K]) -> Self::Output;
654 fn key_range(&mut self, start: &K, end: &K) -> Self::Output;
655 fn index_prefix(
656 &mut self,
657 index_name: &'static str,
658 index_fields: &[&'static str],
659 prefix_len: usize,
660 values: &[Value],
661 ) -> Self::Output;
662 fn index_range(
663 &mut self,
664 index_name: &'static str,
665 index_fields: &[&'static str],
666 prefix_len: usize,
667 prefix: &[Value],
668 lower: &Bound<Value>,
669 upper: &Bound<Value>,
670 ) -> Self::Output;
671 fn full_scan(&mut self) -> Self::Output;
672 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output;
673 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output;
674}
675
676pub(crate) fn project_access_plan<K, P>(plan: &AccessPlan<K>, projection: &mut P) -> P::Output
678where
679 P: AccessPlanProjection<K>,
680{
681 plan.project(projection)
682}
683
684impl<K> AccessPlan<K> {
685 fn project<P>(&self, projection: &mut P) -> P::Output
687 where
688 P: AccessPlanProjection<K>,
689 {
690 match self {
691 Self::Path(path) => path.project(projection),
692 Self::Union(children) => {
693 let children = children
694 .iter()
695 .map(|child| child.project(projection))
696 .collect();
697 projection.union(children)
698 }
699 Self::Intersection(children) => {
700 let children = children
701 .iter()
702 .map(|child| child.project(projection))
703 .collect();
704 projection.intersection(children)
705 }
706 }
707 }
708}
709
710impl<K> AccessPath<K> {
711 fn project<P>(&self, projection: &mut P) -> P::Output
713 where
714 P: AccessPlanProjection<K>,
715 {
716 match self {
717 Self::ByKey(key) => projection.by_key(key),
718 Self::ByKeys(keys) => projection.by_keys(keys),
719 Self::KeyRange { start, end } => projection.key_range(start, end),
720 Self::IndexPrefix { index, values } => {
721 projection.index_prefix(index.name, index.fields, values.len(), values)
722 }
723 Self::IndexRange { spec } => projection.index_range(
724 spec.index().name,
725 spec.index().fields,
726 spec.prefix_values().len(),
727 spec.prefix_values(),
728 spec.lower(),
729 spec.upper(),
730 ),
731 Self::FullScan => projection.full_scan(),
732 }
733 }
734}
735
736pub(crate) fn project_explain_access_path<P>(
737 access: &ExplainAccessPath,
738 projection: &mut P,
739) -> P::Output
740where
741 P: AccessPlanProjection<Value>,
742{
743 match access {
744 ExplainAccessPath::ByKey { key } => projection.by_key(key),
745 ExplainAccessPath::ByKeys { keys } => projection.by_keys(keys),
746 ExplainAccessPath::KeyRange { start, end } => projection.key_range(start, end),
747 ExplainAccessPath::IndexPrefix {
748 name,
749 fields,
750 prefix_len,
751 values,
752 } => projection.index_prefix(name, fields, *prefix_len, values),
753 ExplainAccessPath::IndexRange {
754 name,
755 fields,
756 prefix_len,
757 prefix,
758 lower,
759 upper,
760 } => projection.index_range(name, fields, *prefix_len, prefix, lower, upper),
761 ExplainAccessPath::FullScan => projection.full_scan(),
762 ExplainAccessPath::Union(children) => {
763 let children = children
764 .iter()
765 .map(|child| project_explain_access_path(child, projection))
766 .collect();
767 projection.union(children)
768 }
769 ExplainAccessPath::Intersection(children) => {
770 let children = children
771 .iter()
772 .map(|child| project_explain_access_path(child, projection))
773 .collect();
774 projection.intersection(children)
775 }
776 }
777}
778
779#[cfg(test)]
784mod access_projection_tests {
785 use super::*;
786 use crate::{model::index::IndexModel, value::Value};
787
788 const TEST_INDEX_FIELDS: [&str; 2] = ["group", "rank"];
789 const TEST_INDEX: IndexModel = IndexModel::new(
790 "tests::group_rank",
791 "tests::store",
792 &TEST_INDEX_FIELDS,
793 false,
794 );
795
796 #[derive(Default)]
797 struct AccessPlanEventProjection {
798 events: Vec<&'static str>,
799 union_child_counts: Vec<usize>,
800 intersection_child_counts: Vec<usize>,
801 seen_index: Option<(&'static str, usize, usize, usize)>,
802 }
803
804 impl AccessPlanProjection<u64> for AccessPlanEventProjection {
805 type Output = ();
806
807 fn by_key(&mut self, _key: &u64) -> Self::Output {
808 self.events.push("by_key");
809 }
810
811 fn by_keys(&mut self, keys: &[u64]) -> Self::Output {
812 self.events.push("by_keys");
813 assert_eq!(keys, [2, 3].as_slice());
814 }
815
816 fn key_range(&mut self, start: &u64, end: &u64) -> Self::Output {
817 self.events.push("key_range");
818 assert_eq!((*start, *end), (4, 9));
819 }
820
821 fn index_prefix(
822 &mut self,
823 index_name: &'static str,
824 index_fields: &[&'static str],
825 prefix_len: usize,
826 values: &[Value],
827 ) -> Self::Output {
828 self.events.push("index_prefix");
829 self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
830 }
831
832 fn index_range(
833 &mut self,
834 index_name: &'static str,
835 index_fields: &[&'static str],
836 prefix_len: usize,
837 prefix: &[Value],
838 lower: &Bound<Value>,
839 upper: &Bound<Value>,
840 ) -> Self::Output {
841 self.events.push("index_range");
842 self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
843 assert_eq!(lower, &Bound::Included(Value::Uint(8)));
844 assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
845 }
846
847 fn full_scan(&mut self) -> Self::Output {
848 self.events.push("full_scan");
849 }
850
851 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
852 self.events.push("union");
853 self.union_child_counts.push(children.len());
854 }
855
856 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
857 self.events.push("intersection");
858 self.intersection_child_counts.push(children.len());
859 }
860 }
861
862 #[test]
863 fn project_access_plan_walks_canonical_access_variants() {
864 let plan: AccessPlan<u64> = AccessPlan::Union(vec![
865 AccessPlan::path(AccessPath::ByKey(1)),
866 AccessPlan::path(AccessPath::ByKeys(vec![2, 3])),
867 AccessPlan::path(AccessPath::KeyRange { start: 4, end: 9 }),
868 AccessPlan::path(AccessPath::IndexPrefix {
869 index: TEST_INDEX,
870 values: vec![Value::Uint(7)],
871 }),
872 AccessPlan::path(AccessPath::index_range(
873 TEST_INDEX,
874 vec![Value::Uint(7)],
875 Bound::Included(Value::Uint(8)),
876 Bound::Excluded(Value::Uint(12)),
877 )),
878 AccessPlan::Intersection(vec![
879 AccessPlan::path(AccessPath::FullScan),
880 AccessPlan::path(AccessPath::ByKey(11)),
881 ]),
882 ]);
883
884 let mut projection = AccessPlanEventProjection::default();
885 project_access_plan(&plan, &mut projection);
886
887 assert_eq!(projection.union_child_counts, vec![6]);
888 assert_eq!(projection.intersection_child_counts, vec![2]);
889 assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
890 assert!(
891 projection.events.contains(&"by_key"),
892 "projection must visit by-key variants"
893 );
894 assert!(
895 projection.events.contains(&"by_keys"),
896 "projection must visit by-keys variants"
897 );
898 assert!(
899 projection.events.contains(&"key_range"),
900 "projection must visit key-range variants"
901 );
902 assert!(
903 projection.events.contains(&"index_prefix"),
904 "projection must visit index-prefix variants"
905 );
906 assert!(
907 projection.events.contains(&"index_range"),
908 "projection must visit index-range variants"
909 );
910 assert!(
911 projection.events.contains(&"full_scan"),
912 "projection must visit full-scan variants"
913 );
914 }
915
916 #[derive(Default)]
917 struct ExplainAccessEventProjection {
918 events: Vec<&'static str>,
919 union_child_counts: Vec<usize>,
920 intersection_child_counts: Vec<usize>,
921 seen_index: Option<(&'static str, usize, usize, usize)>,
922 }
923
924 impl AccessPlanProjection<Value> for ExplainAccessEventProjection {
925 type Output = ();
926
927 fn by_key(&mut self, key: &Value) -> Self::Output {
928 self.events.push("by_key");
929 assert_eq!(key, &Value::Uint(10));
930 }
931
932 fn by_keys(&mut self, keys: &[Value]) -> Self::Output {
933 self.events.push("by_keys");
934 assert_eq!(keys, [Value::Uint(20), Value::Uint(30)].as_slice());
935 }
936
937 fn key_range(&mut self, start: &Value, end: &Value) -> Self::Output {
938 self.events.push("key_range");
939 assert_eq!((start, end), (&Value::Uint(40), &Value::Uint(90)));
940 }
941
942 fn index_prefix(
943 &mut self,
944 index_name: &'static str,
945 index_fields: &[&'static str],
946 prefix_len: usize,
947 values: &[Value],
948 ) -> Self::Output {
949 self.events.push("index_prefix");
950 self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
951 }
952
953 fn index_range(
954 &mut self,
955 index_name: &'static str,
956 index_fields: &[&'static str],
957 prefix_len: usize,
958 prefix: &[Value],
959 lower: &Bound<Value>,
960 upper: &Bound<Value>,
961 ) -> Self::Output {
962 self.events.push("index_range");
963 self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
964 assert_eq!(lower, &Bound::Included(Value::Uint(8)));
965 assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
966 }
967
968 fn full_scan(&mut self) -> Self::Output {
969 self.events.push("full_scan");
970 }
971
972 fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
973 self.events.push("union");
974 self.union_child_counts.push(children.len());
975 }
976
977 fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
978 self.events.push("intersection");
979 self.intersection_child_counts.push(children.len());
980 }
981 }
982
983 #[test]
984 fn project_explain_access_path_walks_canonical_access_variants() {
985 let access = ExplainAccessPath::Union(vec![
986 ExplainAccessPath::ByKey {
987 key: Value::Uint(10),
988 },
989 ExplainAccessPath::ByKeys {
990 keys: vec![Value::Uint(20), Value::Uint(30)],
991 },
992 ExplainAccessPath::KeyRange {
993 start: Value::Uint(40),
994 end: Value::Uint(90),
995 },
996 ExplainAccessPath::IndexPrefix {
997 name: TEST_INDEX.name,
998 fields: vec!["group", "rank"],
999 prefix_len: 1,
1000 values: vec![Value::Uint(7)],
1001 },
1002 ExplainAccessPath::IndexRange {
1003 name: TEST_INDEX.name,
1004 fields: vec!["group", "rank"],
1005 prefix_len: 1,
1006 prefix: vec![Value::Uint(7)],
1007 lower: Bound::Included(Value::Uint(8)),
1008 upper: Bound::Excluded(Value::Uint(12)),
1009 },
1010 ExplainAccessPath::Intersection(vec![
1011 ExplainAccessPath::FullScan,
1012 ExplainAccessPath::ByKey {
1013 key: Value::Uint(10),
1014 },
1015 ]),
1016 ]);
1017
1018 let mut projection = ExplainAccessEventProjection::default();
1019 project_explain_access_path(&access, &mut projection);
1020
1021 assert_eq!(projection.union_child_counts, vec![6]);
1022 assert_eq!(projection.intersection_child_counts, vec![2]);
1023 assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
1024 assert!(
1025 projection.events.contains(&"by_key"),
1026 "projection must visit by-key variants"
1027 );
1028 assert!(
1029 projection.events.contains(&"by_keys"),
1030 "projection must visit by-keys variants"
1031 );
1032 assert!(
1033 projection.events.contains(&"key_range"),
1034 "projection must visit key-range variants"
1035 );
1036 assert!(
1037 projection.events.contains(&"index_prefix"),
1038 "projection must visit index-prefix variants"
1039 );
1040 assert!(
1041 projection.events.contains(&"index_range"),
1042 "projection must visit index-range variants"
1043 );
1044 assert!(
1045 projection.events.contains(&"full_scan"),
1046 "projection must visit full-scan variants"
1047 );
1048 }
1049}