Skip to main content

icydb_core/db/query/session/
load.rs

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