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