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