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        query::plan::{
10            AccessPlannedQuery, ContinuationPolicy, DistinctExecutionStrategy,
11            ExecutionShapeSignature, GroupPlan, LogicalPlan, PlannerRouteProfile, QueryMode,
12            ScalarPlan, derive_logical_pushdown_eligibility, expr::ProjectionSpec,
13            filtered_index_predicate_satisfies_query, grouped_cursor_policy_violation,
14            lower_projection_identity, lower_projection_intent,
15        },
16    },
17    model::entity::EntityModel,
18};
19
20impl QueryMode {
21    /// True if this mode represents a load intent.
22    #[must_use]
23    pub const fn is_load(&self) -> bool {
24        match self {
25            Self::Load(_) => true,
26            Self::Delete(_) => false,
27        }
28    }
29
30    /// True if this mode represents a delete intent.
31    #[must_use]
32    pub const fn is_delete(&self) -> bool {
33        match self {
34            Self::Delete(_) => true,
35            Self::Load(_) => false,
36        }
37    }
38}
39
40impl LogicalPlan {
41    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
42    #[must_use]
43    pub(in crate::db) const fn scalar_semantics(&self) -> &ScalarPlan {
44        match self {
45            Self::Scalar(plan) => plan,
46            Self::Grouped(plan) => &plan.scalar,
47        }
48    }
49
50    /// Borrow scalar semantic fields mutably across logical variants for tests.
51    #[must_use]
52    #[cfg(test)]
53    pub(in crate::db) const fn scalar_semantics_mut(&mut self) -> &mut ScalarPlan {
54        match self {
55            Self::Scalar(plan) => plan,
56            Self::Grouped(plan) => &mut plan.scalar,
57        }
58    }
59
60    /// Test-only shorthand for explicit scalar semantic borrowing.
61    #[must_use]
62    #[cfg(test)]
63    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
64        self.scalar_semantics()
65    }
66
67    /// Test-only shorthand for explicit mutable scalar semantic borrowing.
68    #[must_use]
69    #[cfg(test)]
70    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
71        self.scalar_semantics_mut()
72    }
73}
74
75impl AccessPlannedQuery {
76    /// Borrow scalar semantic fields shared by scalar/grouped logical variants.
77    #[must_use]
78    pub(in crate::db) const fn scalar_plan(&self) -> &ScalarPlan {
79        self.logical.scalar_semantics()
80    }
81
82    /// Borrow scalar semantic fields mutably across logical variants for tests.
83    #[must_use]
84    #[cfg(test)]
85    pub(in crate::db) const fn scalar_plan_mut(&mut self) -> &mut ScalarPlan {
86        self.logical.scalar_semantics_mut()
87    }
88
89    /// Test-only shorthand for explicit scalar plan borrowing.
90    #[must_use]
91    #[cfg(test)]
92    pub(in crate::db) const fn scalar(&self) -> &ScalarPlan {
93        self.scalar_plan()
94    }
95
96    /// Test-only shorthand for explicit mutable scalar plan borrowing.
97    #[must_use]
98    #[cfg(test)]
99    pub(in crate::db) const fn scalar_mut(&mut self) -> &mut ScalarPlan {
100        self.scalar_plan_mut()
101    }
102
103    /// Borrow grouped semantic fields when this plan is grouped.
104    #[must_use]
105    pub(in crate::db) const fn grouped_plan(&self) -> Option<&GroupPlan> {
106        match &self.logical {
107            LogicalPlan::Scalar(_) => None,
108            LogicalPlan::Grouped(plan) => Some(plan),
109        }
110    }
111
112    /// Lower this plan into one canonical planner-owned projection semantic spec.
113    #[must_use]
114    pub(in crate::db) fn projection_spec(&self, model: &EntityModel) -> ProjectionSpec {
115        lower_projection_intent(model, &self.logical, &self.projection_selection)
116    }
117
118    /// Lower this plan into one projection semantic shape for identity hashing.
119    #[must_use]
120    pub(in crate::db::query) fn projection_spec_for_identity(&self) -> ProjectionSpec {
121        lower_projection_identity(&self.logical)
122    }
123
124    /// Lower scalar DISTINCT semantics into one executor-facing execution strategy.
125    #[must_use]
126    pub(in crate::db) fn distinct_execution_strategy(&self) -> DistinctExecutionStrategy {
127        if !self.scalar_plan().distinct {
128            return DistinctExecutionStrategy::None;
129        }
130
131        // DISTINCT on duplicate-safe single-path access shapes is a planner
132        // no-op for runtime dedup mechanics. Composite shapes can surface
133        // duplicate keys and therefore retain explicit dedup execution.
134        match distinct_runtime_dedup_strategy(&self.access) {
135            Some(strategy) => strategy,
136            None => DistinctExecutionStrategy::None,
137        }
138    }
139
140    /// Project one planner-owned route profile for executor route planning.
141    #[must_use]
142    pub(in crate::db) fn planner_route_profile(&self, model: &EntityModel) -> PlannerRouteProfile {
143        PlannerRouteProfile::new(
144            derive_continuation_policy_validated(self),
145            derive_logical_pushdown_eligibility(model, self),
146        )
147    }
148
149    /// Build one immutable execution-shape signature contract for runtime layers.
150    #[must_use]
151    pub(in crate::db) fn execution_shape_signature(
152        &self,
153        entity_path: &'static str,
154    ) -> ExecutionShapeSignature {
155        ExecutionShapeSignature::new(self.continuation_signature(entity_path))
156    }
157
158    /// Return whether one filtered index predicate fully satisfies the current
159    /// scalar query predicate without any additional post-access filtering.
160    #[must_use]
161    pub(in crate::db) fn predicate_fully_satisfied_by_filtered_access(&self) -> bool {
162        let Some(query_predicate) = self.scalar_plan().predicate.as_ref() else {
163            return false;
164        };
165        let Some(index) = self.access.selected_index_model() else {
166            return false;
167        };
168
169        filtered_index_predicate_satisfies_query(index, query_predicate)
170    }
171
172    /// Return whether the scalar logical predicate still requires post-access
173    /// filtering after accounting for filtered-index guard predicates.
174    #[must_use]
175    pub(in crate::db) fn has_residual_predicate(&self) -> bool {
176        self.scalar_plan().predicate.is_some()
177            && !self.predicate_fully_satisfied_by_filtered_access()
178    }
179}
180
181fn distinct_runtime_dedup_strategy<K>(access: &AccessPlan<K>) -> Option<DistinctExecutionStrategy> {
182    match access {
183        AccessPlan::Union(_) | AccessPlan::Intersection(_) => {
184            Some(DistinctExecutionStrategy::PreOrdered)
185        }
186        AccessPlan::Path(path) if path.as_ref().is_index_multi_lookup() => {
187            Some(DistinctExecutionStrategy::HashMaterialize)
188        }
189        AccessPlan::Path(_) => None,
190    }
191}
192
193fn derive_continuation_policy_validated(plan: &AccessPlannedQuery) -> ContinuationPolicy {
194    let is_grouped_safe = plan
195        .grouped_plan()
196        .is_none_or(|grouped| grouped_cursor_policy_violation(grouped, true).is_none());
197
198    ContinuationPolicy::new(
199        true, // Continuation resume windows require anchor semantics for pushdown-safe replay.
200        true, // Continuation resumes must advance strictly to prevent replay/regression loops.
201        is_grouped_safe,
202    )
203}