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