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,
12            builder::aggregate::AggregateExpr,
13            explain::ExplainPlan,
14            expr::{FilterExpr, OrderTerm},
15            fluent::load::PartialWindowLoadQuery,
16            intent::{CompiledQuery, PlannedQuery, Query, QueryError},
17            trace::QueryTracePlan,
18        },
19    },
20    traits::{EntityKind, SingletonEntity},
21    types::Id,
22    value::InputValue,
23};
24
25///
26/// FluentLoadQuery
27///
28/// Session-bound load query wrapper.
29/// Owns intent construction and execution routing only.
30/// Result inspection is provided by query API extension traits over `EntityResponse<E>`.
31///
32
33pub struct FluentLoadQuery<'a, E>
34where
35    E: EntityKind,
36{
37    pub(super) session: &'a DbSession<E::Canister>,
38    pub(super) query: Query<E>,
39    pub(super) cursor_token: Option<String>,
40    trusted_read_unchecked: bool,
41}
42
43impl<'a, E> FluentLoadQuery<'a, E>
44where
45    E: EntityKind,
46{
47    pub(in crate::db) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
48        Self {
49            session,
50            query,
51            cursor_token: None,
52            trusted_read_unchecked: false,
53        }
54    }
55
56    // ------------------------------------------------------------------
57    // Intent inspection
58    // ------------------------------------------------------------------
59
60    /// Borrow the current immutable query intent.
61    #[doc(hidden)]
62    #[must_use]
63    pub const fn query(&self) -> &Query<E> {
64        &self.query
65    }
66
67    pub(super) fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
68        self.query = map(self.query);
69        self
70    }
71
72    pub(super) fn try_map_query(
73        mut self,
74        map: impl FnOnce(Query<E>) -> Result<Query<E>, QueryError>,
75    ) -> Result<Self, QueryError> {
76        self.query = map(self.query)?;
77        Ok(self)
78    }
79
80    // Run one read-only session/query projection without mutating the fluent
81    // builder shell so diagnostic and planning surfaces share one handoff
82    // shape from the builder boundary into the session/query layer.
83    fn map_session_query_output<T>(
84        &self,
85        map: impl FnOnce(&DbSession<E::Canister>, &Query<E>) -> Result<T, QueryError>,
86    ) -> Result<T, QueryError> {
87        map(self.session, self.query())
88    }
89
90    // ------------------------------------------------------------------
91    // Intent builders (pure)
92    // ------------------------------------------------------------------
93
94    /// Set the access path to a single typed primary-key value.
95    ///
96    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
97    #[must_use]
98    pub fn by_id(self, id: Id<E>) -> Self {
99        self.map_query(|query| query.by_id(id.key()))
100    }
101
102    /// Set the access path to multiple typed primary-key values.
103    ///
104    /// IDs are public and may come from untrusted input sources.
105    #[must_use]
106    pub fn by_ids<I>(self, ids: I) -> Self
107    where
108        I: IntoIterator<Item = Id<E>>,
109    {
110        self.map_query(|query| query.by_ids(ids.into_iter().map(|id| id.key())))
111    }
112
113    // ------------------------------------------------------------------
114    // Query Refinement
115    // ------------------------------------------------------------------
116
117    /// Add one typed filter expression directly.
118    #[must_use]
119    pub fn filter(self, expr: impl Into<FilterExpr>) -> Self {
120        self.map_query(|query| query.filter(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        let schema = self
142            .session
143            .accepted_schema_info_for_entity::<E>()
144            .map_err(QueryError::execute)?;
145
146        self.try_map_query(|query| query.group_by_with_schema(&field, &schema))
147    }
148
149    /// Add one aggregate terminal via composable aggregate expression.
150    #[must_use]
151    pub fn aggregate(self, aggregate: AggregateExpr) -> Self {
152        self.map_query(|query| query.aggregate(aggregate))
153    }
154
155    /// Override grouped hard limits for grouped execution budget enforcement.
156    #[must_use]
157    pub fn grouped_limits(self, max_groups: u64, max_group_bytes: u64) -> Self {
158        self.map_query(|query| query.grouped_limits(max_groups, max_group_bytes))
159    }
160
161    /// Add one grouped HAVING compare clause over one grouped key field.
162    pub fn having_group(
163        self,
164        field: impl AsRef<str>,
165        op: CompareOp,
166        value: InputValue,
167    ) -> Result<Self, QueryError> {
168        let field = field.as_ref().to_owned();
169        let schema = self
170            .session
171            .accepted_schema_info_for_entity::<E>()
172            .map_err(QueryError::execute)?;
173
174        self.try_map_query(|query| query.having_group_with_schema(&field, &schema, op, value))
175    }
176
177    /// Add one grouped HAVING compare clause over one grouped aggregate output.
178    pub fn having_aggregate(
179        self,
180        aggregate_index: usize,
181        op: CompareOp,
182        value: InputValue,
183    ) -> Result<Self, QueryError> {
184        self.try_map_query(|query| query.having_aggregate(aggregate_index, op, value))
185    }
186
187    /// Return a deliberately partial row window.
188    ///
189    /// This is the fluent load spelling for a low-level result window. Use it
190    /// only when the endpoint contract is a partial row-window result. Semantic
191    /// terminals such as `page(...)`, `collect_complete()`, `exists()`, and
192    /// exact aggregate helpers remain on `FluentLoadQuery`.
193    #[must_use]
194    pub fn partial_window(self, limit: u32) -> PartialWindowLoadQuery<'a, E> {
195        PartialWindowLoadQuery::new(self.map_query(|query| query.limit(limit)))
196    }
197
198    /// Apply a low-level row limit inside core planner/session tests and SQL
199    /// lowering.
200    ///
201    /// Public fluent callers should use `partial_window(...)`, `page(...)`,
202    /// `collect_complete()`, or a semantic terminal instead.
203    #[must_use]
204    #[cfg(test)]
205    pub(in crate::db) fn limit(self, limit: u32) -> Self {
206        self.map_query(|query| query.limit(limit))
207    }
208
209    /// Apply a low-level row offset inside core planner/session tests and SQL
210    /// lowering.
211    ///
212    /// Public fluent callers should use `page(limit)` /
213    /// `next_page(limit, cursor)` rather than offset pagination.
214    #[must_use]
215    #[cfg(test)]
216    pub(in crate::db) fn offset(self, offset: u32) -> Self {
217        self.map_query(|query| query.offset(offset))
218    }
219
220    pub(super) fn with_cursor_token(mut self, token: impl Into<String>) -> Self {
221        self.cursor_token = Some(token.into());
222        self
223    }
224
225    /// Mark this fluent read as trusted and bypass the default bounded read gate.
226    ///
227    /// Use this only for controller/admin maintenance code or internal test
228    /// harnesses that have their own authorization and resource bounds.
229    #[must_use]
230    pub const fn trusted_read_unchecked(mut self) -> Self {
231        self.trusted_read_unchecked = true;
232        self
233    }
234
235    pub(super) const fn trusted_read_unchecked_enabled(&self) -> bool {
236        self.trusted_read_unchecked
237    }
238
239    pub(super) fn ensure_default_read_admission(&self) -> Result<(), QueryError> {
240        if self.trusted_read_unchecked {
241            return Ok(());
242        }
243
244        self.map_session_query_output(|session, query| {
245            session.ensure_query_read_admission_policy(
246                query,
247                &QueryAdmissionPolicy::default_bounded_read(),
248            )
249        })?;
250        Ok(())
251    }
252
253    // ------------------------------------------------------------------
254    // Planning / diagnostics
255    // ------------------------------------------------------------------
256
257    /// Build explain metadata for the current query.
258    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
259        self.map_session_query_output(DbSession::explain_query_with_visible_indexes)
260    }
261
262    /// Return the stable plan hash for this query.
263    pub fn plan_hash_hex(&self) -> Result<String, QueryError> {
264        self.map_session_query_output(DbSession::query_plan_hash_hex_with_visible_indexes)
265    }
266
267    /// Build one trace payload without executing the query.
268    pub fn trace(&self) -> Result<QueryTracePlan, QueryError> {
269        self.map_session_query_output(DbSession::trace_query)
270    }
271
272    /// Build the validated logical plan without compiling execution details.
273    pub fn planned(&self) -> Result<PlannedQuery<E>, QueryError> {
274        self.ensure_cursor_mode_ready()?;
275        self.map_session_query_output(DbSession::planned_query_with_visible_indexes)
276    }
277
278    /// Build the compiled executable plan for this query.
279    pub fn plan(&self) -> Result<CompiledQuery<E>, QueryError> {
280        self.ensure_cursor_mode_ready()?;
281        self.map_session_query_output(DbSession::compile_query_with_visible_indexes)
282    }
283}
284
285impl<E> FluentLoadQuery<'_, E>
286where
287    E: EntityKind + SingletonEntity,
288    E::Key: Default,
289{
290    /// Constrain this query to the singleton entity row.
291    #[must_use]
292    pub fn singleton(self) -> Self {
293        self.map_query(Query::singleton)
294    }
295}