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,
9        predicate::PredicateExecutionModel,
10        query::plan::{
11            AccessPlannedQuery, ContinuationPolicy, DistinctExecutionStrategy,
12            ExecutionShapeSignature, GroupPlan, LogicalPlan, PlannerRouteProfile, QueryMode,
13            ScalarPlan, derive_logical_pushdown_eligibility, expr::ProjectionSpec,
14            grouped_cursor_policy_violation, lower_direct_projection_slots,
15            lower_projection_identity, lower_projection_intent,
16            residual_query_predicate_after_access_path_bounds,
17            residual_query_predicate_after_filtered_access,
18        },
19    },
20    model::entity::EntityModel,
21};
22
23impl QueryMode {
24    /// True if this mode represents a load intent.
25    #[must_use]
26    pub const fn is_load(&self) -> bool {
27        match self {
28            Self::Load(_) => true,
29            Self::Delete(_) => false,
30        }
31    }
32
33    /// True if this mode represents a delete intent.
34    #[must_use]
35    pub const fn is_delete(&self) -> bool {
36        match self {
37            Self::Delete(_) => true,
38            Self::Load(_) => false,
39        }
40    }
41}
42
43impl LogicalPlan {
44    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
45    #[must_use]
46    pub(in crate::db) const fn scalar_semantics(&self) -> &ScalarPlan {
47        match self {
48            Self::Scalar(plan) => plan,
49            Self::Grouped(plan) => &plan.scalar,
50        }
51    }
52
53    /// Borrow scalar semantic fields mutably across logical variants for tests.
54    #[must_use]
55    #[cfg(test)]
56    pub(in crate::db) const fn scalar_semantics_mut(&mut self) -> &mut ScalarPlan {
57        match self {
58            Self::Scalar(plan) => plan,
59            Self::Grouped(plan) => &mut plan.scalar,
60        }
61    }
62
63    /// Test-only shorthand for explicit scalar semantic borrowing.
64    #[must_use]
65    #[cfg(test)]
66    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
67        self.scalar_semantics()
68    }
69
70    /// Test-only shorthand for explicit mutable scalar semantic borrowing.
71    #[must_use]
72    #[cfg(test)]
73    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
74        self.scalar_semantics_mut()
75    }
76}
77
78impl AccessPlannedQuery {
79    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
80    #[must_use]
81    pub(in crate::db) const fn scalar_plan(&self) -> &ScalarPlan {
82        self.logical.scalar_semantics()
83    }
84
85    /// Borrow scalar semantic fields mutably across logical variants for tests.
86    #[must_use]
87    #[cfg(test)]
88    pub(in crate::db) const fn scalar_plan_mut(&mut self) -> &mut ScalarPlan {
89        self.logical.scalar_semantics_mut()
90    }
91
92    /// Test-only shorthand for explicit scalar plan borrowing.
93    #[must_use]
94    #[cfg(test)]
95    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
96        self.scalar_plan()
97    }
98
99    /// Test-only shorthand for explicit mutable scalar plan borrowing.
100    #[must_use]
101    #[cfg(test)]
102    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
103        self.scalar_plan_mut()
104    }
105
106    /// Borrow grouped semantic fields when this plan is grouped.
107    #[must_use]
108    pub(in crate::db) const fn grouped_plan(&self) -> Option<&GroupPlan> {
109        match &self.logical {
110            LogicalPlan::Scalar(_) => None,
111            LogicalPlan::Grouped(plan) => Some(plan),
112        }
113    }
114
115    /// Lower this plan into one canonical planner-owned projection semantic spec.
116    #[must_use]
117    pub(in crate::db) fn projection_spec(&self, model: &EntityModel) -> ProjectionSpec {
118        lower_projection_intent(model, &self.logical, &self.projection_selection)
119    }
120
121    /// Lower this plan into one direct slot projection layout when every
122    /// output remains a unique canonical field reference.
123    #[must_use]
124    pub(in crate::db) fn direct_projection_slots(&self, model: &EntityModel) -> Option<Vec<usize>> {
125        lower_direct_projection_slots(model, &self.logical, &self.projection_selection)
126    }
127
128    /// Lower this plan into one projection semantic shape for identity hashing.
129    #[must_use]
130    pub(in crate::db::query) fn projection_spec_for_identity(&self) -> ProjectionSpec {
131        lower_projection_identity(&self.logical)
132    }
133
134    /// Return the executor-facing predicate after removing only filtered-index
135    /// guard clauses the chosen access path already proves.
136    ///
137    /// This conservative form is used by preparation/explain surfaces that
138    /// still need to see access-bound equalities as index-predicate input.
139    #[must_use]
140    pub(in crate::db) fn execution_preparation_predicate(&self) -> Option<PredicateExecutionModel> {
141        let query_predicate = self.scalar_plan().predicate.as_ref()?;
142
143        match self.access.selected_index_model() {
144            Some(index) => residual_query_predicate_after_filtered_access(index, query_predicate),
145            None => Some(query_predicate.clone()),
146        }
147    }
148
149    /// Return the executor-facing residual predicate after removing any
150    /// filtered-index guard clauses and fixed access-bound equalities already
151    /// guaranteed by the chosen path.
152    #[must_use]
153    pub(in crate::db) fn effective_execution_predicate(&self) -> Option<PredicateExecutionModel> {
154        // Phase 1: strip only filtered-index guard clauses the chosen access
155        // path already proves.
156        let filtered_residual = self.execution_preparation_predicate();
157        let filtered_residual = filtered_residual.as_ref()?;
158
159        // Phase 2: strip any additional equality clauses already guaranteed by
160        // the concrete access-path bounds, such as `tier = 'gold'` on one
161        // selected `IndexPrefix(tier='gold', ...)` route.
162        residual_query_predicate_after_access_path_bounds(self.access.as_path(), filtered_residual)
163    }
164
165    /// Lower scalar DISTINCT semantics into one executor-facing execution strategy.
166    #[must_use]
167    pub(in crate::db) fn distinct_execution_strategy(&self) -> DistinctExecutionStrategy {
168        if !self.scalar_plan().distinct {
169            return DistinctExecutionStrategy::None;
170        }
171
172        // DISTINCT on duplicate-safe single-path access shapes is a planner
173        // no-op for runtime dedup mechanics. Composite shapes can surface
174        // duplicate keys and therefore retain explicit dedup execution.
175        match distinct_runtime_dedup_strategy(&self.access) {
176            Some(strategy) => strategy,
177            None => DistinctExecutionStrategy::None,
178        }
179    }
180
181    /// Freeze one planner-owned route profile after model validation completes.
182    pub(in crate::db) fn finalize_planner_route_profile_for_model(&mut self, model: &EntityModel) {
183        self.set_planner_route_profile(project_planner_route_profile_for_model(model, self));
184    }
185
186    /// Build one immutable execution-shape signature contract for runtime layers.
187    #[must_use]
188    pub(in crate::db) fn execution_shape_signature(
189        &self,
190        entity_path: &'static str,
191    ) -> ExecutionShapeSignature {
192        ExecutionShapeSignature::new(self.continuation_signature(entity_path))
193    }
194
195    /// Return whether the chosen access contract fully satisfies the current
196    /// scalar query predicate without any additional post-access filtering.
197    #[must_use]
198    pub(in crate::db) fn predicate_fully_satisfied_by_access_contract(&self) -> bool {
199        self.scalar_plan().predicate.is_some() && self.effective_execution_predicate().is_none()
200    }
201
202    /// Return whether the scalar logical predicate still requires post-access
203    /// filtering after accounting for filtered-index guard predicates and
204    /// access-path equality bounds.
205    #[must_use]
206    pub(in crate::db) fn has_residual_predicate(&self) -> bool {
207        self.scalar_plan().predicate.is_some()
208            && !self.predicate_fully_satisfied_by_access_contract()
209    }
210}
211
212fn distinct_runtime_dedup_strategy<K>(access: &AccessPlan<K>) -> Option<DistinctExecutionStrategy> {
213    match access {
214        AccessPlan::Union(_) | AccessPlan::Intersection(_) => {
215            Some(DistinctExecutionStrategy::PreOrdered)
216        }
217        AccessPlan::Path(path) if path.as_ref().is_index_multi_lookup() => {
218            Some(DistinctExecutionStrategy::HashMaterialize)
219        }
220        AccessPlan::Path(_) => None,
221    }
222}
223
224fn derive_continuation_policy_validated(plan: &AccessPlannedQuery) -> ContinuationPolicy {
225    let is_grouped_safe = plan
226        .grouped_plan()
227        .is_none_or(|grouped| grouped_cursor_policy_violation(grouped, true).is_none());
228
229    ContinuationPolicy::new(
230        true, // Continuation resume windows require anchor semantics for pushdown-safe replay.
231        true, // Continuation resumes must advance strictly to prevent replay/regression loops.
232        is_grouped_safe,
233    )
234}
235
236/// Project one planner-owned route profile from the finalized logical+access plan.
237#[must_use]
238pub(in crate::db) fn project_planner_route_profile_for_model(
239    model: &EntityModel,
240    plan: &AccessPlannedQuery,
241) -> PlannerRouteProfile {
242    let secondary_order_contract = plan
243        .scalar_plan()
244        .order
245        .as_ref()
246        .and_then(|order| order.deterministic_secondary_order_contract(model.primary_key.name));
247
248    PlannerRouteProfile::new(
249        derive_continuation_policy_validated(plan),
250        derive_logical_pushdown_eligibility(plan, secondary_order_contract.as_ref()),
251        secondary_order_contract,
252    )
253}