Skip to main content

icydb_core/db/query/
intent.rs

1use crate::{
2    db::query::{
3        ReadConsistency,
4        plan::{
5            DeleteLimitSpec, ExecutablePlan, ExplainPlan, LogicalPlan, OrderDirection, OrderSpec,
6            PageSpec, PlanError, ProjectionSpec,
7            planner::{PlannerError, plan_access},
8            validate::validate_access_plan,
9            validate::validate_order,
10        },
11        predicate::{Predicate, SchemaInfo, ValidateError, normalize},
12    },
13    error::InternalError,
14    traits::EntityKind,
15};
16use std::marker::PhantomData;
17use thiserror::Error as ThisError;
18
19///
20/// QueryMode
21/// Discriminates load vs delete intent at planning time.
22///
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum QueryMode {
26    Load,
27    Delete,
28}
29
30///
31/// DeleteLimit
32/// Declarative deletion bound for a query window.
33/// Expressed as a max row count; no offsets.
34///
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub struct DeleteLimit {
38    pub max_rows: u32,
39}
40
41impl DeleteLimit {
42    /// Create a new delete limit bound.
43    #[must_use]
44    pub const fn new(max_rows: u32) -> Self {
45        Self { max_rows }
46    }
47
48    pub(crate) const fn to_spec(self) -> DeleteLimitSpec {
49        DeleteLimitSpec {
50            max_rows: self.max_rows,
51        }
52    }
53}
54
55///
56/// Query
57///
58/// Typed, declarative query intent for a specific entity type.
59///
60/// This intent is:
61/// - schema-agnostic at construction
62/// - normalized and validated only during planning
63/// - free of access-path decisions
64///
65
66#[derive(Debug)]
67pub struct Query<E: EntityKind> {
68    mode: QueryMode,
69    predicate: Option<Predicate>,
70    order: Option<OrderSpec>,
71    delete_limit: Option<DeleteLimit>,
72    page: Option<Page>,
73    projection: ProjectionSpec,
74    consistency: ReadConsistency,
75    _marker: PhantomData<E>,
76}
77
78///
79/// Page
80/// Declarative pagination intent for a query window.
81/// Expressed as limit/offset only; no response semantics.
82///
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85pub struct Page {
86    pub limit: u32,
87    pub offset: u64,
88}
89
90impl Page {
91    /// Create a new pagination intent with a limit and offset.
92    #[must_use]
93    pub const fn new(limit: u32, offset: u64) -> Self {
94        Self { limit, offset }
95    }
96
97    pub(crate) const fn to_spec(self) -> PageSpec {
98        PageSpec {
99            limit: Some(self.limit),
100            offset: self.offset,
101        }
102    }
103}
104
105impl<E: EntityKind> Query<E> {
106    /// Create a new intent with an explicit missing-row policy.
107    /// MissingOk favors idempotency and may mask index/data divergence on deletes.
108    /// Use Strict to surface missing rows during scan/delete execution.
109    #[must_use]
110    pub const fn new(consistency: ReadConsistency) -> Self {
111        Self {
112            mode: QueryMode::Load,
113            predicate: None,
114            order: None,
115            delete_limit: None,
116            page: None,
117            projection: ProjectionSpec::All,
118            consistency,
119            _marker: PhantomData,
120        }
121    }
122
123    /// Add a predicate, implicitly AND-ing with any existing predicate.
124    #[must_use]
125    pub fn filter(mut self, predicate: Predicate) -> Self {
126        self.predicate = match self.predicate.take() {
127            Some(existing) => Some(Predicate::And(vec![existing, predicate])),
128            None => Some(predicate),
129        };
130        self
131    }
132
133    /// Append an ascending sort key.
134    #[must_use]
135    pub fn order_by(mut self, field: &'static str) -> Self {
136        self.order = Some(push_order(self.order, field, OrderDirection::Asc));
137        self
138    }
139
140    /// Append a descending sort key.
141    #[must_use]
142    pub fn order_by_desc(mut self, field: &'static str) -> Self {
143        self.order = Some(push_order(self.order, field, OrderDirection::Desc));
144        self
145    }
146
147    /// Mark this intent as a delete query.
148    #[must_use]
149    pub const fn delete(mut self) -> Self {
150        self.mode = QueryMode::Delete;
151        self
152    }
153
154    /// Bound a delete query to at most `max_rows` rows.
155    #[must_use]
156    pub const fn delete_limit(mut self, max_rows: u32) -> Self {
157        self.delete_limit = Some(DeleteLimit::new(max_rows));
158        self
159    }
160
161    /// Replace the current pagination settings with an explicit limit/offset window.
162    /// Pagination is part of intent and is enforced during planning.
163    #[must_use]
164    pub const fn page(mut self, limit: u32, offset: u64) -> Self {
165        self.page = Some(Page::new(limit, offset));
166        self
167    }
168
169    /// Explain this intent without executing it.
170    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
171        let plan = self.build_plan::<E>()?;
172        Ok(plan.explain())
173    }
174
175    /// Plan this intent into an executor-ready plan.
176    pub fn plan(&self) -> Result<ExecutablePlan<E>, QueryError> {
177        let plan = self.build_plan::<E>()?;
178        Ok(ExecutablePlan::new(plan))
179    }
180
181    fn build_plan<T: EntityKind>(&self) -> Result<LogicalPlan, QueryError> {
182        // Phase 1: schema surface and intent validation.
183        let model = T::MODEL;
184        let schema_info = SchemaInfo::from_entity_model(model)?;
185        self.validate_intent()?;
186
187        if let Some(order) = &self.order {
188            validate_order(&schema_info, order)?;
189        }
190
191        // Phase 2: predicate normalization and access planning.
192        let normalized_predicate = self.predicate.as_ref().map(normalize);
193        let access_plan = plan_access::<T>(&schema_info, normalized_predicate.as_ref())?;
194
195        validate_access_plan(&schema_info, model, &access_plan)?;
196
197        // Phase 3: assemble the executor-ready plan.
198        let plan = LogicalPlan {
199            mode: self.mode,
200            access: access_plan,
201            predicate: normalized_predicate,
202            order: self.order.clone(),
203            delete_limit: self.delete_limit.map(DeleteLimit::to_spec),
204            page: self.page.map(Page::to_spec),
205            projection: self.projection.clone(),
206            consistency: self.consistency,
207        };
208
209        Ok(plan)
210    }
211
212    // Validate delete-specific intent rules before planning.
213    const fn validate_intent(&self) -> Result<(), IntentError> {
214        match self.mode {
215            QueryMode::Load => {
216                if self.delete_limit.is_some() {
217                    return Err(IntentError::DeleteLimitOnLoad);
218                }
219            }
220            QueryMode::Delete => {
221                if self.page.is_some() && self.delete_limit.is_some() {
222                    return Err(IntentError::DeleteLimitWithPagination);
223                }
224                if self.page.is_some() {
225                    return Err(IntentError::DeletePaginationNotSupported);
226                }
227                if self.delete_limit.is_some() && self.order.is_none() {
228                    return Err(IntentError::DeleteLimitRequiresOrder);
229                }
230            }
231        }
232
233        Ok(())
234    }
235}
236
237///
238/// QueryError
239///
240#[derive(Debug, ThisError)]
241pub enum QueryError {
242    #[error("{0}")]
243    Validate(#[from] ValidateError),
244    #[error("{0}")]
245    Plan(#[from] PlanError),
246    #[error("{0}")]
247    Intent(#[from] IntentError),
248    #[error("{0}")]
249    Execute(#[from] InternalError),
250}
251
252impl From<PlannerError> for QueryError {
253    fn from(err: PlannerError) -> Self {
254        match err {
255            PlannerError::Plan(err) => Self::Plan(err),
256            PlannerError::Internal(err) => Self::Execute(err),
257        }
258    }
259}
260
261///
262/// IntentError
263///
264#[derive(Debug, ThisError)]
265pub enum IntentError {
266    #[error("delete limit is only valid for delete intents")]
267    DeleteLimitOnLoad,
268    #[error("delete queries do not support pagination offsets")]
269    DeletePaginationNotSupported,
270    #[error("delete limit cannot be combined with pagination")]
271    DeleteLimitWithPagination,
272    #[error("delete limit requires an explicit ordering")]
273    DeleteLimitRequiresOrder,
274}
275
276/// Helper to append an ordering field while preserving existing order spec.
277fn push_order(
278    order: Option<OrderSpec>,
279    field: &'static str,
280    direction: OrderDirection,
281) -> OrderSpec {
282    match order {
283        Some(mut spec) => {
284            spec.fields.push((field.to_string(), direction));
285            spec
286        }
287        None => OrderSpec {
288            fields: vec![(field.to_string(), direction)],
289        },
290    }
291}
292
293#[cfg(test)]
294mod tests;