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    /// Borrow the current immutable query intent.
52    #[must_use]
53    pub const fn query(&self) -> &Query<E> {
54        &self.query
55    }
56
57    pub(super) fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
58        self.query = map(self.query);
59        self
60    }
61
62    pub(super) fn try_map_query(
63        mut self,
64        map: impl FnOnce(Query<E>) -> Result<Query<E>, QueryError>,
65    ) -> Result<Self, QueryError> {
66        self.query = map(self.query)?;
67        Ok(self)
68    }
69
70    // ------------------------------------------------------------------
71    // Intent builders (pure)
72    // ------------------------------------------------------------------
73
74    /// Set the access path to a single typed primary-key value.
75    ///
76    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
77    #[must_use]
78    pub fn by_id(mut self, id: Id<E>) -> Self {
79        self.query = self.query.by_id(id.key());
80        self
81    }
82
83    /// Set the access path to multiple typed primary-key values.
84    ///
85    /// IDs are public and may come from untrusted input sources.
86    #[must_use]
87    pub fn by_ids<I>(mut self, ids: I) -> Self
88    where
89        I: IntoIterator<Item = Id<E>>,
90    {
91        self.query = self.query.by_ids(ids.into_iter().map(|id| id.key()));
92        self
93    }
94
95    // ------------------------------------------------------------------
96    // Query Refinement
97    // ------------------------------------------------------------------
98
99    /// Add a typed predicate expression directly.
100    #[must_use]
101    pub fn filter(self, predicate: Predicate) -> Self {
102        self.map_query(|query| query.filter(predicate))
103    }
104
105    /// Add a serialized filter expression after lowering and validation.
106    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
107        self.try_map_query(|query| query.filter_expr(expr))
108    }
109
110    /// Add sort clauses from a serialized sort expression.
111    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
112        self.try_map_query(|query| query.sort_expr(expr))
113    }
114
115    /// Append ascending order for one field.
116    #[must_use]
117    pub fn order_by(self, field: impl AsRef<str>) -> Self {
118        self.map_query(|query| query.order_by(field))
119    }
120
121    /// Append descending order for one field.
122    #[must_use]
123    pub fn order_by_desc(self, field: impl AsRef<str>) -> Self {
124        self.map_query(|query| query.order_by_desc(field))
125    }
126
127    /// Add one grouped key field.
128    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
129        let field = field.as_ref().to_owned();
130        self.try_map_query(|query| query.group_by(&field))
131    }
132
133    /// Add one aggregate terminal via composable aggregate expression.
134    #[must_use]
135    pub fn aggregate(self, aggregate: AggregateExpr) -> Self {
136        self.map_query(|query| query.aggregate(aggregate))
137    }
138
139    /// Override grouped hard limits for grouped execution budget enforcement.
140    #[must_use]
141    pub fn grouped_limits(self, max_groups: u64, max_group_bytes: u64) -> Self {
142        self.map_query(|query| query.grouped_limits(max_groups, max_group_bytes))
143    }
144
145    /// Add one grouped HAVING compare clause over one grouped key field.
146    pub fn having_group(
147        self,
148        field: impl AsRef<str>,
149        op: CompareOp,
150        value: Value,
151    ) -> Result<Self, QueryError> {
152        let field = field.as_ref().to_owned();
153        self.try_map_query(|query| query.having_group(&field, op, value))
154    }
155
156    /// Add one grouped HAVING compare clause over one grouped aggregate output.
157    pub fn having_aggregate(
158        self,
159        aggregate_index: usize,
160        op: CompareOp,
161        value: Value,
162    ) -> Result<Self, QueryError> {
163        self.try_map_query(|query| query.having_aggregate(aggregate_index, op, value))
164    }
165
166    /// Bound the number of returned rows.
167    ///
168    /// Scalar pagination requires explicit ordering; combine `limit` and/or
169    /// `offset` with `order_by(...)` or planning fails for scalar loads.
170    /// GROUP BY pagination uses canonical grouped-key order by default.
171    #[must_use]
172    pub fn limit(self, limit: u32) -> Self {
173        self.map_query(|query| query.limit(limit))
174    }
175
176    /// Skip a number of rows in the ordered result stream.
177    ///
178    /// Scalar pagination requires explicit ordering; combine `offset` and/or
179    /// `limit` with `order_by(...)` or planning fails for scalar loads.
180    /// GROUP BY pagination uses canonical grouped-key order by default.
181    #[must_use]
182    pub fn offset(self, offset: u32) -> Self {
183        self.map_query(|query| query.offset(offset))
184    }
185
186    /// Attach an opaque cursor token for continuation pagination.
187    ///
188    /// Cursor-mode invariants are checked before planning/execution:
189    /// - explicit `order_by(...)` is required
190    /// - explicit `limit(...)` is required
191    #[must_use]
192    pub fn cursor(mut self, token: impl Into<String>) -> Self {
193        self.cursor_token = Some(token.into());
194        self
195    }
196
197    // ------------------------------------------------------------------
198    // Planning / diagnostics
199    // ------------------------------------------------------------------
200
201    /// Build explain metadata for the current query.
202    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
203        self.query.explain()
204    }
205
206    /// Return the stable plan hash for this query.
207    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
208        self.query.plan_hash_hex()
209    }
210
211    /// Build one trace payload without executing the query.
212    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
213        self.session.trace_query(self.query())
214    }
215
216    /// Build the validated logical plan without compiling execution details.
217    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
218        if let Some(err) = self.cursor_intent_error() {
219            return Err(QueryError::Intent(err));
220        }
221
222        self.query.planned()
223    }
224
225    /// Build the compiled executable plan for this query.
226    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
227        if let Some(err) = self.cursor_intent_error() {
228            return Err(QueryError::Intent(err));
229        }
230
231        self.query.plan()
232    }
233}
234
235impl<E> FluentLoadQuery<'_, E>
236where
237    E: EntityKind + SingletonEntity,
238    E::Key: Default,
239{
240    /// Constrain this query to the singleton entity row.
241    #[must_use]
242    pub fn only(self) -> Self {
243        self.map_query(Query::only)
244    }
245}