Skip to main content

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

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