Skip to main content

icydb_core/db/query/fluent/load/
builder.rs

1use crate::{
2    db::{
3        DbSession,
4        predicate::{CompareOp, Predicate},
5        query::{
6            builder::aggregate::AggregateExpr,
7            explain::ExplainPlan,
8            expr::{FilterExpr, SortExpr},
9            intent::{CompiledQuery, PlannedQuery, Query, QueryError},
10            trace::QueryTracePlan,
11        },
12    },
13    traits::{EntityKind, SingletonEntity},
14    types::Id,
15    value::Value,
16};
17
18///
19/// FluentLoadQuery
20///
21/// Session-bound load query wrapper.
22/// Owns intent construction and execution routing only.
23/// Result inspection is provided by query API extension traits over `EntityResponse<E>`.
24///
25
26pub struct FluentLoadQuery<'a, E>
27where
28    E: EntityKind,
29{
30    pub(super) session: &'a DbSession<E::Canister>,
31    pub(super) query: Query<E>,
32    pub(super) cursor_token: Option<String>,
33}
34
35impl<'a, E> FluentLoadQuery<'a, E>
36where
37    E: EntityKind,
38{
39    pub(crate) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
40        Self {
41            session,
42            query,
43            cursor_token: None,
44        }
45    }
46
47    // ------------------------------------------------------------------
48    // Intent inspection
49    // ------------------------------------------------------------------
50
51    #[must_use]
52    pub const fn query(&self) -> &Query<E> {
53        &self.query
54    }
55
56    pub(super) fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
57        self.query = map(self.query);
58        self
59    }
60
61    pub(super) fn try_map_query(
62        mut self,
63        map: impl FnOnce(Query<E>) -> Result<Query<E>, QueryError>,
64    ) -> Result<Self, QueryError> {
65        self.query = map(self.query)?;
66        Ok(self)
67    }
68
69    // ------------------------------------------------------------------
70    // Intent builders (pure)
71    // ------------------------------------------------------------------
72
73    /// Set the access path to a single typed primary-key value.
74    ///
75    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
76    #[must_use]
77    pub fn by_id(mut self, id: Id<E>) -> Self {
78        self.query = self.query.by_id(id.key());
79        self
80    }
81
82    /// Set the access path to multiple typed primary-key values.
83    ///
84    /// IDs are public and may come from untrusted input sources.
85    #[must_use]
86    pub fn by_ids<I>(mut self, ids: I) -> Self
87    where
88        I: IntoIterator<Item = Id<E>>,
89    {
90        self.query = self.query.by_ids(ids.into_iter().map(|id| id.key()));
91        self
92    }
93
94    // ------------------------------------------------------------------
95    // Query Refinement
96    // ------------------------------------------------------------------
97
98    #[must_use]
99    pub fn filter(self, predicate: Predicate) -> Self {
100        self.map_query(|query| query.filter(predicate))
101    }
102
103    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
104        self.try_map_query(|query| query.filter_expr(expr))
105    }
106
107    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
108        self.try_map_query(|query| query.sort_expr(expr))
109    }
110
111    #[must_use]
112    pub fn order_by(self, field: impl AsRef<str>) -> Self {
113        self.map_query(|query| query.order_by(field))
114    }
115
116    #[must_use]
117    pub fn order_by_desc(self, field: impl AsRef<str>) -> Self {
118        self.map_query(|query| query.order_by_desc(field))
119    }
120
121    /// Add one grouped key field.
122    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
123        let field = field.as_ref().to_owned();
124        self.try_map_query(|query| query.group_by(&field))
125    }
126
127    /// Add one aggregate terminal via composable aggregate expression.
128    #[must_use]
129    pub fn aggregate(self, aggregate: AggregateExpr) -> Self {
130        self.map_query(|query| query.aggregate(aggregate))
131    }
132
133    /// Override grouped hard limits for grouped execution budget enforcement.
134    #[must_use]
135    pub fn grouped_limits(self, max_groups: u64, max_group_bytes: u64) -> Self {
136        self.map_query(|query| query.grouped_limits(max_groups, max_group_bytes))
137    }
138
139    /// Add one grouped HAVING compare clause over one grouped key field.
140    pub fn having_group(
141        self,
142        field: impl AsRef<str>,
143        op: CompareOp,
144        value: Value,
145    ) -> Result<Self, QueryError> {
146        let field = field.as_ref().to_owned();
147        self.try_map_query(|query| query.having_group(&field, op, value))
148    }
149
150    /// Add one grouped HAVING compare clause over one grouped aggregate output.
151    pub fn having_aggregate(
152        self,
153        aggregate_index: usize,
154        op: CompareOp,
155        value: Value,
156    ) -> Result<Self, QueryError> {
157        self.try_map_query(|query| query.having_aggregate(aggregate_index, op, value))
158    }
159
160    /// Bound the number of returned rows.
161    ///
162    /// Scalar pagination requires explicit ordering; combine `limit` and/or
163    /// `offset` with `order_by(...)` or planning fails for scalar loads.
164    /// GROUP BY pagination uses canonical grouped-key order by default.
165    #[must_use]
166    pub fn limit(self, limit: u32) -> Self {
167        self.map_query(|query| query.limit(limit))
168    }
169
170    /// Skip a number of rows in the ordered result stream.
171    ///
172    /// Scalar pagination requires explicit ordering; combine `offset` and/or
173    /// `limit` with `order_by(...)` or planning fails for scalar loads.
174    /// GROUP BY pagination uses canonical grouped-key order by default.
175    #[must_use]
176    pub fn offset(self, offset: u32) -> Self {
177        self.map_query(|query| query.offset(offset))
178    }
179
180    /// Attach an opaque cursor token for continuation pagination.
181    ///
182    /// Cursor-mode invariants are checked before planning/execution:
183    /// - explicit `order_by(...)` is required
184    /// - explicit `limit(...)` is required
185    #[must_use]
186    pub fn cursor(mut self, token: impl Into<String>) -> Self {
187        self.cursor_token = Some(token.into());
188        self
189    }
190
191    // ------------------------------------------------------------------
192    // Planning / diagnostics
193    // ------------------------------------------------------------------
194
195    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
196        self.query.explain()
197    }
198
199    /// Return the stable plan hash for this query.
200    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
201        self.query.plan_hash_hex()
202    }
203
204    /// Build one trace payload without executing the query.
205    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
206        self.session.trace_query(self.query())
207    }
208
209    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
210        if let Some(err) = self.cursor_intent_error() {
211            return Err(QueryError::Intent(err));
212        }
213
214        self.query.planned()
215    }
216
217    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
218        if let Some(err) = self.cursor_intent_error() {
219            return Err(QueryError::Intent(err));
220        }
221
222        self.query.plan()
223    }
224}
225
226impl<E> FluentLoadQuery<'_, E>
227where
228    E: EntityKind + SingletonEntity,
229    E::Key: Default,
230{
231    #[must_use]
232    pub fn only(self) -> Self {
233        self.map_query(Query::only)
234    }
235}