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