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