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, OrderTerm},
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    /// Append one typed ORDER BY term.
124    #[must_use]
125    pub fn order_term(self, term: OrderTerm) -> Self {
126        self.map_query(|query| query.order_term(term))
127    }
128
129    /// Append multiple typed ORDER BY terms in declaration order.
130    #[must_use]
131    pub fn order_terms<I>(self, terms: I) -> Self
132    where
133        I: IntoIterator<Item = OrderTerm>,
134    {
135        self.map_query(|query| query.order_terms(terms))
136    }
137
138    /// Add one grouped key field.
139    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
140        let field = field.as_ref().to_owned();
141        self.try_map_query(|query| query.group_by(&field))
142    }
143
144    /// Add one aggregate terminal via composable aggregate expression.
145    #[must_use]
146    pub fn aggregate(self, aggregate: AggregateExpr) -> Self {
147        self.map_query(|query| query.aggregate(aggregate))
148    }
149
150    /// Override grouped hard limits for grouped execution budget enforcement.
151    #[must_use]
152    pub fn grouped_limits(self, max_groups: u64, max_group_bytes: u64) -> Self {
153        self.map_query(|query| query.grouped_limits(max_groups, max_group_bytes))
154    }
155
156    /// Add one grouped HAVING compare clause over one grouped key field.
157    pub fn having_group(
158        self,
159        field: impl AsRef<str>,
160        op: CompareOp,
161        value: Value,
162    ) -> Result<Self, QueryError> {
163        let field = field.as_ref().to_owned();
164        self.try_map_query(|query| query.having_group(&field, op, value))
165    }
166
167    /// Add one grouped HAVING compare clause over one grouped aggregate output.
168    pub fn having_aggregate(
169        self,
170        aggregate_index: usize,
171        op: CompareOp,
172        value: Value,
173    ) -> Result<Self, QueryError> {
174        self.try_map_query(|query| query.having_aggregate(aggregate_index, op, value))
175    }
176
177    /// Bound the number of returned rows.
178    ///
179    /// Scalar pagination requires explicit ordering; combine `limit` and/or
180    /// `offset` with `order_term(...)` or planning fails for scalar loads.
181    /// GROUP BY pagination uses canonical grouped-key order by default.
182    #[must_use]
183    pub fn limit(self, limit: u32) -> Self {
184        self.map_query(|query| query.limit(limit))
185    }
186
187    /// Skip a number of rows in the ordered result stream.
188    ///
189    /// Scalar pagination requires explicit ordering; combine `offset` and/or
190    /// `limit` with `order_term(...)` or planning fails for scalar loads.
191    /// GROUP BY pagination uses canonical grouped-key order by default.
192    #[must_use]
193    pub fn offset(self, offset: u32) -> Self {
194        self.map_query(|query| query.offset(offset))
195    }
196
197    /// Attach an opaque cursor token for continuation pagination.
198    ///
199    /// Cursor-mode invariants are checked before planning/execution:
200    /// - explicit `order_term(...)` is required
201    /// - explicit `limit(...)` is required
202    #[must_use]
203    pub fn cursor(mut self, token: impl Into<String>) -> Self {
204        self.cursor_token = Some(token.into());
205        self
206    }
207
208    // ------------------------------------------------------------------
209    // Planning / diagnostics
210    // ------------------------------------------------------------------
211
212    /// Build explain metadata for the current query.
213    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
214        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
215    }
216
217    /// Return the stable plan hash for this query.
218    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
219        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
220    }
221
222    /// Build one trace payload without executing the query.
223    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
224        self.map_session_query_output(DbSession::trace_query)
225    }
226
227    /// Build the validated logical plan without compiling execution details.
228    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
229        self.ensure_cursor_mode_ready()?;
230        self.map_session_query_output(DbSession::planned_query_with_visible_indexes)
231    }
232
233    /// Build the compiled executable plan for this query.
234    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
235        self.ensure_cursor_mode_ready()?;
236        self.map_session_query_output(DbSession::compile_query_with_visible_indexes)
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}