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            admission::{QueryAdmissionPolicy, QueryAdmissionSummary},
12            builder::aggregate::AggregateExpr,
13            explain::ExplainPlan,
14            expr::{FilterExpr, OrderTerm},
15            intent::{CompiledQuery, PlannedQuery, Query, QueryError},
16            trace::QueryTracePlan,
17        },
18    },
19    traits::{EntityKind, SingletonEntity},
20    types::Id,
21    value::InputValue,
22};
23
24///
25/// FluentLoadQuery
26///
27/// Session-bound load query wrapper.
28/// Owns intent construction and execution routing only.
29/// Result inspection is provided by query API extension traits over `EntityResponse<E>`.
30///
31
32pub struct FluentLoadQuery<'a, E>
33where
34    E: EntityKind,
35{
36    pub(super) session: &'a DbSession<E::Canister>,
37    pub(super) query: Query<E>,
38    pub(super) cursor_token: Option<String>,
39}
40
41impl<'a, E> FluentLoadQuery<'a, E>
42where
43    E: EntityKind,
44{
45    pub(in crate::db) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
46        Self {
47            session,
48            query,
49            cursor_token: None,
50        }
51    }
52
53    // ------------------------------------------------------------------
54    // Intent inspection
55    // ------------------------------------------------------------------
56
57    /// Borrow the current immutable query intent.
58    #[must_use]
59    pub const fn query(&self) -> &Query<E> {
60        &self.query
61    }
62
63    pub(super) fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
64        self.query = map(self.query);
65        self
66    }
67
68    pub(super) fn try_map_query(
69        mut self,
70        map: impl FnOnce(Query<E>) -> Result<Query<E>, QueryError>,
71    ) -> Result<Self, QueryError> {
72        self.query = map(self.query)?;
73        Ok(self)
74    }
75
76    // Run one read-only session/query projection without mutating the fluent
77    // builder shell so diagnostic and planning surfaces share one handoff
78    // shape from the builder boundary into the session/query layer.
79    fn map_session_query_output<T>(
80        &self,
81        map: impl FnOnce(&DbSession<E::Canister>, &Query<E>) -> Result<T, QueryError>,
82    ) -> Result<T, QueryError> {
83        map(self.session, self.query())
84    }
85
86    // ------------------------------------------------------------------
87    // Intent builders (pure)
88    // ------------------------------------------------------------------
89
90    /// Set the access path to a single typed primary-key value.
91    ///
92    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
93    #[must_use]
94    pub fn by_id(self, id: Id<E>) -> Self {
95        self.map_query(|query| query.by_id(id.key()))
96    }
97
98    /// Set the access path to multiple typed primary-key values.
99    ///
100    /// IDs are public and may come from untrusted input sources.
101    #[must_use]
102    pub fn by_ids<I>(self, ids: I) -> Self
103    where
104        I: IntoIterator<Item = Id<E>>,
105    {
106        self.map_query(|query| query.by_ids(ids.into_iter().map(|id| id.key())))
107    }
108
109    // ------------------------------------------------------------------
110    // Query Refinement
111    // ------------------------------------------------------------------
112
113    /// Add one typed filter expression directly.
114    #[must_use]
115    pub fn filter(self, expr: impl Into<FilterExpr>) -> Self {
116        self.map_query(|query| query.filter(expr))
117    }
118
119    /// Append one typed ORDER BY term.
120    #[must_use]
121    pub fn order_term(self, term: OrderTerm) -> Self {
122        self.map_query(|query| query.order_term(term))
123    }
124
125    /// Append multiple typed ORDER BY terms in declaration order.
126    #[must_use]
127    pub fn order_terms<I>(self, terms: I) -> Self
128    where
129        I: IntoIterator<Item = OrderTerm>,
130    {
131        self.map_query(|query| query.order_terms(terms))
132    }
133
134    /// Add one grouped key field.
135    pub fn group_by(self, field: impl AsRef<str>) -> Result<Self, QueryError> {
136        let field = field.as_ref().to_owned();
137        let schema = self
138            .session
139            .accepted_schema_info_for_entity::<E>()
140            .map_err(QueryError::execute)?;
141
142        self.try_map_query(|query| query.group_by_with_schema(&field, &schema))
143    }
144
145    /// Add one aggregate terminal via composable aggregate expression.
146    #[must_use]
147    pub fn aggregate(self, aggregate: AggregateExpr) -> Self {
148        self.map_query(|query| query.aggregate(aggregate))
149    }
150
151    /// Override grouped hard limits for grouped execution budget enforcement.
152    #[must_use]
153    pub fn grouped_limits(self, max_groups: u64, max_group_bytes: u64) -> Self {
154        self.map_query(|query| query.grouped_limits(max_groups, max_group_bytes))
155    }
156
157    /// Add one grouped HAVING compare clause over one grouped key field.
158    pub fn having_group(
159        self,
160        field: impl AsRef<str>,
161        op: CompareOp,
162        value: InputValue,
163    ) -> Result<Self, QueryError> {
164        let field = field.as_ref().to_owned();
165        let schema = self
166            .session
167            .accepted_schema_info_for_entity::<E>()
168            .map_err(QueryError::execute)?;
169
170        self.try_map_query(|query| query.having_group_with_schema(&field, &schema, op, value))
171    }
172
173    /// Add one grouped HAVING compare clause over one grouped aggregate output.
174    pub fn having_aggregate(
175        self,
176        aggregate_index: usize,
177        op: CompareOp,
178        value: InputValue,
179    ) -> Result<Self, QueryError> {
180        self.try_map_query(|query| query.having_aggregate(aggregate_index, op, value))
181    }
182
183    /// Bound the number of returned rows.
184    ///
185    /// Scalar pagination requires explicit ordering; combine `limit` and/or
186    /// `offset` with `order_term(...)` or planning fails for scalar loads.
187    /// GROUP BY pagination uses canonical grouped-key order by default.
188    #[must_use]
189    pub fn limit(self, limit: u32) -> Self {
190        self.map_query(|query| query.limit(limit))
191    }
192
193    /// Skip a number of rows in the ordered result stream.
194    ///
195    /// Scalar pagination requires explicit ordering; combine `offset` and/or
196    /// `limit` with `order_term(...)` or planning fails for scalar loads.
197    /// GROUP BY pagination uses canonical grouped-key order by default.
198    #[must_use]
199    pub fn offset(self, offset: u32) -> Self {
200        self.map_query(|query| query.offset(offset))
201    }
202
203    /// Attach an opaque cursor token for continuation pagination.
204    ///
205    /// Cursor-mode invariants are checked before planning/execution:
206    /// - explicit `order_term(...)` is required
207    /// - explicit `limit(...)` is required
208    #[must_use]
209    pub fn cursor(mut self, token: impl Into<String>) -> Self {
210        self.cursor_token = Some(token.into());
211        self
212    }
213
214    // ------------------------------------------------------------------
215    // Planning / diagnostics
216    // ------------------------------------------------------------------
217
218    /// Build explain metadata for the current query.
219    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
220        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
221    }
222
223    /// Return the stable plan hash for this query.
224    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
225        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
226    }
227
228    /// Build one trace payload without executing the query.
229    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
230        self.map_session_query_output(DbSession::trace_query)
231    }
232
233    /// Evaluate the current query plan against a read-admission policy without executing rows.
234    ///
235    /// The returned summary captures plan-level admission facts. It is not a
236    /// substitute for enforcing response-byte budgets on the final typed
237    /// response payload.
238    pub fn read_admission(
239        &self,
240        policy: &QueryAdmissionPolicy,
241    ) -> Result<QueryAdmissionSummary, QueryError> {
242        self.map_session_query_output(|session, query| {
243            session.evaluate_query_read_admission_policy(query, policy)
244        })
245    }
246
247    /// Require the current query plan to be admitted without executing rows.
248    ///
249    /// On rejection this returns the same read-admission `QueryError`
250    /// diagnostic family used by policy-bound SQL reads.
251    pub fn ensure_read_admission(
252        &self,
253        policy: &QueryAdmissionPolicy,
254    ) -> Result<QueryAdmissionSummary, QueryError> {
255        self.map_session_query_output(|session, query| {
256            session.ensure_query_read_admission_policy(query, policy)
257        })
258    }
259
260    /// Build the validated logical plan without compiling execution details.
261    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
262        self.ensure_cursor_mode_ready()?;
263        self.map_session_query_output(DbSession::planned_query_with_visible_indexes)
264    }
265
266    /// Build the compiled executable plan for this query.
267    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
268        self.ensure_cursor_mode_ready()?;
269        self.map_session_query_output(DbSession::compile_query_with_visible_indexes)
270    }
271}
272
273impl<E> FluentLoadQuery<'_, E>
274where
275    E: EntityKind + SingletonEntity,
276    E::Key: Default,
277{
278    /// Constrain this query to the singleton entity row.
279    #[must_use]
280    pub fn only(self) -> Self {
281        self.map_query(Query::only)
282    }
283}