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    /// Project one planner-owned route profile for executor route planning.
174    #[must_use]
175    pub(in crate::db) fn planner_route_profile(&self, model: &EntityModel) -> PlannerRouteProfile {
176        PlannerRouteProfile::new(
177            derive_continuation_policy_validated(self),
178            derive_logical_pushdown_eligibility(model, self),
179        )
180    }
181
182    /// Build one immutable execution-shape signature contract for runtime layers.
183    #[must_use]
184    pub(in crate::db) fn execution_shape_signature(
185        &self,
186        entity_path: &'static str,
187    ) -> ExecutionShapeSignature {
188        ExecutionShapeSignature::new(self.continuation_signature(entity_path))
189    }
190
191    /// Return whether the chosen access contract fully satisfies the current
192    /// scalar query predicate without any additional post-access filtering.
193    #[must_use]
194    pub(in crate::db) fn predicate_fully_satisfied_by_access_contract(&self) -> bool {
195        self.scalar_plan().predicate.is_some() && self.effective_execution_predicate().is_none()
196    }
197
198    /// Return whether the scalar logical predicate still requires post-access
199    /// filtering after accounting for filtered-index guard predicates and
200    /// access-path equality bounds.
201    #[must_use]
202    pub(in crate::db) fn has_residual_predicate(&self) -> bool {
203        self.scalar_plan().predicate.is_some()
204            && !self.predicate_fully_satisfied_by_access_contract()
205    }
206}
207
208fn distinct_runtime_dedup_strategy<K>(access: &AccessPlan<K>) -> Option<DistinctExecutionStrategy> {
209    match access {
210        AccessPlan::Union(_) | AccessPlan::Intersection(_) => {
211            Some(DistinctExecutionStrategy::PreOrdered)
212        }
213        AccessPlan::Path(path) if path.as_ref().is_index_multi_lookup() => {
214            Some(DistinctExecutionStrategy::HashMaterialize)
215        }
216        AccessPlan::Path(_) => None,
217    }
218}
219
220fn derive_continuation_policy_validated(plan: &AccessPlannedQuery) -> ContinuationPolicy {
221    let is_grouped_safe = plan
222        .grouped_plan()
223        .is_none_or(|grouped| grouped_cursor_policy_violation(grouped, true).is_none());
224
225    ContinuationPolicy::new(
226        true, // Continuation resume windows require anchor semantics for pushdown-safe replay.
227        true, // Continuation resumes must advance strictly to prevent replay/regression loops.
228        is_grouped_safe,
229    )
230}