Skip to main content

icydb_core/db/query/plan/
mod.rs

1//! Module: query::plan
2//! Responsibility: logical query plan contracts, planning, and validation wiring.
3//! Does not own: executor runtime behavior.
4//! Boundary: intent/explain layers produce and consume these plan contracts.
5
6mod group;
7mod planner;
8mod pushdown;
9#[cfg(test)]
10mod tests;
11pub(crate) mod validate;
12
13use crate::{
14    db::{
15        access::{AccessPath, AccessPlan},
16        direction::Direction,
17        predicate::{MissingRowPolicy, PredicateExecutionModel},
18        query::explain::ExplainAccessPath,
19    },
20    model::entity::{EntityModel, resolve_field_slot},
21    value::Value,
22};
23use std::ops::Bound;
24#[cfg(test)]
25use std::ops::{Deref, DerefMut};
26
27pub(in crate::db) use group::{GroupedExecutorHandoff, grouped_executor_handoff};
28pub(crate) use pushdown::assess_secondary_order_pushdown_from_parts;
29pub(in crate::db) use pushdown::derive_secondary_pushdown_applicability_validated;
30#[cfg(test)]
31pub(crate) use pushdown::{
32    assess_secondary_order_pushdown, assess_secondary_order_pushdown_if_applicable,
33    assess_secondary_order_pushdown_if_applicable_validated,
34};
35pub use validate::PlanError;
36pub(crate) use validate::{GroupPlanError, validate_group_query_semantics};
37
38///
39/// QueryMode
40///
41/// Discriminates load vs delete intent at planning time.
42/// Encodes mode-specific fields so invalid states are unrepresentable.
43/// Mode checks are explicit and stable at execution time.
44///
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum QueryMode {
48    Load(LoadSpec),
49    Delete(DeleteSpec),
50}
51
52impl QueryMode {
53    /// True if this mode represents a load intent.
54    #[must_use]
55    pub const fn is_load(&self) -> bool {
56        match self {
57            Self::Load(_) => true,
58            Self::Delete(_) => false,
59        }
60    }
61
62    /// True if this mode represents a delete intent.
63    #[must_use]
64    pub const fn is_delete(&self) -> bool {
65        match self {
66            Self::Delete(_) => true,
67            Self::Load(_) => false,
68        }
69    }
70}
71
72///
73/// LoadSpec
74///
75/// Mode-specific fields for load intents.
76/// Encodes pagination without leaking into delete intents.
77///
78#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
79pub struct LoadSpec {
80    pub limit: Option<u32>,
81    pub offset: u32,
82}
83
84impl LoadSpec {
85    /// Create an empty load spec.
86    #[must_use]
87    pub const fn new() -> Self {
88        Self {
89            limit: None,
90            offset: 0,
91        }
92    }
93}
94
95///
96/// DeleteSpec
97///
98/// Mode-specific fields for delete intents.
99/// Encodes delete limits without leaking into load intents.
100///
101
102#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
103pub struct DeleteSpec {
104    pub limit: Option<u32>,
105}
106
107impl DeleteSpec {
108    /// Create an empty delete spec.
109    #[must_use]
110    pub const fn new() -> Self {
111        Self { limit: None }
112    }
113}
114
115///
116/// OrderDirection
117/// Executor-facing ordering direction (applied after filtering).
118///
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum OrderDirection {
121    Asc,
122    Desc,
123}
124
125impl OrderDirection {
126    /// Convert canonical order direction into execution scan direction.
127    #[must_use]
128    pub(in crate::db) const fn as_direction(self) -> Direction {
129        match self {
130            Self::Asc => Direction::Asc,
131            Self::Desc => Direction::Desc,
132        }
133    }
134
135    /// Convert execution scan direction into canonical order direction.
136    #[must_use]
137    pub(in crate::db) const fn from_direction(direction: Direction) -> Self {
138        match direction {
139            Direction::Asc => Self::Asc,
140            Direction::Desc => Self::Desc,
141        }
142    }
143}
144
145///
146/// OrderSpec
147/// Executor-facing ordering specification.
148///
149
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub(crate) struct OrderSpec {
152    pub(crate) fields: Vec<(String, OrderDirection)>,
153}
154
155///
156/// DeleteLimitSpec
157/// Executor-facing delete bound with no offsets.
158///
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub(crate) struct DeleteLimitSpec {
162    pub max_rows: u32,
163}
164
165///
166/// PageSpec
167/// Executor-facing pagination specification.
168///
169
170#[derive(Clone, Debug, Eq, PartialEq)]
171pub(crate) struct PageSpec {
172    pub limit: Option<u32>,
173    pub offset: u32,
174}
175
176///
177/// AggregateKind
178///
179/// Canonical aggregate terminal taxonomy owned by query planning.
180/// All layers (query, explain, fingerprint, executor) must interpret aggregate
181/// terminal semantics through this single enum authority.
182/// Executor must derive traversal and fold direction exclusively from this enum.
183///
184
185#[allow(dead_code)]
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub enum AggregateKind {
188    Count,
189    Exists,
190    Min,
191    Max,
192    First,
193    Last,
194}
195
196impl AggregateKind {
197    /// Return whether this terminal kind is `COUNT`.
198    #[must_use]
199    pub(in crate::db) const fn is_count(self) -> bool {
200        matches!(self, Self::Count)
201    }
202
203    /// Return whether this terminal kind supports explicit field targets.
204    #[must_use]
205    pub(in crate::db) const fn supports_field_targets(self) -> bool {
206        matches!(self, Self::Min | Self::Max)
207    }
208
209    /// Return whether this terminal kind belongs to the extrema family.
210    #[must_use]
211    pub(in crate::db) const fn is_extrema(self) -> bool {
212        self.supports_field_targets()
213    }
214
215    /// Return whether this terminal kind supports first/last value projection.
216    #[must_use]
217    pub(in crate::db) const fn supports_terminal_value_projection(self) -> bool {
218        matches!(self, Self::First | Self::Last)
219    }
220
221    /// Return whether reducer updates for this kind require a decoded id payload.
222    #[must_use]
223    pub(in crate::db) const fn requires_decoded_id(self) -> bool {
224        !matches!(self, Self::Count | Self::Exists)
225    }
226
227    /// Return the canonical extrema traversal direction for this terminal kind.
228    #[must_use]
229    pub(in crate::db) const fn extrema_direction(self) -> Option<Direction> {
230        match self {
231            Self::Min => Some(Direction::Asc),
232            Self::Max => Some(Direction::Desc),
233            Self::Count | Self::Exists | Self::First | Self::Last => None,
234        }
235    }
236
237    /// Return the canonical non-short-circuit materialized reduction direction.
238    #[must_use]
239    pub(in crate::db) const fn materialized_fold_direction(self) -> Direction {
240        match self {
241            Self::Min => Direction::Desc,
242            Self::Count | Self::Exists | Self::Max | Self::First | Self::Last => Direction::Asc,
243        }
244    }
245
246    /// Return the canonical grouped aggregate fingerprint tag (v1).
247    #[must_use]
248    pub(in crate::db) const fn fingerprint_tag_v1(self) -> u8 {
249        match self {
250            Self::Count => 0x01,
251            Self::Exists => 0x02,
252            Self::Min => 0x03,
253            Self::Max => 0x04,
254            Self::First => 0x05,
255            Self::Last => 0x06,
256        }
257    }
258
259    /// Return true when this kind can use bounded aggregate probe hints.
260    #[must_use]
261    pub(in crate::db) const fn supports_bounded_probe_hint(self) -> bool {
262        !self.is_count()
263    }
264
265    /// Derive a bounded aggregate probe fetch hint for this kind.
266    #[must_use]
267    pub(in crate::db) fn bounded_probe_fetch_hint(
268        self,
269        direction: Direction,
270        offset: usize,
271        page_limit: Option<usize>,
272    ) -> Option<usize> {
273        match self {
274            Self::Exists | Self::First => Some(offset.saturating_add(1)),
275            Self::Min if direction == Direction::Asc => Some(offset.saturating_add(1)),
276            Self::Max if direction == Direction::Desc => Some(offset.saturating_add(1)),
277            Self::Last => page_limit.map(|limit| offset.saturating_add(limit)),
278            Self::Count | Self::Min | Self::Max => None,
279        }
280    }
281}
282
283/// Compatibility alias for grouped planning callsites.
284pub(crate) type GroupAggregateKind = AggregateKind;
285
286///
287/// GroupAggregateSpec
288///
289/// One grouped aggregate terminal specification declared at query-plan time.
290/// `target_field` remains optional so future field-target grouped terminals can
291/// reuse this contract without mutating the wrapper shape.
292///
293
294#[derive(Clone, Debug, Eq, PartialEq)]
295pub(crate) struct GroupAggregateSpec {
296    pub(crate) kind: AggregateKind,
297    pub(crate) target_field: Option<String>,
298}
299
300impl GroupAggregateSpec {
301    /// Return the canonical grouped aggregate terminal kind.
302    #[must_use]
303    pub(crate) const fn kind(&self) -> AggregateKind {
304        self.kind
305    }
306
307    /// Return the optional grouped aggregate target field.
308    #[must_use]
309    pub(crate) fn target_field(&self) -> Option<&str> {
310        self.target_field.as_deref()
311    }
312}
313
314///
315/// FieldSlot
316///
317/// Canonical resolved field reference used by logical planning.
318/// `index` is the stable slot in `EntityModel::fields`; `field` is retained
319/// for diagnostics and explain surfaces.
320///
321
322#[derive(Clone, Debug, Eq, PartialEq)]
323pub(crate) struct FieldSlot {
324    index: usize,
325    field: String,
326}
327
328impl FieldSlot {
329    /// Resolve one field name into its canonical model slot.
330    #[must_use]
331    pub(crate) fn resolve(model: &EntityModel, field: &str) -> Option<Self> {
332        let index = resolve_field_slot(model, field)?;
333        let canonical = model
334            .fields
335            .get(index)
336            .map_or(field, |model_field| model_field.name);
337
338        Some(Self {
339            index,
340            field: canonical.to_string(),
341        })
342    }
343
344    /// Build one field slot directly for tests that need invalid slot shapes.
345    #[cfg(test)]
346    #[must_use]
347    pub(crate) fn from_parts_for_test(index: usize, field: impl Into<String>) -> Self {
348        Self {
349            index,
350            field: field.into(),
351        }
352    }
353
354    /// Return the stable slot index in `EntityModel::fields`.
355    #[must_use]
356    pub(crate) const fn index(&self) -> usize {
357        self.index
358    }
359
360    /// Return the diagnostic field label associated with this slot.
361    #[must_use]
362    pub(crate) fn field(&self) -> &str {
363        &self.field
364    }
365}
366
367///
368/// GroupedExecutionConfig
369///
370/// Declarative grouped-execution budget policy selected by query planning.
371/// This remains planner-owned input; executor policy bridges may still apply
372/// defaults and enforcement strategy at runtime boundaries.
373///
374
375#[derive(Clone, Copy, Debug, Eq, PartialEq)]
376pub(crate) struct GroupedExecutionConfig {
377    pub(crate) max_groups: u64,
378    pub(crate) max_group_bytes: u64,
379}
380
381impl GroupedExecutionConfig {
382    /// Build one grouped execution config with explicit hard limits.
383    #[must_use]
384    pub(crate) const fn with_hard_limits(max_groups: u64, max_group_bytes: u64) -> Self {
385        Self {
386            max_groups,
387            max_group_bytes,
388        }
389    }
390
391    /// Build one unbounded grouped execution config.
392    #[must_use]
393    pub(crate) const fn unbounded() -> Self {
394        Self::with_hard_limits(u64::MAX, u64::MAX)
395    }
396
397    /// Return grouped hard limit for maximum groups.
398    #[must_use]
399    pub(crate) const fn max_groups(&self) -> u64 {
400        self.max_groups
401    }
402
403    /// Return grouped hard limit for estimated grouped bytes.
404    #[must_use]
405    pub(crate) const fn max_group_bytes(&self) -> u64 {
406        self.max_group_bytes
407    }
408}
409
410impl Default for GroupedExecutionConfig {
411    fn default() -> Self {
412        Self::unbounded()
413    }
414}
415
416///
417/// GroupSpec
418///
419/// Declarative GROUP BY stage contract attached to a validated base plan.
420/// This wrapper is intentionally semantic-only; field-slot resolution and
421/// execution-mode derivation remain executor-owned boundaries.
422///
423
424#[derive(Clone, Debug, Eq, PartialEq)]
425pub(crate) struct GroupSpec {
426    pub(crate) group_fields: Vec<FieldSlot>,
427    pub(crate) aggregates: Vec<GroupAggregateSpec>,
428    pub(crate) execution: GroupedExecutionConfig,
429}
430
431///
432/// ScalarPlan
433///
434/// Pure scalar logical query intent produced by the planner.
435///
436/// A `ScalarPlan` represents the access-independent query semantics:
437/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
438/// and read-consistency mode.
439///
440/// Design notes:
441/// - Predicates are applied *after* data access
442/// - Ordering is applied after filtering
443/// - Pagination is applied after ordering (load only)
444/// - Delete limits are applied after ordering (delete only)
445/// - Missing-row policy is explicit and must not depend on access strategy
446///
447/// This struct is the logical compiler stage output and intentionally excludes
448/// access-path details.
449///
450
451#[derive(Clone, Debug, Eq, PartialEq)]
452pub(crate) struct ScalarPlan {
453    /// Load vs delete intent.
454    pub(crate) mode: QueryMode,
455
456    /// Optional residual predicate applied after access.
457    pub(crate) predicate: Option<PredicateExecutionModel>,
458
459    /// Optional ordering specification.
460    pub(crate) order: Option<OrderSpec>,
461
462    /// Optional distinct semantics over ordered rows.
463    pub(crate) distinct: bool,
464
465    /// Optional delete bound (delete intents only).
466    pub(crate) delete_limit: Option<DeleteLimitSpec>,
467
468    /// Optional pagination specification.
469    pub(crate) page: Option<PageSpec>,
470
471    /// Missing-row policy for execution.
472    pub(crate) consistency: MissingRowPolicy,
473}
474
475///
476/// GroupPlan
477///
478/// Pure grouped logical intent emitted by grouped planning.
479/// Group metadata is carried through one canonical `GroupSpec` contract.
480///
481
482#[derive(Clone, Debug, Eq, PartialEq)]
483pub(crate) struct GroupPlan {
484    pub(crate) scalar: ScalarPlan,
485    pub(crate) group: GroupSpec,
486}
487
488///
489/// LogicalPlan
490///
491/// Exclusive logical query intent emitted by planning.
492/// Scalar and grouped semantics are distinct variants by construction.
493///
494
495#[derive(Clone, Debug, Eq, PartialEq)]
496pub(crate) enum LogicalPlan {
497    Scalar(ScalarPlan),
498    Grouped(GroupPlan),
499}
500
501impl LogicalPlan {
502    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
503    #[must_use]
504    pub(in crate::db) const fn scalar_semantics(&self) -> &ScalarPlan {
505        match self {
506            Self::Scalar(plan) => plan,
507            Self::Grouped(plan) => &plan.scalar,
508        }
509    }
510
511    /// Borrow scalar semantic fields mutably across logical variants.
512    #[must_use]
513    #[cfg(test)]
514    pub(in crate::db) const fn scalar_semantics_mut(&mut self) -> &mut ScalarPlan {
515        match self {
516            Self::Scalar(plan) => plan,
517            Self::Grouped(plan) => &mut plan.scalar,
518        }
519    }
520}
521
522#[cfg(test)]
523impl Deref for LogicalPlan {
524    type Target = ScalarPlan;
525
526    fn deref(&self) -> &Self::Target {
527        self.scalar_semantics()
528    }
529}
530
531#[cfg(test)]
532impl DerefMut for LogicalPlan {
533    fn deref_mut(&mut self) -> &mut Self::Target {
534        self.scalar_semantics_mut()
535    }
536}
537
538///
539/// AccessPlannedQuery
540///
541/// Access-planned query produced after access-path selection.
542/// Binds one pure `LogicalPlan` to one chosen `AccessPlan`.
543///
544
545#[derive(Clone, Debug, Eq, PartialEq)]
546pub(crate) struct AccessPlannedQuery<K> {
547    pub(crate) logical: LogicalPlan,
548    pub(crate) access: AccessPlan<K>,
549}
550
551impl<K> AccessPlannedQuery<K> {
552    /// Construct an access-planned query from logical + access stages.
553    #[must_use]
554    pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
555        Self { logical, access }
556    }
557
558    /// Decompose into logical + access stages.
559    #[must_use]
560    pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
561        (self.logical, self.access)
562    }
563
564    /// Convert this plan into grouped logical form with one explicit group spec.
565    #[must_use]
566    pub(in crate::db) fn into_grouped(self, group: GroupSpec) -> Self {
567        let Self { logical, access } = self;
568        let scalar = match logical {
569            LogicalPlan::Scalar(plan) => plan,
570            LogicalPlan::Grouped(plan) => plan.scalar,
571        };
572
573        Self {
574            logical: LogicalPlan::Grouped(GroupPlan { scalar, group }),
575            access,
576        }
577    }
578
579    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
580    #[must_use]
581    pub(in crate::db) const fn scalar_plan(&self) -> &ScalarPlan {
582        self.logical.scalar_semantics()
583    }
584
585    /// Borrow grouped semantic fields when this plan is grouped.
586    #[must_use]
587    pub(in crate::db) const fn grouped_plan(&self) -> Option<&GroupPlan> {
588        match &self.logical {
589            LogicalPlan::Scalar(_) => None,
590            LogicalPlan::Grouped(plan) => Some(plan),
591        }
592    }
593
594    /// Borrow scalar semantic fields mutably across logical variants.
595    #[must_use]
596    #[cfg(test)]
597    pub(in crate::db) const fn scalar_plan_mut(&mut self) -> &mut ScalarPlan {
598        self.logical.scalar_semantics_mut()
599    }
600
601    /// Construct a minimal access-planned query with only an access path.
602    ///
603    /// Predicates, ordering, and pagination may be attached later.
604    #[cfg(test)]
605    pub(crate) fn new(
606        access: crate::db::access::AccessPath<K>,
607        consistency: MissingRowPolicy,
608    ) -> Self {
609        Self {
610            logical: LogicalPlan::Scalar(crate::db::query::plan::ScalarPlan {
611                mode: QueryMode::Load(LoadSpec::new()),
612                predicate: None,
613                order: None,
614                distinct: false,
615                delete_limit: None,
616                page: None,
617                consistency,
618            }),
619            access: AccessPlan::path(access),
620        }
621    }
622}
623
624#[cfg(test)]
625impl<K> Deref for AccessPlannedQuery<K> {
626    type Target = ScalarPlan;
627
628    fn deref(&self) -> &Self::Target {
629        self.scalar_plan()
630    }
631}
632
633#[cfg(test)]
634impl<K> DerefMut for AccessPlannedQuery<K> {
635    fn deref_mut(&mut self) -> &mut Self::Target {
636        self.scalar_plan_mut()
637    }
638}
639
640pub(crate) use planner::{PlannerError, plan_access};
641
642///
643/// AccessPlanProjection
644///
645/// Shared visitor for projecting `AccessPlan` / `AccessPath` into
646/// diagnostics-specific representations.
647///
648
649pub(crate) trait AccessPlanProjection<K> {
650    type Output;
651
652    fn by_key(&mut self, key: &K) -> Self::Output;
653    fn by_keys(&mut self, keys: &[K]) -> Self::Output;
654    fn key_range(&mut self, start: &K, end: &K) -> Self::Output;
655    fn index_prefix(
656        &mut self,
657        index_name: &'static str,
658        index_fields: &[&'static str],
659        prefix_len: usize,
660        values: &[Value],
661    ) -> Self::Output;
662    fn index_range(
663        &mut self,
664        index_name: &'static str,
665        index_fields: &[&'static str],
666        prefix_len: usize,
667        prefix: &[Value],
668        lower: &Bound<Value>,
669        upper: &Bound<Value>,
670    ) -> Self::Output;
671    fn full_scan(&mut self) -> Self::Output;
672    fn union(&mut self, children: Vec<Self::Output>) -> Self::Output;
673    fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output;
674}
675
676/// Project an access plan by exhaustively walking canonical access variants.
677pub(crate) fn project_access_plan<K, P>(plan: &AccessPlan<K>, projection: &mut P) -> P::Output
678where
679    P: AccessPlanProjection<K>,
680{
681    plan.project(projection)
682}
683
684impl<K> AccessPlan<K> {
685    // Project this plan by recursively visiting all access nodes.
686    fn project<P>(&self, projection: &mut P) -> P::Output
687    where
688        P: AccessPlanProjection<K>,
689    {
690        match self {
691            Self::Path(path) => path.project(projection),
692            Self::Union(children) => {
693                let children = children
694                    .iter()
695                    .map(|child| child.project(projection))
696                    .collect();
697                projection.union(children)
698            }
699            Self::Intersection(children) => {
700                let children = children
701                    .iter()
702                    .map(|child| child.project(projection))
703                    .collect();
704                projection.intersection(children)
705            }
706        }
707    }
708}
709
710impl<K> AccessPath<K> {
711    // Project one concrete path variant via the shared projection surface.
712    fn project<P>(&self, projection: &mut P) -> P::Output
713    where
714        P: AccessPlanProjection<K>,
715    {
716        match self {
717            Self::ByKey(key) => projection.by_key(key),
718            Self::ByKeys(keys) => projection.by_keys(keys),
719            Self::KeyRange { start, end } => projection.key_range(start, end),
720            Self::IndexPrefix { index, values } => {
721                projection.index_prefix(index.name, index.fields, values.len(), values)
722            }
723            Self::IndexRange { spec } => projection.index_range(
724                spec.index().name,
725                spec.index().fields,
726                spec.prefix_values().len(),
727                spec.prefix_values(),
728                spec.lower(),
729                spec.upper(),
730            ),
731            Self::FullScan => projection.full_scan(),
732        }
733    }
734}
735
736pub(crate) fn project_explain_access_path<P>(
737    access: &ExplainAccessPath,
738    projection: &mut P,
739) -> P::Output
740where
741    P: AccessPlanProjection<Value>,
742{
743    match access {
744        ExplainAccessPath::ByKey { key } => projection.by_key(key),
745        ExplainAccessPath::ByKeys { keys } => projection.by_keys(keys),
746        ExplainAccessPath::KeyRange { start, end } => projection.key_range(start, end),
747        ExplainAccessPath::IndexPrefix {
748            name,
749            fields,
750            prefix_len,
751            values,
752        } => projection.index_prefix(name, fields, *prefix_len, values),
753        ExplainAccessPath::IndexRange {
754            name,
755            fields,
756            prefix_len,
757            prefix,
758            lower,
759            upper,
760        } => projection.index_range(name, fields, *prefix_len, prefix, lower, upper),
761        ExplainAccessPath::FullScan => projection.full_scan(),
762        ExplainAccessPath::Union(children) => {
763            let children = children
764                .iter()
765                .map(|child| project_explain_access_path(child, projection))
766                .collect();
767            projection.union(children)
768        }
769        ExplainAccessPath::Intersection(children) => {
770            let children = children
771                .iter()
772                .map(|child| project_explain_access_path(child, projection))
773                .collect();
774            projection.intersection(children)
775        }
776    }
777}
778
779///
780/// TESTS
781///
782
783#[cfg(test)]
784mod access_projection_tests {
785    use super::*;
786    use crate::{model::index::IndexModel, value::Value};
787
788    const TEST_INDEX_FIELDS: [&str; 2] = ["group", "rank"];
789    const TEST_INDEX: IndexModel = IndexModel::new(
790        "tests::group_rank",
791        "tests::store",
792        &TEST_INDEX_FIELDS,
793        false,
794    );
795
796    #[derive(Default)]
797    struct AccessPlanEventProjection {
798        events: Vec<&'static str>,
799        union_child_counts: Vec<usize>,
800        intersection_child_counts: Vec<usize>,
801        seen_index: Option<(&'static str, usize, usize, usize)>,
802    }
803
804    impl AccessPlanProjection<u64> for AccessPlanEventProjection {
805        type Output = ();
806
807        fn by_key(&mut self, _key: &u64) -> Self::Output {
808            self.events.push("by_key");
809        }
810
811        fn by_keys(&mut self, keys: &[u64]) -> Self::Output {
812            self.events.push("by_keys");
813            assert_eq!(keys, [2, 3].as_slice());
814        }
815
816        fn key_range(&mut self, start: &u64, end: &u64) -> Self::Output {
817            self.events.push("key_range");
818            assert_eq!((*start, *end), (4, 9));
819        }
820
821        fn index_prefix(
822            &mut self,
823            index_name: &'static str,
824            index_fields: &[&'static str],
825            prefix_len: usize,
826            values: &[Value],
827        ) -> Self::Output {
828            self.events.push("index_prefix");
829            self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
830        }
831
832        fn index_range(
833            &mut self,
834            index_name: &'static str,
835            index_fields: &[&'static str],
836            prefix_len: usize,
837            prefix: &[Value],
838            lower: &Bound<Value>,
839            upper: &Bound<Value>,
840        ) -> Self::Output {
841            self.events.push("index_range");
842            self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
843            assert_eq!(lower, &Bound::Included(Value::Uint(8)));
844            assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
845        }
846
847        fn full_scan(&mut self) -> Self::Output {
848            self.events.push("full_scan");
849        }
850
851        fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
852            self.events.push("union");
853            self.union_child_counts.push(children.len());
854        }
855
856        fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
857            self.events.push("intersection");
858            self.intersection_child_counts.push(children.len());
859        }
860    }
861
862    #[test]
863    fn project_access_plan_walks_canonical_access_variants() {
864        let plan: AccessPlan<u64> = AccessPlan::Union(vec![
865            AccessPlan::path(AccessPath::ByKey(1)),
866            AccessPlan::path(AccessPath::ByKeys(vec![2, 3])),
867            AccessPlan::path(AccessPath::KeyRange { start: 4, end: 9 }),
868            AccessPlan::path(AccessPath::IndexPrefix {
869                index: TEST_INDEX,
870                values: vec![Value::Uint(7)],
871            }),
872            AccessPlan::path(AccessPath::index_range(
873                TEST_INDEX,
874                vec![Value::Uint(7)],
875                Bound::Included(Value::Uint(8)),
876                Bound::Excluded(Value::Uint(12)),
877            )),
878            AccessPlan::Intersection(vec![
879                AccessPlan::path(AccessPath::FullScan),
880                AccessPlan::path(AccessPath::ByKey(11)),
881            ]),
882        ]);
883
884        let mut projection = AccessPlanEventProjection::default();
885        project_access_plan(&plan, &mut projection);
886
887        assert_eq!(projection.union_child_counts, vec![6]);
888        assert_eq!(projection.intersection_child_counts, vec![2]);
889        assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
890        assert!(
891            projection.events.contains(&"by_key"),
892            "projection must visit by-key variants"
893        );
894        assert!(
895            projection.events.contains(&"by_keys"),
896            "projection must visit by-keys variants"
897        );
898        assert!(
899            projection.events.contains(&"key_range"),
900            "projection must visit key-range variants"
901        );
902        assert!(
903            projection.events.contains(&"index_prefix"),
904            "projection must visit index-prefix variants"
905        );
906        assert!(
907            projection.events.contains(&"index_range"),
908            "projection must visit index-range variants"
909        );
910        assert!(
911            projection.events.contains(&"full_scan"),
912            "projection must visit full-scan variants"
913        );
914    }
915
916    #[derive(Default)]
917    struct ExplainAccessEventProjection {
918        events: Vec<&'static str>,
919        union_child_counts: Vec<usize>,
920        intersection_child_counts: Vec<usize>,
921        seen_index: Option<(&'static str, usize, usize, usize)>,
922    }
923
924    impl AccessPlanProjection<Value> for ExplainAccessEventProjection {
925        type Output = ();
926
927        fn by_key(&mut self, key: &Value) -> Self::Output {
928            self.events.push("by_key");
929            assert_eq!(key, &Value::Uint(10));
930        }
931
932        fn by_keys(&mut self, keys: &[Value]) -> Self::Output {
933            self.events.push("by_keys");
934            assert_eq!(keys, [Value::Uint(20), Value::Uint(30)].as_slice());
935        }
936
937        fn key_range(&mut self, start: &Value, end: &Value) -> Self::Output {
938            self.events.push("key_range");
939            assert_eq!((start, end), (&Value::Uint(40), &Value::Uint(90)));
940        }
941
942        fn index_prefix(
943            &mut self,
944            index_name: &'static str,
945            index_fields: &[&'static str],
946            prefix_len: usize,
947            values: &[Value],
948        ) -> Self::Output {
949            self.events.push("index_prefix");
950            self.seen_index = Some((index_name, index_fields.len(), prefix_len, values.len()));
951        }
952
953        fn index_range(
954            &mut self,
955            index_name: &'static str,
956            index_fields: &[&'static str],
957            prefix_len: usize,
958            prefix: &[Value],
959            lower: &Bound<Value>,
960            upper: &Bound<Value>,
961        ) -> Self::Output {
962            self.events.push("index_range");
963            self.seen_index = Some((index_name, index_fields.len(), prefix_len, prefix.len()));
964            assert_eq!(lower, &Bound::Included(Value::Uint(8)));
965            assert_eq!(upper, &Bound::Excluded(Value::Uint(12)));
966        }
967
968        fn full_scan(&mut self) -> Self::Output {
969            self.events.push("full_scan");
970        }
971
972        fn union(&mut self, children: Vec<Self::Output>) -> Self::Output {
973            self.events.push("union");
974            self.union_child_counts.push(children.len());
975        }
976
977        fn intersection(&mut self, children: Vec<Self::Output>) -> Self::Output {
978            self.events.push("intersection");
979            self.intersection_child_counts.push(children.len());
980        }
981    }
982
983    #[test]
984    fn project_explain_access_path_walks_canonical_access_variants() {
985        let access = ExplainAccessPath::Union(vec![
986            ExplainAccessPath::ByKey {
987                key: Value::Uint(10),
988            },
989            ExplainAccessPath::ByKeys {
990                keys: vec![Value::Uint(20), Value::Uint(30)],
991            },
992            ExplainAccessPath::KeyRange {
993                start: Value::Uint(40),
994                end: Value::Uint(90),
995            },
996            ExplainAccessPath::IndexPrefix {
997                name: TEST_INDEX.name,
998                fields: vec!["group", "rank"],
999                prefix_len: 1,
1000                values: vec![Value::Uint(7)],
1001            },
1002            ExplainAccessPath::IndexRange {
1003                name: TEST_INDEX.name,
1004                fields: vec!["group", "rank"],
1005                prefix_len: 1,
1006                prefix: vec![Value::Uint(7)],
1007                lower: Bound::Included(Value::Uint(8)),
1008                upper: Bound::Excluded(Value::Uint(12)),
1009            },
1010            ExplainAccessPath::Intersection(vec![
1011                ExplainAccessPath::FullScan,
1012                ExplainAccessPath::ByKey {
1013                    key: Value::Uint(10),
1014                },
1015            ]),
1016        ]);
1017
1018        let mut projection = ExplainAccessEventProjection::default();
1019        project_explain_access_path(&access, &mut projection);
1020
1021        assert_eq!(projection.union_child_counts, vec![6]);
1022        assert_eq!(projection.intersection_child_counts, vec![2]);
1023        assert_eq!(projection.seen_index, Some((TEST_INDEX.name, 2, 1, 1)));
1024        assert!(
1025            projection.events.contains(&"by_key"),
1026            "projection must visit by-key variants"
1027        );
1028        assert!(
1029            projection.events.contains(&"by_keys"),
1030            "projection must visit by-keys variants"
1031        );
1032        assert!(
1033            projection.events.contains(&"key_range"),
1034            "projection must visit key-range variants"
1035        );
1036        assert!(
1037            projection.events.contains(&"index_prefix"),
1038            "projection must visit index-prefix variants"
1039        );
1040        assert!(
1041            projection.events.contains(&"index_range"),
1042            "projection must visit index-range variants"
1043        );
1044        assert!(
1045            projection.events.contains(&"full_scan"),
1046            "projection must visit full-scan variants"
1047        );
1048    }
1049}