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;
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
30/// Re-Exports
31pub(crate) use validate::PlanError;
32
33/// validate
34///
35/// Semantic query-plan validation namespace.
36///
37/// This explicit nested namespace keeps semantic validation ownership
38/// anchored under query planning (`db::query::plan::validate`).
39pub(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///
48/// QueryMode
49///
50/// Discriminates load vs delete intent at planning time.
51/// Encodes mode-specific fields so invalid states are unrepresentable.
52/// Mode checks are explicit and stable at execution time.
53///
54#[derive(Clone, Copy, Debug, Eq, PartialEq)]
55pub enum QueryMode {
56    Load(LoadSpec),
57    Delete(DeleteSpec),
58}
59
60impl QueryMode {
61    /// True if this mode represents a load intent.
62    #[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    /// True if this mode represents a delete intent.
71    #[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///
81/// LoadSpec
82///
83/// Mode-specific fields for load intents.
84/// Encodes pagination without leaking into delete intents.
85///
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
87pub struct LoadSpec {
88    pub limit: Option<u32>,
89    pub offset: u32,
90}
91
92impl LoadSpec {
93    /// Create an empty load spec.
94    #[must_use]
95    pub const fn new() -> Self {
96        Self {
97            limit: None,
98            offset: 0,
99        }
100    }
101}
102
103///
104/// DeleteSpec
105///
106/// Mode-specific fields for delete intents.
107/// Encodes delete limits without leaking into load intents.
108///
109
110#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
111pub struct DeleteSpec {
112    pub limit: Option<u32>,
113}
114
115impl DeleteSpec {
116    /// Create an empty delete spec.
117    #[must_use]
118    pub const fn new() -> Self {
119        Self { limit: None }
120    }
121}
122
123///
124/// OrderDirection
125/// Executor-facing ordering direction (applied after filtering).
126///
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub enum OrderDirection {
129    Asc,
130    Desc,
131}
132
133impl OrderDirection {
134    /// Convert canonical order direction into execution scan direction.
135    #[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    /// Convert execution scan direction into canonical order direction.
144    #[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///
154/// OrderSpec
155/// Executor-facing ordering specification.
156///
157
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub(crate) struct OrderSpec {
160    pub(crate) fields: Vec<(String, OrderDirection)>,
161}
162
163///
164/// DeleteLimitSpec
165/// Executor-facing delete bound with no offsets.
166///
167
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169pub(crate) struct DeleteLimitSpec {
170    pub max_rows: u32,
171}
172
173///
174/// PageSpec
175/// Executor-facing pagination specification.
176///
177
178#[derive(Clone, Debug, Eq, PartialEq)]
179pub(crate) struct PageSpec {
180    pub limit: Option<u32>,
181    pub offset: u32,
182}
183
184///
185/// GroupAggregateKind
186///
187/// Declarative grouped aggregate terminal taxonomy owned by query planning.
188/// This query-layer enum intentionally avoids coupling to executor aggregate
189/// reducer internals while preserving terminal intent shape.
190///
191
192#[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///
204/// GroupAggregateSpec
205///
206/// One grouped aggregate terminal specification declared at query-plan time.
207/// `target_field` remains optional so future field-target grouped terminals can
208/// reuse this contract without mutating the wrapper shape.
209///
210#[derive(Clone, Debug, Eq, PartialEq)]
211pub(crate) struct GroupAggregateSpec {
212    pub(crate) kind: GroupAggregateKind,
213    pub(crate) target_field: Option<String>,
214}
215
216///
217/// GroupedExecutionConfig
218///
219/// Declarative grouped-execution budget policy selected by query planning.
220/// This remains planner-owned input; executor policy bridges may still apply
221/// defaults and enforcement strategy while grouped execution is scaffold-only.
222///
223#[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    /// Build one grouped execution config with explicit hard limits.
231    #[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    /// Build one unbounded grouped execution config for scaffold callers.
240    #[must_use]
241    pub(crate) const fn unbounded() -> Self {
242        Self::with_hard_limits(u64::MAX, u64::MAX)
243    }
244
245    /// Return grouped hard limit for maximum groups.
246    #[must_use]
247    pub(crate) const fn max_groups(&self) -> u64 {
248        self.max_groups
249    }
250
251    /// Return grouped hard limit for estimated grouped bytes.
252    #[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///
265/// GroupSpec
266///
267/// Declarative GROUP BY stage contract attached to a validated base plan.
268/// This wrapper is intentionally semantic-only; field-slot resolution and
269/// execution-mode derivation remain executor-owned boundaries.
270///
271#[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///
279/// LogicalPlan
280///
281/// Pure logical query intent produced by the planner.
282///
283/// A `LogicalPlan` represents the access-independent query semantics:
284/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
285/// and read-consistency mode.
286///
287/// Design notes:
288/// - Predicates are applied *after* data access
289/// - Ordering is applied after filtering
290/// - Pagination is applied after ordering (load only)
291/// - Delete limits are applied after ordering (delete only)
292/// - Missing-row policy is explicit and must not depend on access strategy
293///
294/// This struct is the logical compiler stage output and intentionally excludes
295/// access-path details.
296///
297#[derive(Clone, Debug, Eq, PartialEq)]
298pub(crate) struct LogicalPlan {
299    /// Load vs delete intent.
300    pub(crate) mode: QueryMode,
301
302    /// Optional residual predicate applied after access.
303    pub(crate) predicate: Option<PredicateExecutionModel>,
304
305    /// Optional ordering specification.
306    pub(crate) order: Option<OrderSpec>,
307
308    /// Optional distinct semantics over ordered rows.
309    pub(crate) distinct: bool,
310
311    /// Optional delete bound (delete intents only).
312    pub(crate) delete_limit: Option<DeleteLimitSpec>,
313
314    /// Optional pagination specification.
315    pub(crate) page: Option<PageSpec>,
316
317    /// Missing-row policy for execution.
318    pub(crate) consistency: ReadConsistency,
319}
320
321///
322/// AccessPlannedQuery
323///
324/// Access-planned query produced after access-path selection.
325/// Binds one pure `LogicalPlan` to one chosen `AccessPlan`.
326///
327#[derive(Clone, Debug, Eq, PartialEq)]
328pub(crate) struct AccessPlannedQuery<K> {
329    pub(crate) logical: LogicalPlan,
330    pub(crate) access: AccessPlan<K>,
331}
332
333///
334/// GroupedPlan
335///
336/// Declarative grouped wrapper around one access-planned base query.
337/// This wrapper keeps GROUP BY scaffolding additive and low-churn while
338/// leaving existing `LogicalPlan` literals unchanged.
339///
340#[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    /// Construct an access-planned query from logical + access stages.
348    #[must_use]
349    pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
350        Self { logical, access }
351    }
352
353    /// Decompose into logical + access stages.
354    #[must_use]
355    pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
356        (self.logical, self.access)
357    }
358
359    /// Construct a minimal access-planned query with only an access path.
360    ///
361    /// Predicates, ordering, and pagination may be attached later.
362    #[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    /// Build one continuation boundary from one entity using canonical order.
382    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    /// Build one grouped query wrapper from base access plan + group spec.
401    #[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
443// Core matcher for secondary ORDER BY pushdown eligibility.
444fn 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
520// Evaluate pushdown eligibility for ORDER BY + single index-prefix shapes.
521fn 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
531// Evaluate secondary ORDER BY pushdown over one access-planned query.
532fn 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
591/// Evaluate the secondary-index ORDER BY pushdown matrix for one plan.
592pub(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
604/// Derive pushdown applicability from one plan already validated by planner semantics.
605pub(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)]
618/// Evaluate pushdown eligibility only when secondary-index ORDER BY is applicable.
619pub(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/// Evaluate pushdown applicability for plans that have already passed full
627/// logical/executor validation.
628#[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
638///
639/// AccessPlanProjection
640///
641/// Shared visitor for projecting `AccessPlan` / `AccessPath` into
642/// diagnostics-specific representations.
643///
644
645pub(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
672/// Project an access plan by exhaustively walking canonical access variants.
673pub(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    // Project this plan by recursively visiting all access nodes.
682    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    // Project one concrete path variant via the shared projection surface.
708    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///
776/// TESTS
777///
778
779#[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}