Skip to main content

icydb_core/db/query/plan/
mod.rs

1//! Query plan contracts, planning, and validation wiring.
2
3mod 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///
30/// QueryMode
31///
32/// Discriminates load vs delete intent at planning time.
33/// Encodes mode-specific fields so invalid states are unrepresentable.
34/// Mode checks are explicit and stable at execution time.
35///
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum QueryMode {
39    Load(LoadSpec),
40    Delete(DeleteSpec),
41}
42
43impl QueryMode {
44    /// True if this mode represents a load intent.
45    #[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    /// True if this mode represents a delete intent.
54    #[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///
64/// LoadSpec
65///
66/// Mode-specific fields for load intents.
67/// Encodes pagination without leaking into delete intents.
68///
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub struct LoadSpec {
71    pub limit: Option<u32>,
72    pub offset: u32,
73}
74
75impl LoadSpec {
76    /// Create an empty load spec.
77    #[must_use]
78    pub const fn new() -> Self {
79        Self {
80            limit: None,
81            offset: 0,
82        }
83    }
84}
85
86///
87/// DeleteSpec
88///
89/// Mode-specific fields for delete intents.
90/// Encodes delete limits without leaking into load intents.
91///
92
93#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
94pub struct DeleteSpec {
95    pub limit: Option<u32>,
96}
97
98impl DeleteSpec {
99    /// Create an empty delete spec.
100    #[must_use]
101    pub const fn new() -> Self {
102        Self { limit: None }
103    }
104}
105
106///
107/// OrderDirection
108/// Executor-facing ordering direction (applied after filtering).
109///
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub enum OrderDirection {
112    Asc,
113    Desc,
114}
115
116impl OrderDirection {
117    /// Convert canonical order direction into execution scan direction.
118    #[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    /// Convert execution scan direction into canonical order direction.
127    #[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///
137/// OrderSpec
138/// Executor-facing ordering specification.
139///
140
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub(crate) struct OrderSpec {
143    pub(crate) fields: Vec<(String, OrderDirection)>,
144}
145
146///
147/// DeleteLimitSpec
148/// Executor-facing delete bound with no offsets.
149///
150
151#[derive(Clone, Copy, Debug, Eq, PartialEq)]
152pub(crate) struct DeleteLimitSpec {
153    pub max_rows: u32,
154}
155
156///
157/// PageSpec
158/// Executor-facing pagination specification.
159///
160
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub(crate) struct PageSpec {
163    pub limit: Option<u32>,
164    pub offset: u32,
165}
166
167///
168/// GroupAggregateKind
169///
170/// Declarative grouped aggregate terminal taxonomy owned by query planning.
171/// This query-layer enum intentionally avoids coupling to executor aggregate
172/// reducer internals while preserving terminal intent shape.
173///
174
175#[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///
187/// GroupAggregateSpec
188///
189/// One grouped aggregate terminal specification declared at query-plan time.
190/// `target_field` remains optional so future field-target grouped terminals can
191/// reuse this contract without mutating the wrapper shape.
192///
193#[derive(Clone, Debug, Eq, PartialEq)]
194pub(crate) struct GroupAggregateSpec {
195    pub(crate) kind: GroupAggregateKind,
196    pub(crate) target_field: Option<String>,
197}
198
199///
200/// GroupedExecutionConfig
201///
202/// Declarative grouped-execution budget policy selected by query planning.
203/// This remains planner-owned input; executor policy bridges may still apply
204/// defaults and enforcement strategy while grouped execution is scaffold-only.
205///
206#[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    /// Build one grouped execution config with explicit hard limits.
214    #[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    /// Build one unbounded grouped execution config for scaffold callers.
223    #[must_use]
224    pub(crate) const fn unbounded() -> Self {
225        Self::with_hard_limits(u64::MAX, u64::MAX)
226    }
227
228    /// Return grouped hard limit for maximum groups.
229    #[must_use]
230    pub(crate) const fn max_groups(&self) -> u64 {
231        self.max_groups
232    }
233
234    /// Return grouped hard limit for estimated grouped bytes.
235    #[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///
248/// GroupSpec
249///
250/// Declarative GROUP BY stage contract attached to a validated base plan.
251/// This wrapper is intentionally semantic-only; field-slot resolution and
252/// execution-mode derivation remain executor-owned boundaries.
253///
254#[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///
262/// LogicalPlan
263///
264/// Pure logical query intent produced by the planner.
265///
266/// A `LogicalPlan` represents the access-independent query semantics:
267/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
268/// and read-consistency mode.
269///
270/// Design notes:
271/// - Predicates are applied *after* data access
272/// - Ordering is applied after filtering
273/// - Pagination is applied after ordering (load only)
274/// - Delete limits are applied after ordering (delete only)
275/// - Missing-row policy is explicit and must not depend on access strategy
276///
277/// This struct is the logical compiler stage output and intentionally excludes
278/// access-path details.
279///
280#[derive(Clone, Debug, Eq, PartialEq)]
281pub(crate) struct LogicalPlan {
282    /// Load vs delete intent.
283    pub(crate) mode: QueryMode,
284
285    /// Optional residual predicate applied after access.
286    pub(crate) predicate: Option<PredicateExecutionModel>,
287
288    /// Optional ordering specification.
289    pub(crate) order: Option<OrderSpec>,
290
291    /// Optional distinct semantics over ordered rows.
292    pub(crate) distinct: bool,
293
294    /// Optional delete bound (delete intents only).
295    pub(crate) delete_limit: Option<DeleteLimitSpec>,
296
297    /// Optional pagination specification.
298    pub(crate) page: Option<PageSpec>,
299
300    /// Missing-row policy for execution.
301    pub(crate) consistency: ReadConsistency,
302}
303
304///
305/// AccessPlannedQuery
306///
307/// Access-planned query produced after access-path selection.
308/// Binds one pure `LogicalPlan` to one chosen `AccessPlan`.
309///
310#[derive(Clone, Debug, Eq, PartialEq)]
311pub(crate) struct AccessPlannedQuery<K> {
312    pub(crate) logical: LogicalPlan,
313    pub(crate) access: AccessPlan<K>,
314}
315
316///
317/// GroupedPlan
318///
319/// Declarative grouped wrapper around one access-planned base query.
320/// This wrapper keeps GROUP BY scaffolding additive and low-churn while
321/// leaving existing `LogicalPlan` literals unchanged.
322///
323#[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    /// Construct an access-planned query from logical + access stages.
331    #[must_use]
332    pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
333        Self { logical, access }
334    }
335
336    /// Decompose into logical + access stages.
337    #[must_use]
338    pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
339        (self.logical, self.access)
340    }
341
342    /// Construct a minimal access-planned query with only an access path.
343    ///
344    /// Predicates, ordering, and pagination may be attached later.
345    #[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    /// Build one continuation boundary from one entity using canonical order.
365    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    /// Build one grouped query wrapper from base access plan + group spec.
384    #[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
426// Core matcher for secondary ORDER BY pushdown eligibility.
427fn 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
503// Evaluate pushdown eligibility for ORDER BY + single index-prefix shapes.
504fn 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
514// Evaluate secondary ORDER BY pushdown over one access-planned query.
515fn 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
577/// Evaluate the secondary-index ORDER BY pushdown matrix for one plan.
578pub(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
590/// Derive pushdown applicability from one plan already validated by planner semantics.
591pub(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)]
604/// Evaluate pushdown eligibility only when secondary-index ORDER BY is applicable.
605pub(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/// Evaluate pushdown applicability for plans that have already passed full
613/// logical/executor validation.
614#[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
624///
625/// AccessPlanProjection
626///
627/// Shared visitor for projecting `AccessPlan` / `AccessPath` into
628/// diagnostics-specific representations.
629///
630
631pub(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
658/// Project an access plan by exhaustively walking canonical access variants.
659pub(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    // Project this plan by recursively visiting all access nodes.
668    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    // Project one concrete path variant via the shared projection surface.
694    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///
762/// TESTS
763///
764
765#[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}