Skip to main content

icydb_core/db/query/intent/
mod.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/// Encodes mode-specific fields so invalid states are unrepresentable.
23/// Mode checks are explicit and stable at execution time.
24///
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27pub enum QueryMode {
28    Load(LoadSpec),
29    Delete(DeleteSpec),
30}
31
32impl QueryMode {
33    /// True if this mode represents a load intent.
34    #[must_use]
35    pub const fn is_load(&self) -> bool {
36        match self {
37            Self::Load(_) => true,
38            Self::Delete(_) => false,
39        }
40    }
41
42    /// True if this mode represents a delete intent.
43    #[must_use]
44    pub const fn is_delete(&self) -> bool {
45        match self {
46            Self::Delete(_) => true,
47            Self::Load(_) => false,
48        }
49    }
50}
51
52///
53/// LoadSpec
54/// Mode-specific fields for load intents.
55/// Encodes pagination without leaking into delete intents.
56///
57
58#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
59pub struct LoadSpec {
60    pub limit: Option<u32>,
61    pub offset: u64,
62}
63
64impl LoadSpec {
65    /// Create an empty load spec.
66    #[must_use]
67    pub const fn new() -> Self {
68        Self {
69            limit: None,
70            offset: 0,
71        }
72    }
73}
74
75///
76/// DeleteSpec
77/// Mode-specific fields for delete intents.
78/// Encodes delete limits without leaking into load intents.
79///
80
81#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
82pub struct DeleteSpec {
83    pub limit: Option<u32>,
84}
85
86impl DeleteSpec {
87    /// Create an empty delete spec.
88    #[must_use]
89    pub const fn new() -> Self {
90        Self { limit: None }
91    }
92}
93
94///
95/// Query
96///
97/// Typed, declarative query intent for a specific entity type.
98///
99/// This intent is:
100/// - schema-agnostic at construction
101/// - normalized and validated only during planning
102/// - free of access-path decisions
103///
104
105#[derive(Debug)]
106pub struct Query<E: EntityKind> {
107    mode: QueryMode,
108    predicate: Option<Predicate>,
109    order: Option<OrderSpec>,
110    projection: ProjectionSpec,
111    consistency: ReadConsistency,
112    _marker: PhantomData<E>,
113}
114
115impl<E: EntityKind> Query<E> {
116    /// Create a new intent with an explicit missing-row policy.
117    /// MissingOk favors idempotency and may mask index/data divergence on deletes.
118    /// Use Strict to surface missing rows during scan/delete execution.
119    #[must_use]
120    pub const fn new(consistency: ReadConsistency) -> Self {
121        Self {
122            mode: QueryMode::Load(LoadSpec::new()),
123            predicate: None,
124            order: None,
125            projection: ProjectionSpec::All,
126            consistency,
127            _marker: PhantomData,
128        }
129    }
130
131    /// Return the intent mode (load vs delete).
132    #[must_use]
133    pub const fn mode(&self) -> QueryMode {
134        self.mode
135    }
136
137    /// Add a predicate, implicitly AND-ing with any existing predicate.
138    #[must_use]
139    pub fn filter(mut self, predicate: Predicate) -> Self {
140        self.predicate = match self.predicate.take() {
141            Some(existing) => Some(Predicate::And(vec![existing, predicate])),
142            None => Some(predicate),
143        };
144        self
145    }
146
147    /// Append an ascending sort key.
148    #[must_use]
149    pub fn order_by(mut self, field: &'static str) -> Self {
150        self.order = Some(push_order(self.order, field, OrderDirection::Asc));
151        self
152    }
153
154    /// Append a descending sort key.
155    #[must_use]
156    pub fn order_by_desc(mut self, field: &'static str) -> Self {
157        self.order = Some(push_order(self.order, field, OrderDirection::Desc));
158        self
159    }
160
161    /// Mark this intent as a delete query.
162    #[must_use]
163    pub const fn delete(mut self) -> Self {
164        if self.mode.is_load() {
165            self.mode = QueryMode::Delete(DeleteSpec::new());
166        }
167        self
168    }
169
170    /// Apply a limit to the current mode.
171    ///
172    /// Load limits bound result size; delete limits bound mutation size.
173    #[must_use]
174    pub const fn limit(mut self, limit: u32) -> Self {
175        match self.mode {
176            QueryMode::Load(mut spec) => {
177                spec.limit = Some(limit);
178                self.mode = QueryMode::Load(spec);
179            }
180            QueryMode::Delete(mut spec) => {
181                spec.limit = Some(limit);
182                self.mode = QueryMode::Delete(spec);
183            }
184        }
185        self
186    }
187
188    /// Apply an offset to a load intent.
189    #[must_use]
190    pub const fn offset(mut self, offset: u64) -> Self {
191        if let QueryMode::Load(mut spec) = self.mode {
192            spec.offset = offset;
193            self.mode = QueryMode::Load(spec);
194        }
195        self
196    }
197
198    /// Explain this intent without executing it.
199    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
200        let plan = self.build_plan::<E>()?;
201
202        Ok(plan.explain())
203    }
204
205    /// Plan this intent into an executor-ready plan.
206    pub fn plan(&self) -> Result<ExecutablePlan<E>, QueryError> {
207        let plan = self.build_plan::<E>()?;
208
209        Ok(ExecutablePlan::new(plan))
210    }
211
212    // Build a logical plan for the current intent.
213    fn build_plan<T: EntityKind>(&self) -> Result<LogicalPlan, QueryError> {
214        // Phase 1: schema surface and intent validation.
215        let model = T::MODEL;
216        let schema_info = SchemaInfo::from_entity_model(model)?;
217        self.validate_intent()?;
218
219        if let Some(order) = &self.order {
220            validate_order(&schema_info, order)?;
221        }
222
223        // Phase 2: predicate normalization and access planning.
224        let normalized_predicate = self.predicate.as_ref().map(normalize);
225        let access_plan = plan_access::<T>(&schema_info, normalized_predicate.as_ref())?;
226
227        validate_access_plan(&schema_info, model, &access_plan)?;
228
229        // Phase 3: assemble the executor-ready plan.
230        let plan = LogicalPlan {
231            mode: self.mode,
232            access: access_plan,
233            predicate: normalized_predicate,
234            order: self.order.clone(),
235            delete_limit: match self.mode {
236                QueryMode::Delete(spec) => spec.limit.map(|max_rows| DeleteLimitSpec { max_rows }),
237                QueryMode::Load(_) => None,
238            },
239            page: match self.mode {
240                QueryMode::Load(spec) => {
241                    if spec.limit.is_some() || spec.offset > 0 {
242                        Some(PageSpec {
243                            limit: spec.limit,
244                            offset: spec.offset,
245                        })
246                    } else {
247                        None
248                    }
249                }
250                QueryMode::Delete(_) => None,
251            },
252            projection: self.projection.clone(),
253            consistency: self.consistency,
254        };
255
256        Ok(plan)
257    }
258
259    // Validate delete-specific intent rules before planning.
260    const fn validate_intent(&self) -> Result<(), IntentError> {
261        match self.mode {
262            QueryMode::Load(_) => {}
263            QueryMode::Delete(spec) => {
264                if spec.limit.is_some() && self.order.is_none() {
265                    return Err(IntentError::DeleteLimitRequiresOrder);
266                }
267            }
268        }
269
270        Ok(())
271    }
272}
273
274///
275/// QueryError
276///
277
278#[derive(Debug, ThisError)]
279pub enum QueryError {
280    #[error("{0}")]
281    Validate(#[from] ValidateError),
282
283    #[error("{0}")]
284    Plan(#[from] PlanError),
285
286    #[error("{0}")]
287    Intent(#[from] IntentError),
288
289    #[error("{0}")]
290    Execute(#[from] InternalError),
291}
292
293impl From<PlannerError> for QueryError {
294    fn from(err: PlannerError) -> Self {
295        match err {
296            PlannerError::Plan(err) => Self::Plan(err),
297            PlannerError::Internal(err) => Self::Execute(err),
298        }
299    }
300}
301
302///
303/// IntentError
304///
305
306#[derive(Clone, Copy, Debug, ThisError)]
307pub enum IntentError {
308    #[error("delete limit requires an explicit ordering")]
309    DeleteLimitRequiresOrder,
310}
311
312/// Helper to append an ordering field while preserving existing order spec.
313fn push_order(
314    order: Option<OrderSpec>,
315    field: &'static str,
316    direction: OrderDirection,
317) -> OrderSpec {
318    match order {
319        Some(mut spec) => {
320            spec.fields.push((field.to_string(), direction));
321            spec
322        }
323        None => OrderSpec {
324            fields: vec![(field.to_string(), direction)],
325        },
326    }
327}
328
329#[cfg(test)]
330mod tests;