Skip to main content

icydb_core/db/query/intent/
query.rs

1use crate::{
2    db::{
3        predicate::{CompareOp, MissingRowPolicy, Predicate},
4        query::{
5            builder::aggregate::AggregateExpr,
6            explain::ExplainPlan,
7            expr::{FilterExpr, SortExpr},
8            intent::{
9                LoadSpec, QueryError, QueryMode, access_plan_to_entity_keys, model::QueryModel,
10            },
11            plan::AccessPlannedQuery,
12        },
13    },
14    traits::{EntityKind, SingletonEntity},
15    value::Value,
16};
17use std::marker::PhantomData;
18
19///
20/// Query
21///
22/// Typed, declarative query intent for a specific entity type.
23///
24/// This intent is:
25/// - schema-agnostic at construction
26/// - normalized and validated only during planning
27/// - free of access-path decisions
28///
29
30///
31/// PlannedQuery
32///
33/// Neutral query-owned planned contract produced by query planning.
34/// Stores logical + access shape without executor compilation state.
35///
36#[derive(Debug)]
37pub struct PlannedQuery<E: EntityKind> {
38    plan: AccessPlannedQuery<E::Key>,
39    _marker: PhantomData<E>,
40}
41
42impl<E: EntityKind> PlannedQuery<E> {
43    #[must_use]
44    pub(in crate::db) const fn new(plan: AccessPlannedQuery<E::Key>) -> Self {
45        Self {
46            plan,
47            _marker: PhantomData,
48        }
49    }
50
51    #[must_use]
52    pub fn explain(&self) -> ExplainPlan {
53        self.plan.explain_with_model(E::MODEL)
54    }
55}
56
57///
58/// CompiledQuery
59///
60/// Query-owned compiled handoff produced by `Query::plan()`.
61/// This type intentionally carries only logical/access query semantics.
62/// Executor runtime shape is derived explicitly at the executor boundary.
63///
64#[derive(Clone, Debug)]
65pub struct CompiledQuery<E: EntityKind> {
66    plan: AccessPlannedQuery<E::Key>,
67    _marker: PhantomData<E>,
68}
69
70impl<E: EntityKind> CompiledQuery<E> {
71    #[must_use]
72    pub(in crate::db) const fn new(plan: AccessPlannedQuery<E::Key>) -> Self {
73        Self {
74            plan,
75            _marker: PhantomData,
76        }
77    }
78
79    #[must_use]
80    pub fn explain(&self) -> ExplainPlan {
81        self.plan.explain_with_model(E::MODEL)
82    }
83
84    /// Borrow planner-lowered projection semantics for this compiled query.
85    #[must_use]
86    #[cfg(test)]
87    pub(crate) fn projection_spec(&self) -> crate::db::query::plan::expr::ProjectionSpec {
88        self.plan.projection_spec(E::MODEL)
89    }
90
91    #[must_use]
92    pub(in crate::db) fn into_inner(self) -> AccessPlannedQuery<E::Key> {
93        self.plan
94    }
95}
96
97#[derive(Debug)]
98pub struct Query<E: EntityKind> {
99    intent: QueryModel<'static, E::Key>,
100    _marker: PhantomData<E>,
101}
102
103impl<E: EntityKind> Query<E> {
104    /// Create a new intent with an explicit missing-row policy.
105    /// Ignore favors idempotency and may mask index/data divergence on deletes.
106    /// Use Error to surface missing rows during scan/delete execution.
107    #[must_use]
108    pub const fn new(consistency: MissingRowPolicy) -> Self {
109        Self {
110            intent: QueryModel::new(E::MODEL, consistency),
111            _marker: PhantomData,
112        }
113    }
114
115    /// Return the intent mode (load vs delete).
116    #[must_use]
117    pub const fn mode(&self) -> QueryMode {
118        self.intent.mode()
119    }
120
121    #[must_use]
122    pub(crate) fn has_explicit_order(&self) -> bool {
123        self.intent.has_explicit_order()
124    }
125
126    #[must_use]
127    pub(crate) const fn has_grouping(&self) -> bool {
128        self.intent.has_grouping()
129    }
130
131    #[must_use]
132    pub(crate) const fn load_spec(&self) -> Option<LoadSpec> {
133        self.intent.load_spec()
134    }
135
136    /// Add a predicate, implicitly AND-ing with any existing predicate.
137    #[must_use]
138    pub fn filter(mut self, predicate: Predicate) -> Self {
139        self.intent = self.intent.filter(predicate);
140        self
141    }
142
143    /// Apply a dynamic filter expression.
144    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
145        let Self { intent, _marker } = self;
146        let intent = intent.filter_expr(expr)?;
147
148        Ok(Self { intent, _marker })
149    }
150
151    /// Apply a dynamic sort expression.
152    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
153        let Self { intent, _marker } = self;
154        let intent = intent.sort_expr(expr)?;
155
156        Ok(Self { intent, _marker })
157    }
158
159    /// Append an ascending sort key.
160    #[must_use]
161    pub fn order_by(mut self, field: impl AsRef<str>) -> Self {
162        self.intent = self.intent.order_by(field);
163        self
164    }
165
166    /// Append a descending sort key.
167    #[must_use]
168    pub fn order_by_desc(mut self, field: impl AsRef<str>) -> Self {
169        self.intent = self.intent.order_by_desc(field);
170        self
171    }
172
173    /// Enable DISTINCT semantics for this query.
174    #[must_use]
175    pub fn distinct(mut self) -> Self {
176        self.intent = self.intent.distinct();
177        self
178    }
179
180    /// Add one GROUP BY field.
181    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
182        let Self { intent, _marker } = self;
183        let intent = intent.push_group_field(field.as_ref())?;
184
185        Ok(Self { intent, _marker })
186    }
187
188    /// Add one aggregate terminal via composable aggregate expression.
189    #[must_use]
190    pub fn aggregate(mut self, aggregate: AggregateExpr) -> Self {
191        self.intent = self.intent.push_group_aggregate(aggregate);
192        self
193    }
194
195    /// Override grouped hard limits for grouped execution budget enforcement.
196    #[must_use]
197    pub fn grouped_limits(mut self, max_groups: u64, max_group_bytes: u64) -> Self {
198        self.intent = self.intent.grouped_limits(max_groups, max_group_bytes);
199        self
200    }
201
202    /// Add one grouped HAVING compare clause over one grouped key field.
203    pub fn having_group(
204        self,
205        field: impl AsRef<str>,
206        op: CompareOp,
207        value: Value,
208    ) -> Result<Self, QueryError> {
209        let field = field.as_ref().to_owned();
210        let Self { intent, _marker } = self;
211        let intent = intent.push_having_group_clause(&field, op, value)?;
212
213        Ok(Self { intent, _marker })
214    }
215
216    /// Add one grouped HAVING compare clause over one grouped aggregate output.
217    pub fn having_aggregate(
218        self,
219        aggregate_index: usize,
220        op: CompareOp,
221        value: Value,
222    ) -> Result<Self, QueryError> {
223        let Self { intent, _marker } = self;
224        let intent = intent.push_having_aggregate_clause(aggregate_index, op, value)?;
225
226        Ok(Self { intent, _marker })
227    }
228
229    /// Set the access path to a single primary key lookup.
230    pub(crate) fn by_id(self, id: E::Key) -> Self {
231        let Self { intent, _marker } = self;
232        Self {
233            intent: intent.by_id(id),
234            _marker,
235        }
236    }
237
238    /// Set the access path to a primary key batch lookup.
239    pub(crate) fn by_ids<I>(self, ids: I) -> Self
240    where
241        I: IntoIterator<Item = E::Key>,
242    {
243        let Self { intent, _marker } = self;
244        Self {
245            intent: intent.by_ids(ids),
246            _marker,
247        }
248    }
249
250    /// Mark this intent as a delete query.
251    #[must_use]
252    pub fn delete(mut self) -> Self {
253        self.intent = self.intent.delete();
254        self
255    }
256
257    /// Apply a limit to the current mode.
258    ///
259    /// Load limits bound result size; delete limits bound mutation size.
260    /// For scalar load queries, any use of `limit` or `offset` requires an
261    /// explicit `order_by(...)` so pagination is deterministic.
262    /// GROUP BY queries use canonical grouped-key order by default.
263    #[must_use]
264    pub fn limit(mut self, limit: u32) -> Self {
265        self.intent = self.intent.limit(limit);
266        self
267    }
268
269    /// Apply an offset to a load intent.
270    ///
271    /// Scalar pagination requires an explicit `order_by(...)`.
272    /// GROUP BY queries use canonical grouped-key order by default.
273    #[must_use]
274    pub fn offset(mut self, offset: u32) -> Self {
275        self.intent = self.intent.offset(offset);
276        self
277    }
278
279    /// Explain this intent without executing it.
280    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
281        let plan = self.planned()?;
282
283        Ok(plan.explain())
284    }
285
286    /// Plan this intent into a neutral planned query contract.
287    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
288        let plan = self.build_plan()?;
289        let _projection = plan.projection_spec(E::MODEL);
290
291        Ok(PlannedQuery::new(plan))
292    }
293
294    /// Compile this intent into query-owned handoff state.
295    ///
296    /// This boundary intentionally does not expose executor runtime shape.
297    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
298        let plan = self.build_plan()?;
299        let _projection = plan.projection_spec(E::MODEL);
300
301        Ok(CompiledQuery::new(plan))
302    }
303
304    // Build a logical plan for the current intent.
305    fn build_plan(&self) -> Result<AccessPlannedQuery<E::Key>, QueryError> {
306        let plan_value = self.intent.build_plan_model()?;
307        let (logical, access) = plan_value.into_parts();
308        let access = access_plan_to_entity_keys::<E>(E::MODEL, access)?;
309        let plan = AccessPlannedQuery::from_parts(logical, access);
310
311        Ok(plan)
312    }
313}
314
315impl<E> Query<E>
316where
317    E: EntityKind + SingletonEntity,
318    E::Key: Default,
319{
320    /// Set the access path to the singleton primary key.
321    pub(crate) fn only(self) -> Self {
322        let Self { intent, _marker } = self;
323
324        Self {
325            intent: intent.only(E::Key::default()),
326            _marker,
327        }
328    }
329}