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