Skip to main content

icydb_core/db/query/fluent/
load.rs

1use crate::{
2    db::{
3        DbSession, PagedLoadExecution, PagedLoadExecutionWithTrace,
4        query::{
5            expr::{FilterExpr, SortExpr},
6            intent::{IntentError, Query, QueryError},
7            plan::{ExecutablePlan, ExplainPlan},
8            policy,
9            predicate::Predicate,
10        },
11        response::Response,
12    },
13    traits::{EntityKind, EntityValue, SingletonEntity},
14    types::Id,
15};
16
17///
18/// FluentLoadQuery
19///
20/// Session-bound load query wrapper.
21/// Owns intent construction and execution routing only.
22/// All result inspection and projection is performed on `Response<E>`.
23///
24
25pub struct FluentLoadQuery<'a, E>
26where
27    E: EntityKind,
28{
29    session: &'a DbSession<E::Canister>,
30    query: Query<E>,
31    cursor_token: Option<String>,
32}
33
34///
35/// PagedLoadQuery
36///
37/// Session-bound cursor pagination wrapper.
38/// This wrapper only exposes cursor continuation and paged execution.
39///
40
41pub struct PagedLoadQuery<'a, E>
42where
43    E: EntityKind,
44{
45    inner: FluentLoadQuery<'a, E>,
46}
47
48impl<'a, E> FluentLoadQuery<'a, E>
49where
50    E: EntityKind,
51{
52    pub(crate) const fn new(session: &'a DbSession<E::Canister>, query: Query<E>) -> Self {
53        Self {
54            session,
55            query,
56            cursor_token: None,
57        }
58    }
59
60    // ------------------------------------------------------------------
61    // Intent inspection
62    // ------------------------------------------------------------------
63
64    #[must_use]
65    pub const fn query(&self) -> &Query<E> {
66        &self.query
67    }
68
69    fn map_query(mut self, map: impl FnOnce(Query<E>) -> Query<E>) -> Self {
70        self.query = map(self.query);
71        self
72    }
73
74    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    // ------------------------------------------------------------------
83    // Intent builders (pure)
84    // ------------------------------------------------------------------
85
86    /// Set the access path to a single typed primary-key value.
87    ///
88    /// `Id<E>` is treated as a plain query input value here. It does not grant access.
89    #[must_use]
90    pub fn by_id(mut self, id: Id<E>) -> Self {
91        self.query = self.query.by_id(id.key());
92        self
93    }
94
95    /// Set the access path to multiple typed primary-key values.
96    ///
97    /// IDs are public and may come from untrusted input sources.
98    #[must_use]
99    pub fn by_ids<I>(mut self, ids: I) -> Self
100    where
101        I: IntoIterator<Item = Id<E>>,
102    {
103        self.query = self.query.by_ids(ids.into_iter().map(|id| id.key()));
104        self
105    }
106
107    // ------------------------------------------------------------------
108    // Query Refinement
109    // ------------------------------------------------------------------
110
111    #[must_use]
112    pub fn filter(self, predicate: Predicate) -> Self {
113        self.map_query(|query| query.filter(predicate))
114    }
115
116    pub fn filter_expr(self, expr: FilterExpr) -> Result<Self, QueryError> {
117        self.try_map_query(|query| query.filter_expr(expr))
118    }
119
120    pub fn sort_expr(self, expr: SortExpr) -> Result<Self, QueryError> {
121        self.try_map_query(|query| query.sort_expr(expr))
122    }
123
124    #[must_use]
125    pub fn order_by(self, field: impl AsRef<str>) -> Self {
126        self.map_query(|query| query.order_by(field))
127    }
128
129    #[must_use]
130    pub fn order_by_desc(self, field: impl AsRef<str>) -> Self {
131        self.map_query(|query| query.order_by_desc(field))
132    }
133
134    /// Bound the number of returned rows.
135    ///
136    /// Pagination is only valid with explicit ordering; combine `limit` and/or
137    /// `offset` with `order_by(...)` or planning fails.
138    #[must_use]
139    pub fn limit(self, limit: u32) -> Self {
140        self.map_query(|query| query.limit(limit))
141    }
142
143    /// Skip a number of rows in the ordered result stream.
144    ///
145    /// Pagination is only valid with explicit ordering; combine `offset` and/or
146    /// `limit` with `order_by(...)` or planning fails.
147    #[must_use]
148    pub fn offset(self, offset: u32) -> Self {
149        self.map_query(|query| query.offset(offset))
150    }
151
152    /// Attach an opaque cursor token for continuation pagination.
153    ///
154    /// Cursor-mode invariants are checked before planning/execution:
155    /// - explicit `order_by(...)` is required
156    /// - explicit `limit(...)` is required
157    #[must_use]
158    pub fn cursor(mut self, token: impl Into<String>) -> Self {
159        self.cursor_token = Some(token.into());
160        self
161    }
162
163    // ------------------------------------------------------------------
164    // Planning / diagnostics
165    // ------------------------------------------------------------------
166
167    pub fn explain(&self) -> Result<ExplainPlan, QueryError> {
168        self.query.explain()
169    }
170
171    pub fn plan(&self) -> Result<ExecutablePlan<E>, QueryError> {
172        if let Some(err) = self.cursor_intent_error() {
173            return Err(QueryError::Intent(err));
174        }
175
176        self.query.plan()
177    }
178
179    // ------------------------------------------------------------------
180    // Execution (single semantic boundary)
181    // ------------------------------------------------------------------
182
183    /// Execute this query using the session's policy settings.
184    pub fn execute(&self) -> Result<Response<E>, QueryError>
185    where
186        E: EntityValue,
187    {
188        self.session.execute_query(self.query())
189    }
190
191    /// Enter typed cursor-pagination mode for this query.
192    ///
193    /// Cursor pagination requires:
194    /// - explicit `order_by(...)`
195    /// - explicit `limit(...)`
196    ///
197    /// Requests are deterministic under canonical ordering, but continuation is
198    /// best-effort and forward-only over live state.
199    /// No snapshot/version is pinned across requests, so concurrent writes may
200    /// shift page boundaries.
201    pub fn page(self) -> Result<PagedLoadQuery<'a, E>, QueryError> {
202        self.ensure_paged_mode_ready()?;
203
204        Ok(PagedLoadQuery { inner: self })
205    }
206
207    /// Execute this query as cursor pagination and return items + next cursor.
208    ///
209    /// The returned cursor token is opaque and must be passed back via `.cursor(...)`.
210    pub fn execute_paged(self) -> Result<(Response<E>, Option<Vec<u8>>), QueryError>
211    where
212        E: EntityValue,
213    {
214        self.page()?.execute()
215    }
216
217    // ------------------------------------------------------------------
218    // Execution terminals — semantic only
219    // ------------------------------------------------------------------
220
221    /// Execute and return whether the result set is empty.
222    pub fn is_empty(&self) -> Result<bool, QueryError>
223    where
224        E: EntityValue,
225    {
226        Ok(!self.exists()?)
227    }
228
229    /// Execute and return whether at least one matching row exists.
230    pub fn exists(&self) -> Result<bool, QueryError>
231    where
232        E: EntityValue,
233    {
234        self.session.execute_load_query_exists(self.query())
235    }
236
237    /// Execute and return the number of matching rows.
238    pub fn count(&self) -> Result<u32, QueryError>
239    where
240        E: EntityValue,
241    {
242        self.session.execute_load_query_count(self.query())
243    }
244
245    /// Execute and return the smallest matching identifier, if any.
246    pub fn min(&self) -> Result<Option<Id<E>>, QueryError>
247    where
248        E: EntityValue,
249    {
250        self.session.execute_load_query_min(self.query())
251    }
252
253    /// Execute and return the largest matching identifier, if any.
254    pub fn max(&self) -> Result<Option<Id<E>>, QueryError>
255    where
256        E: EntityValue,
257    {
258        self.session.execute_load_query_max(self.query())
259    }
260
261    /// Execute and require exactly one matching row.
262    pub fn require_one(&self) -> Result<(), QueryError>
263    where
264        E: EntityValue,
265    {
266        self.execute()?.require_one()?;
267        Ok(())
268    }
269
270    /// Execute and require at least one matching row.
271    pub fn require_some(&self) -> Result<(), QueryError>
272    where
273        E: EntityValue,
274    {
275        self.execute()?.require_some()?;
276        Ok(())
277    }
278}
279
280impl<E> FluentLoadQuery<'_, E>
281where
282    E: EntityKind,
283{
284    fn cursor_intent_error(&self) -> Option<IntentError> {
285        self.cursor_token
286            .as_ref()
287            .and_then(|_| self.paged_intent_error())
288    }
289
290    fn paged_intent_error(&self) -> Option<IntentError> {
291        let spec = self.query.load_spec()?;
292
293        policy::validate_cursor_paging_requirements(self.query.has_explicit_order(), spec)
294            .err()
295            .map(IntentError::from)
296    }
297
298    fn ensure_paged_mode_ready(&self) -> Result<(), QueryError> {
299        if let Some(err) = self.paged_intent_error() {
300            return Err(QueryError::Intent(err));
301        }
302
303        Ok(())
304    }
305}
306
307impl<E> FluentLoadQuery<'_, E>
308where
309    E: EntityKind + SingletonEntity,
310    E::Key: Default,
311{
312    #[must_use]
313    pub fn only(self) -> Self {
314        self.map_query(Query::only)
315    }
316}
317
318impl<E> PagedLoadQuery<'_, E>
319where
320    E: EntityKind,
321{
322    // ------------------------------------------------------------------
323    // Intent inspection
324    // ------------------------------------------------------------------
325
326    #[must_use]
327    pub const fn query(&self) -> &Query<E> {
328        self.inner.query()
329    }
330
331    // ------------------------------------------------------------------
332    // Cursor continuation
333    // ------------------------------------------------------------------
334
335    /// Attach an opaque continuation token for the next page.
336    #[must_use]
337    pub fn cursor(mut self, token: impl Into<String>) -> Self {
338        self.inner = self.inner.cursor(token);
339        self
340    }
341
342    // ------------------------------------------------------------------
343    // Execution
344    // ------------------------------------------------------------------
345
346    /// Execute in cursor-pagination mode and return items + next cursor.
347    ///
348    /// Continuation is best-effort and forward-only over live state:
349    /// deterministic per request under canonical ordering, with no
350    /// snapshot/version pinned across requests.
351    pub fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
352    where
353        E: EntityValue,
354    {
355        self.execute_with_trace()
356            .map(|(items, next_cursor, _)| (items, next_cursor))
357    }
358
359    /// Execute in cursor-pagination mode and return items, next cursor,
360    /// and optional execution trace details when session debug mode is enabled.
361    ///
362    /// Trace collection is opt-in via `DbSession::debug()` and does not
363    /// change query planning or result semantics.
364    pub fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
365    where
366        E: EntityValue,
367    {
368        self.inner.ensure_paged_mode_ready()?;
369
370        self.inner.session.execute_load_query_paged_with_trace(
371            self.inner.query(),
372            self.inner.cursor_token.as_deref(),
373        )
374    }
375}