Skip to main content

icydb_core/db/query/plan/
mod.rs

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