Skip to main content

icydb_core/db/query/plan/
mod.rs

1//! Query plan contracts, planning, and validation wiring.
2
3mod access_projection;
4pub(crate) mod planner;
5#[cfg(test)]
6mod tests;
7pub(crate) mod validate;
8
9use crate::{
10    db::{
11        access::{
12            AccessPlan, PushdownApplicability, SecondaryOrderPushdownEligibility,
13            assess_secondary_order_pushdown_from_parts,
14            assess_secondary_order_pushdown_if_applicable_validated_from_parts,
15        },
16        contracts::ReadConsistency,
17        direction::Direction,
18        query::predicate::Predicate,
19    },
20    model::entity::EntityModel,
21};
22use std::ops::{Deref, DerefMut};
23
24pub(in crate::db) use crate::db::query::fingerprint::canonical;
25pub(crate) use access_projection::{
26    AccessPlanProjection, project_access_plan, project_explain_access_path,
27};
28
29pub(crate) use validate::OrderPlanError;
30///
31/// Re-Exports
32///
33pub use validate::PlanError;
34
35///
36/// QueryMode
37///
38/// Discriminates load vs delete intent at planning time.
39/// Encodes mode-specific fields so invalid states are unrepresentable.
40/// Mode checks are explicit and stable at execution time.
41///
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum QueryMode {
44    Load(LoadSpec),
45    Delete(DeleteSpec),
46}
47
48impl QueryMode {
49    /// True if this mode represents a load intent.
50    #[must_use]
51    pub const fn is_load(&self) -> bool {
52        match self {
53            Self::Load(_) => true,
54            Self::Delete(_) => false,
55        }
56    }
57
58    /// True if this mode represents a delete intent.
59    #[must_use]
60    pub const fn is_delete(&self) -> bool {
61        match self {
62            Self::Delete(_) => true,
63            Self::Load(_) => false,
64        }
65    }
66}
67
68///
69/// LoadSpec
70///
71/// Mode-specific fields for load intents.
72/// Encodes pagination without leaking into delete intents.
73///
74#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
75pub struct LoadSpec {
76    pub limit: Option<u32>,
77    pub offset: u32,
78}
79
80impl LoadSpec {
81    /// Create an empty load spec.
82    #[must_use]
83    pub const fn new() -> Self {
84        Self {
85            limit: None,
86            offset: 0,
87        }
88    }
89}
90
91///
92/// DeleteSpec
93///
94/// Mode-specific fields for delete intents.
95/// Encodes delete limits without leaking into load intents.
96///
97#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
98pub struct DeleteSpec {
99    pub limit: Option<u32>,
100}
101
102impl DeleteSpec {
103    /// Create an empty delete spec.
104    #[must_use]
105    pub const fn new() -> Self {
106        Self { limit: None }
107    }
108}
109
110///
111/// OrderDirection
112/// Executor-facing ordering direction (applied after filtering).
113///
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
115pub enum OrderDirection {
116    Asc,
117    Desc,
118}
119
120///
121/// OrderSpec
122/// Executor-facing ordering specification.
123///
124#[derive(Clone, Debug, Eq, PartialEq)]
125pub(crate) struct OrderSpec {
126    pub(crate) fields: Vec<(String, OrderDirection)>,
127}
128
129///
130/// DeleteLimitSpec
131/// Executor-facing delete bound with no offsets.
132///
133#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134pub(crate) struct DeleteLimitSpec {
135    pub max_rows: u32,
136}
137
138///
139/// PageSpec
140/// Executor-facing pagination specification.
141///
142#[derive(Clone, Debug, Eq, PartialEq)]
143pub(crate) struct PageSpec {
144    pub limit: Option<u32>,
145    pub offset: u32,
146}
147
148///
149/// LogicalPlan
150///
151/// Pure logical query intent produced by the planner.
152///
153/// A `LogicalPlan` represents the access-independent query semantics:
154/// predicate/filter, ordering, distinct behavior, pagination/delete windows,
155/// and read-consistency mode.
156///
157/// Design notes:
158/// - Predicates are applied *after* data access
159/// - Ordering is applied after filtering
160/// - Pagination is applied after ordering (load only)
161/// - Delete limits are applied after ordering (delete only)
162/// - Missing-row policy is explicit and must not depend on access strategy
163///
164/// This struct is the logical compiler stage output and intentionally excludes
165/// access-path details.
166///
167#[derive(Clone, Debug, Eq, PartialEq)]
168pub(crate) struct LogicalPlan {
169    /// Load vs delete intent.
170    pub(crate) mode: QueryMode,
171
172    /// Optional residual predicate applied after access.
173    pub(crate) predicate: Option<Predicate>,
174
175    /// Optional ordering specification.
176    pub(crate) order: Option<OrderSpec>,
177
178    /// Optional distinct semantics over ordered rows.
179    pub(crate) distinct: bool,
180
181    /// Optional delete bound (delete intents only).
182    pub(crate) delete_limit: Option<DeleteLimitSpec>,
183
184    /// Optional pagination specification.
185    pub(crate) page: Option<PageSpec>,
186
187    /// Missing-row policy for execution.
188    pub(crate) consistency: ReadConsistency,
189}
190
191///
192/// AccessPlannedQuery
193///
194/// Access-planned query produced after access-path selection.
195/// Binds one pure `LogicalPlan` to one chosen `AccessPlan`.
196///
197#[derive(Clone, Debug, Eq, PartialEq)]
198pub(crate) struct AccessPlannedQuery<K> {
199    pub(crate) logical: LogicalPlan,
200    pub(crate) access: AccessPlan<K>,
201}
202
203impl<K> AccessPlannedQuery<K> {
204    /// Construct an access-planned query from logical + access stages.
205    #[must_use]
206    pub(crate) const fn from_parts(logical: LogicalPlan, access: AccessPlan<K>) -> Self {
207        Self { logical, access }
208    }
209
210    /// Decompose into logical + access stages.
211    #[must_use]
212    pub(crate) fn into_parts(self) -> (LogicalPlan, AccessPlan<K>) {
213        (self.logical, self.access)
214    }
215
216    /// Construct a minimal access-planned query with only an access path.
217    ///
218    /// Predicates, ordering, and pagination may be attached later.
219    #[cfg(test)]
220    pub(crate) fn new(
221        access: crate::db::access::AccessPath<K>,
222        consistency: ReadConsistency,
223    ) -> Self {
224        Self {
225            logical: LogicalPlan {
226                mode: QueryMode::Load(LoadSpec::new()),
227                predicate: None,
228                order: None,
229                distinct: false,
230                delete_limit: None,
231                page: None,
232                consistency,
233            },
234            access: AccessPlan::path(access),
235        }
236    }
237}
238
239impl<K> Deref for AccessPlannedQuery<K> {
240    type Target = LogicalPlan;
241
242    fn deref(&self) -> &Self::Target {
243        &self.logical
244    }
245}
246
247impl<K> DerefMut for AccessPlannedQuery<K> {
248    fn deref_mut(&mut self) -> &mut Self::Target {
249        &mut self.logical
250    }
251}
252
253fn direction_from_order(direction: OrderDirection) -> Direction {
254    if direction == OrderDirection::Desc {
255        Direction::Desc
256    } else {
257        Direction::Asc
258    }
259}
260
261fn order_fields_as_direction_refs(
262    order_fields: &[(String, OrderDirection)],
263) -> Vec<(&str, Direction)> {
264    order_fields
265        .iter()
266        .map(|(field, direction)| (field.as_str(), direction_from_order(*direction)))
267        .collect()
268}
269
270/// Evaluate the secondary-index ORDER BY pushdown matrix for one plan.
271pub(crate) fn assess_secondary_order_pushdown<K>(
272    model: &EntityModel,
273    plan: &AccessPlannedQuery<K>,
274) -> SecondaryOrderPushdownEligibility {
275    let order_fields = plan
276        .order
277        .as_ref()
278        .map(|order| order_fields_as_direction_refs(&order.fields));
279
280    assess_secondary_order_pushdown_from_parts(model, order_fields.as_deref(), &plan.access)
281}
282
283#[cfg(test)]
284/// Evaluate pushdown eligibility only when secondary-index ORDER BY is applicable.
285pub(crate) fn assess_secondary_order_pushdown_if_applicable<K>(
286    model: &EntityModel,
287    plan: &AccessPlannedQuery<K>,
288) -> PushdownApplicability {
289    let order_fields = plan
290        .order
291        .as_ref()
292        .map(|order| order_fields_as_direction_refs(&order.fields));
293
294    crate::db::access::assess_secondary_order_pushdown_if_applicable_from_parts(
295        model,
296        order_fields.as_deref(),
297        &plan.access,
298    )
299}
300
301/// Evaluate pushdown applicability for plans that have already passed full
302/// logical/executor validation.
303pub(crate) fn assess_secondary_order_pushdown_if_applicable_validated<K>(
304    model: &EntityModel,
305    plan: &AccessPlannedQuery<K>,
306) -> PushdownApplicability {
307    let order_fields = plan
308        .order
309        .as_ref()
310        .map(|order| order_fields_as_direction_refs(&order.fields));
311
312    assess_secondary_order_pushdown_if_applicable_validated_from_parts(
313        model,
314        order_fields.as_deref(),
315        &plan.access,
316    )
317}