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