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