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