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