Skip to main content

icydb_core/db/query/plan/semantics/
logical.rs

1//! Module: query::plan::semantics::logical
2//! Responsibility: logical-plan semantic lowering from planner contracts to access-planned queries.
3//! Does not own: access-path index selection internals or runtime execution behavior.
4//! Boundary: derives planner-owned execution semantics, shape signatures, and continuation policy.
5
6use crate::{
7    db::{
8        access::{AccessPlan, ExecutableAccessPlan},
9        predicate::IndexCompileTarget,
10        predicate::{PredicateExecutionModel, PredicateProgram},
11        query::plan::{
12            AccessPlannedQuery, ContinuationPolicy, DistinctExecutionStrategy,
13            ExecutionShapeSignature, ExpressionOrderTerm, GroupPlan, GroupedAggregateExecutionSpec,
14            GroupedDistinctExecutionStrategy, LogicalPlan, PlannerRouteProfile, QueryMode,
15            ResolvedOrder, ResolvedOrderField, ResolvedOrderValueSource, ScalarPlan,
16            StaticPlanningShape, derive_logical_pushdown_eligibility,
17            expr::{
18                Expr, ProjectionField, ProjectionSpec, ScalarProjectionExpr,
19                compile_scalar_projection_plan,
20            },
21            grouped_aggregate_execution_specs_with_model,
22            grouped_aggregate_projection_specs_from_projection_spec,
23            grouped_cursor_policy_violation, lower_direct_projection_slots,
24            lower_projection_identity, lower_projection_intent,
25            residual_query_predicate_after_access_path_bounds,
26            residual_query_predicate_after_filtered_access,
27            resolved_grouped_distinct_execution_strategy_for_model,
28        },
29    },
30    error::InternalError,
31    model::{
32        entity::{EntityModel, resolve_field_slot},
33        index::IndexKeyItemsRef,
34    },
35};
36
37impl QueryMode {
38    /// True if this mode represents a load intent.
39    #[must_use]
40    pub const fn is_load(&self) -> bool {
41        match self {
42            Self::Load(_) => true,
43            Self::Delete(_) => false,
44        }
45    }
46
47    /// True if this mode represents a delete intent.
48    #[must_use]
49    pub const fn is_delete(&self) -> bool {
50        match self {
51            Self::Delete(_) => true,
52            Self::Load(_) => false,
53        }
54    }
55}
56
57impl LogicalPlan {
58    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
59    #[must_use]
60    pub(in crate::db) const fn scalar_semantics(&self) -> &ScalarPlan {
61        match self {
62            Self::Scalar(plan) => plan,
63            Self::Grouped(plan) => &plan.scalar,
64        }
65    }
66
67    /// Borrow scalar semantic fields mutably across logical variants for tests.
68    #[must_use]
69    #[cfg(test)]
70    pub(in crate::db) const fn scalar_semantics_mut(&mut self) -> &mut ScalarPlan {
71        match self {
72            Self::Scalar(plan) => plan,
73            Self::Grouped(plan) => &mut plan.scalar,
74        }
75    }
76
77    /// Test-only shorthand for explicit scalar semantic borrowing.
78    #[must_use]
79    #[cfg(test)]
80    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
81        self.scalar_semantics()
82    }
83
84    /// Test-only shorthand for explicit mutable scalar semantic borrowing.
85    #[must_use]
86    #[cfg(test)]
87    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
88        self.scalar_semantics_mut()
89    }
90}
91
92impl AccessPlannedQuery {
93    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
94    #[must_use]
95    pub(in crate::db) const fn scalar_plan(&self) -> &ScalarPlan {
96        self.logical.scalar_semantics()
97    }
98
99    /// Borrow scalar semantic fields mutably across logical variants for tests.
100    #[must_use]
101    #[cfg(test)]
102    pub(in crate::db) const fn scalar_plan_mut(&mut self) -> &mut ScalarPlan {
103        self.logical.scalar_semantics_mut()
104    }
105
106    /// Test-only shorthand for explicit scalar plan borrowing.
107    #[must_use]
108    #[cfg(test)]
109    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
110        self.scalar_plan()
111    }
112
113    /// Test-only shorthand for explicit mutable scalar plan borrowing.
114    #[must_use]
115    #[cfg(test)]
116    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
117        self.scalar_plan_mut()
118    }
119
120    /// Borrow grouped semantic fields when this plan is grouped.
121    #[must_use]
122    pub(in crate::db) const fn grouped_plan(&self) -> Option<&GroupPlan> {
123        match &self.logical {
124            LogicalPlan::Scalar(_) => None,
125            LogicalPlan::Grouped(plan) => Some(plan),
126        }
127    }
128
129    /// Lower this plan into one canonical planner-owned projection semantic spec.
130    #[must_use]
131    pub(in crate::db) fn projection_spec(&self, model: &EntityModel) -> ProjectionSpec {
132        if let Some(static_shape) = &self.static_planning_shape {
133            return static_shape.projection_spec.clone();
134        }
135
136        lower_projection_intent(model, &self.logical, &self.projection_selection)
137    }
138
139    /// Lower this plan into one projection semantic shape for identity hashing.
140    #[must_use]
141    pub(in crate::db::query) fn projection_spec_for_identity(&self) -> ProjectionSpec {
142        lower_projection_identity(&self.logical)
143    }
144
145    /// Return the executor-facing predicate after removing only filtered-index
146    /// guard clauses the chosen access path already proves.
147    ///
148    /// This conservative form is used by preparation/explain surfaces that
149    /// still need to see access-bound equalities as index-predicate input.
150    #[must_use]
151    pub(in crate::db) fn execution_preparation_predicate(&self) -> Option<PredicateExecutionModel> {
152        let query_predicate = self.scalar_plan().predicate.as_ref()?;
153
154        match self.access.selected_index_model() {
155            Some(index) => residual_query_predicate_after_filtered_access(index, query_predicate),
156            None => Some(query_predicate.clone()),
157        }
158    }
159
160    /// Return the executor-facing residual predicate after removing any
161    /// filtered-index guard clauses and fixed access-bound equalities already
162    /// guaranteed by the chosen path.
163    #[must_use]
164    pub(in crate::db) fn effective_execution_predicate(&self) -> Option<PredicateExecutionModel> {
165        // Phase 1: strip only filtered-index guard clauses the chosen access
166        // path already proves.
167        let filtered_residual = self.execution_preparation_predicate();
168        let filtered_residual = filtered_residual.as_ref()?;
169
170        // Phase 2: strip any additional equality clauses already guaranteed by
171        // the concrete access-path bounds, such as `tier = 'gold'` on one
172        // selected `IndexPrefix(tier='gold', ...)` route.
173        residual_query_predicate_after_access_path_bounds(self.access.as_path(), filtered_residual)
174    }
175
176    /// Borrow the planner-compiled execution-preparation predicate program.
177    #[must_use]
178    pub(in crate::db) const fn execution_preparation_compiled_predicate(
179        &self,
180    ) -> Option<&PredicateProgram> {
181        self.static_planning_shape()
182            .execution_preparation_compiled_predicate
183            .as_ref()
184    }
185
186    /// Borrow the planner-compiled effective runtime predicate program.
187    #[must_use]
188    pub(in crate::db) const fn effective_runtime_compiled_predicate(
189        &self,
190    ) -> Option<&PredicateProgram> {
191        self.static_planning_shape()
192            .effective_runtime_compiled_predicate
193            .as_ref()
194    }
195
196    /// Lower scalar DISTINCT semantics into one executor-facing execution strategy.
197    #[must_use]
198    pub(in crate::db) fn distinct_execution_strategy(&self) -> DistinctExecutionStrategy {
199        if !self.scalar_plan().distinct {
200            return DistinctExecutionStrategy::None;
201        }
202
203        // DISTINCT on duplicate-safe single-path access shapes is a planner
204        // no-op for runtime dedup mechanics. Composite shapes can surface
205        // duplicate keys and therefore retain explicit dedup execution.
206        match distinct_runtime_dedup_strategy(&self.access) {
207            Some(strategy) => strategy,
208            None => DistinctExecutionStrategy::None,
209        }
210    }
211
212    /// Freeze one planner-owned route profile after model validation completes.
213    pub(in crate::db) fn finalize_planner_route_profile_for_model(&mut self, model: &EntityModel) {
214        self.set_planner_route_profile(project_planner_route_profile_for_model(model, self));
215    }
216
217    /// Freeze planner-owned executor metadata after logical/access planning completes.
218    pub(in crate::db) fn finalize_static_planning_shape_for_model(
219        &mut self,
220        model: &EntityModel,
221    ) -> Result<(), InternalError> {
222        self.static_planning_shape = Some(project_static_planning_shape_for_model(model, self)?);
223
224        Ok(())
225    }
226
227    /// Build one immutable execution-shape signature contract for runtime layers.
228    #[must_use]
229    pub(in crate::db) fn execution_shape_signature(
230        &self,
231        entity_path: &'static str,
232    ) -> ExecutionShapeSignature {
233        ExecutionShapeSignature::new(self.continuation_signature(entity_path))
234    }
235
236    /// Return whether the chosen access contract fully satisfies the current
237    /// scalar query predicate without any additional post-access filtering.
238    #[must_use]
239    pub(in crate::db) fn predicate_fully_satisfied_by_access_contract(&self) -> bool {
240        self.scalar_plan().predicate.is_some() && self.effective_execution_predicate().is_none()
241    }
242
243    /// Return whether the scalar logical predicate still requires post-access
244    /// filtering after accounting for filtered-index guard predicates and
245    /// access-path equality bounds.
246    #[must_use]
247    pub(in crate::db) fn has_residual_predicate(&self) -> bool {
248        self.scalar_plan().predicate.is_some()
249            && !self.predicate_fully_satisfied_by_access_contract()
250    }
251
252    /// Borrow the planner-frozen compiled scalar projection program.
253    #[must_use]
254    pub(in crate::db) fn scalar_projection_plan(&self) -> Option<&[ScalarProjectionExpr]> {
255        self.static_planning_shape()
256            .scalar_projection_plan
257            .as_deref()
258    }
259
260    /// Borrow the planner-frozen primary-key field name.
261    #[must_use]
262    pub(in crate::db) const fn primary_key_name(&self) -> &'static str {
263        self.static_planning_shape().primary_key_name
264    }
265
266    /// Borrow the planner-frozen projection slot reachability set.
267    #[must_use]
268    pub(in crate::db) const fn projection_referenced_slots(&self) -> &[usize] {
269        self.static_planning_shape()
270            .projection_referenced_slots
271            .as_slice()
272    }
273
274    /// Borrow the planner-frozen mask for direct projected output slots.
275    #[must_use]
276    pub(in crate::db) const fn projected_slot_mask(&self) -> &[bool] {
277        self.static_planning_shape().projected_slot_mask.as_slice()
278    }
279
280    /// Return whether projection remains the full model-identity field list.
281    #[must_use]
282    pub(in crate::db) const fn projection_is_model_identity(&self) -> bool {
283        self.static_planning_shape().projection_is_model_identity
284    }
285
286    /// Borrow the planner-frozen ORDER BY slot reachability set, if any.
287    #[must_use]
288    pub(in crate::db) fn order_referenced_slots(&self) -> Option<&[usize]> {
289        self.static_planning_shape()
290            .order_referenced_slots
291            .as_deref()
292    }
293
294    /// Borrow the planner-frozen resolved ORDER BY program, if one exists.
295    #[must_use]
296    pub(in crate::db) const fn resolved_order(&self) -> Option<&ResolvedOrder> {
297        self.static_planning_shape().resolved_order.as_ref()
298    }
299
300    /// Borrow the planner-frozen access slot map used by index predicate compilation.
301    #[must_use]
302    pub(in crate::db) fn slot_map(&self) -> Option<&[usize]> {
303        self.static_planning_shape().slot_map.as_deref()
304    }
305
306    /// Borrow grouped aggregate execution specs already resolved during static planning.
307    #[must_use]
308    pub(in crate::db) fn grouped_aggregate_execution_specs(
309        &self,
310    ) -> Option<&[GroupedAggregateExecutionSpec]> {
311        self.static_planning_shape()
312            .grouped_aggregate_execution_specs
313            .as_deref()
314    }
315
316    /// Borrow the planner-resolved grouped DISTINCT execution strategy when present.
317    #[must_use]
318    pub(in crate::db) const fn grouped_distinct_execution_strategy(
319        &self,
320    ) -> Option<&GroupedDistinctExecutionStrategy> {
321        self.static_planning_shape()
322            .grouped_distinct_execution_strategy
323            .as_ref()
324    }
325
326    /// Borrow the frozen projection semantic shape without reopening model ownership.
327    #[must_use]
328    pub(in crate::db) const fn frozen_projection_spec(&self) -> &ProjectionSpec {
329        &self.static_planning_shape().projection_spec
330    }
331
332    /// Borrow the frozen direct projection slots without reopening model ownership.
333    #[must_use]
334    pub(in crate::db) fn frozen_direct_projection_slots(&self) -> Option<&[usize]> {
335        self.static_planning_shape()
336            .projection_direct_slots
337            .as_deref()
338    }
339
340    /// Borrow the planner-frozen key-item-aware compile targets for the chosen access path.
341    #[must_use]
342    pub(in crate::db) fn index_compile_targets(&self) -> Option<&[IndexCompileTarget]> {
343        self.static_planning_shape()
344            .index_compile_targets
345            .as_deref()
346    }
347
348    const fn static_planning_shape(&self) -> &StaticPlanningShape {
349        self.static_planning_shape
350            .as_ref()
351            .expect("access-planned queries must freeze static planning shape before execution")
352    }
353}
354
355fn distinct_runtime_dedup_strategy<K>(access: &AccessPlan<K>) -> Option<DistinctExecutionStrategy> {
356    match access {
357        AccessPlan::Union(_) | AccessPlan::Intersection(_) => {
358            Some(DistinctExecutionStrategy::PreOrdered)
359        }
360        AccessPlan::Path(path) if path.as_ref().is_index_multi_lookup() => {
361            Some(DistinctExecutionStrategy::HashMaterialize)
362        }
363        AccessPlan::Path(_) => None,
364    }
365}
366
367fn derive_continuation_policy_validated(plan: &AccessPlannedQuery) -> ContinuationPolicy {
368    let is_grouped_safe = plan
369        .grouped_plan()
370        .is_none_or(|grouped| grouped_cursor_policy_violation(grouped, true).is_none());
371
372    ContinuationPolicy::new(
373        true, // Continuation resume windows require anchor semantics for pushdown-safe replay.
374        true, // Continuation resumes must advance strictly to prevent replay/regression loops.
375        is_grouped_safe,
376    )
377}
378
379/// Project one planner-owned route profile from the finalized logical+access plan.
380#[must_use]
381pub(in crate::db) fn project_planner_route_profile_for_model(
382    model: &EntityModel,
383    plan: &AccessPlannedQuery,
384) -> PlannerRouteProfile {
385    let secondary_order_contract = plan
386        .scalar_plan()
387        .order
388        .as_ref()
389        .and_then(|order| order.deterministic_secondary_order_contract(model.primary_key.name));
390
391    PlannerRouteProfile::new(
392        derive_continuation_policy_validated(plan),
393        derive_logical_pushdown_eligibility(plan, secondary_order_contract.as_ref()),
394        secondary_order_contract,
395    )
396}
397
398fn project_static_planning_shape_for_model(
399    model: &EntityModel,
400    plan: &AccessPlannedQuery,
401) -> Result<StaticPlanningShape, InternalError> {
402    let projection_spec = lower_projection_intent(model, &plan.logical, &plan.projection_selection);
403    let execution_preparation_compiled_predicate = plan
404        .execution_preparation_predicate()
405        .as_ref()
406        .map(|predicate| PredicateProgram::compile_with_model(model, predicate));
407    let effective_runtime_compiled_predicate = plan
408        .effective_execution_predicate()
409        .as_ref()
410        .map(|predicate| PredicateProgram::compile_with_model(model, predicate));
411    let scalar_projection_plan =
412        if plan.grouped_plan().is_none() {
413            Some(compile_scalar_projection_plan(model, &projection_spec).ok_or_else(|| {
414            InternalError::query_executor_invariant(
415                "scalar projection program must compile during static planning finalization",
416            )
417        })?)
418        } else {
419            None
420        };
421    let grouped_aggregate_execution_specs = if let Some(grouped) = plan.grouped_plan() {
422        #[cfg(not(test))]
423        let aggregate_projection_specs = grouped_aggregate_projection_specs_from_projection_spec(
424            &projection_spec,
425            grouped.group.group_fields.as_slice(),
426            grouped.group.aggregates.as_slice(),
427        );
428        #[cfg(test)]
429        let aggregate_projection_specs = grouped_aggregate_projection_specs_from_projection_spec(
430            &projection_spec,
431            grouped.group.group_fields.as_slice(),
432            grouped.group.aggregates.as_slice(),
433        )?;
434
435        Some(grouped_aggregate_execution_specs_with_model(
436            model,
437            aggregate_projection_specs.as_slice(),
438        )?)
439    } else {
440        None
441    };
442    let grouped_distinct_execution_strategy = if let Some(grouped) = plan.grouped_plan() {
443        Some(resolved_grouped_distinct_execution_strategy_for_model(
444            model,
445            grouped.group.group_fields.as_slice(),
446            grouped.group.aggregates.as_slice(),
447            grouped.having.as_ref(),
448        )?)
449    } else {
450        None
451    };
452    let projection_direct_slots =
453        lower_direct_projection_slots(model, &plan.logical, &plan.projection_selection);
454    let projection_referenced_slots =
455        projection_referenced_slots_for_spec(model, &projection_spec)?;
456    let projected_slot_mask =
457        projected_slot_mask_for_spec(model, &projection_spec, projection_direct_slots.as_deref());
458    let projection_is_model_identity =
459        projection_is_model_identity_for_spec(model, &projection_spec);
460    let resolved_order = resolved_order_for_plan(model, plan)?;
461    let order_referenced_slots = order_referenced_slots_for_resolved_order(resolved_order.as_ref());
462    let slot_map = slot_map_for_model_plan(model, plan);
463    let index_compile_targets = index_compile_targets_for_model_plan(model, plan);
464
465    Ok(StaticPlanningShape {
466        primary_key_name: model.primary_key.name,
467        projection_spec,
468        execution_preparation_compiled_predicate,
469        effective_runtime_compiled_predicate,
470        scalar_projection_plan,
471        grouped_aggregate_execution_specs,
472        grouped_distinct_execution_strategy,
473        projection_direct_slots,
474        projection_referenced_slots,
475        projected_slot_mask,
476        projection_is_model_identity,
477        resolved_order,
478        order_referenced_slots,
479        slot_map,
480        index_compile_targets,
481    })
482}
483
484fn projection_referenced_slots_for_spec(
485    model: &EntityModel,
486    projection: &ProjectionSpec,
487) -> Result<Vec<usize>, InternalError> {
488    let mut referenced = vec![false; model.fields().len()];
489
490    for field in projection.fields() {
491        match field {
492            ProjectionField::Scalar { expr, .. } => {
493                mark_projection_expr_slots(model, expr, referenced.as_mut_slice())?;
494            }
495        }
496    }
497
498    Ok(referenced
499        .into_iter()
500        .enumerate()
501        .filter_map(|(slot, required)| required.then_some(slot))
502        .collect())
503}
504
505fn mark_projection_expr_slots(
506    model: &EntityModel,
507    expr: &Expr,
508    referenced: &mut [bool],
509) -> Result<(), InternalError> {
510    match expr {
511        Expr::Field(field_id) => {
512            let field_name = field_id.as_str();
513            let slot = resolve_field_slot(model, field_name).ok_or_else(|| {
514                InternalError::query_invalid_logical_plan(format!(
515                    "projection expression references unknown field '{field_name}'",
516                ))
517            })?;
518            referenced[slot] = true;
519        }
520        Expr::Aggregate(_) => {}
521        #[cfg(test)]
522        Expr::Literal(_) => {}
523        #[cfg(test)]
524        Expr::Alias { expr, .. } => {
525            mark_projection_expr_slots(model, expr.as_ref(), referenced)?;
526        }
527        #[cfg(test)]
528        Expr::Unary { expr, .. } => {
529            mark_projection_expr_slots(model, expr.as_ref(), referenced)?;
530        }
531        #[cfg(test)]
532        Expr::Binary { left, right, .. } => {
533            mark_projection_expr_slots(model, left.as_ref(), referenced)?;
534            mark_projection_expr_slots(model, right.as_ref(), referenced)?;
535        }
536    }
537
538    Ok(())
539}
540
541fn projected_slot_mask_for_spec(
542    model: &EntityModel,
543    projection: &ProjectionSpec,
544    direct_projection_slots: Option<&[usize]>,
545) -> Vec<bool> {
546    let mut projected_slots = vec![false; model.fields().len()];
547
548    let Some(direct_projection_slots) = direct_projection_slots else {
549        return projected_slots;
550    };
551
552    for (field, slot) in projection
553        .fields()
554        .zip(direct_projection_slots.iter().copied())
555    {
556        if matches!(field, ProjectionField::Scalar { .. })
557            && let Some(projected) = projected_slots.get_mut(slot)
558        {
559            *projected = true;
560        }
561    }
562
563    projected_slots
564}
565
566fn projection_is_model_identity_for_spec(model: &EntityModel, projection: &ProjectionSpec) -> bool {
567    if projection.len() != model.fields().len() {
568        return false;
569    }
570
571    for (field_model, projected_field) in model.fields().iter().zip(projection.fields()) {
572        match projected_field {
573            ProjectionField::Scalar {
574                expr: Expr::Field(field_id),
575                alias: None,
576            } if field_id.as_str() == field_model.name() => {}
577            ProjectionField::Scalar { .. } => return false,
578        }
579    }
580
581    true
582}
583
584fn resolved_order_for_plan(
585    model: &EntityModel,
586    plan: &AccessPlannedQuery,
587) -> Result<Option<ResolvedOrder>, InternalError> {
588    let Some(order) = plan.scalar_plan().order.as_ref() else {
589        return Ok(None);
590    };
591
592    let mut fields = Vec::with_capacity(order.fields.len());
593    for (field, direction) in &order.fields {
594        fields.push(ResolvedOrderField::new(
595            resolved_order_value_source_for_field(model, field)?,
596            *direction,
597        ));
598    }
599
600    Ok(Some(ResolvedOrder::new(fields)))
601}
602
603fn resolved_order_value_source_for_field(
604    model: &EntityModel,
605    field: &str,
606) -> Result<ResolvedOrderValueSource, InternalError> {
607    if let Some(expression) = ExpressionOrderTerm::parse(field) {
608        let slot = resolve_field_slot(model, expression.field()).ok_or_else(|| {
609            InternalError::query_invalid_logical_plan(format!(
610                "order expression references unknown field '{field}'",
611            ))
612        })?;
613
614        return Ok(match expression {
615            ExpressionOrderTerm::Lower(_) => ResolvedOrderValueSource::expression_lower(slot),
616            ExpressionOrderTerm::Upper(_) => ResolvedOrderValueSource::expression_upper(slot),
617        });
618    }
619
620    let slot = resolve_field_slot(model, field).ok_or_else(|| {
621        InternalError::query_invalid_logical_plan(format!(
622            "order expression references unknown field '{field}'",
623        ))
624    })?;
625
626    Ok(ResolvedOrderValueSource::direct_field(slot))
627}
628
629fn order_referenced_slots_for_resolved_order(
630    resolved_order: Option<&ResolvedOrder>,
631) -> Option<Vec<usize>> {
632    let resolved_order = resolved_order?;
633    let mut referenced = Vec::new();
634
635    // Keep one stable slot list without re-parsing order expressions after the
636    // planner has already frozen structural ORDER BY sources.
637    for field in resolved_order.fields() {
638        let slot = field.source().slot();
639        if !referenced.contains(&slot) {
640            referenced.push(slot);
641        }
642    }
643
644    Some(referenced)
645}
646
647fn slot_map_for_model_plan(model: &EntityModel, plan: &AccessPlannedQuery) -> Option<Vec<usize>> {
648    let access_strategy = plan.access.resolve_strategy();
649    let executable = access_strategy.executable();
650
651    resolved_index_slots_for_access_path(model, executable)
652}
653
654fn resolved_index_slots_for_access_path(
655    model: &EntityModel,
656    access: &ExecutableAccessPlan<'_, crate::value::Value>,
657) -> Option<Vec<usize>> {
658    let path = access.as_path()?;
659    let path_capabilities = path.capabilities();
660    let index_fields = path_capabilities.index_fields_for_slot_map()?;
661    let mut slots = Vec::with_capacity(index_fields.len());
662
663    for field_name in index_fields {
664        let slot = resolve_field_slot(model, field_name)?;
665        slots.push(slot);
666    }
667
668    Some(slots)
669}
670
671fn index_compile_targets_for_model_plan(
672    model: &EntityModel,
673    plan: &AccessPlannedQuery,
674) -> Option<Vec<IndexCompileTarget>> {
675    let index = plan.access.as_path()?.selected_index_model()?;
676    let mut targets = Vec::new();
677
678    match index.key_items() {
679        IndexKeyItemsRef::Fields(fields) => {
680            for (component_index, &field_name) in fields.iter().enumerate() {
681                let field_slot = resolve_field_slot(model, field_name)?;
682                targets.push(IndexCompileTarget {
683                    component_index,
684                    field_slot,
685                    key_item: crate::model::index::IndexKeyItem::Field(field_name),
686                });
687            }
688        }
689        IndexKeyItemsRef::Items(items) => {
690            for (component_index, &key_item) in items.iter().enumerate() {
691                let field_slot = resolve_field_slot(model, key_item.field())?;
692                targets.push(IndexCompileTarget {
693                    component_index,
694                    field_slot,
695                    key_item,
696                });
697            }
698        }
699    }
700
701    Some(targets)
702}