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