Skip to main content

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

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