Skip to main content

icydb_core/db/query/session/
load.rs

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