Skip to main content

icydb_core/db/query/fluent/load/
pagination.rs

1//! Module: query::fluent::load::pagination
2//! Responsibility: fluent paged-query wrapper APIs and cursor continuation terminals.
3//! Does not own: planner semantic validation or runtime execution internals.
4//! Boundary: exposes paged execution surfaces over fluent load query contracts.
5
6use crate::{
7    db::{
8        PagedLoadExecution, PagedLoadExecutionWithTrace, PersistedRow,
9        query::fluent::load::FluentLoadQuery,
10        query::{
11            intent::{IntentError, QueryError},
12            read_intent::{ADMIN_BATCH_ROWS, AdminBatchRequest, PageRequest, ReadIntentKind},
13        },
14    },
15    traits::{EntityKind, EntityValue},
16};
17
18struct PagedLoadQuery<'a, E>
19where
20    E: EntityKind,
21{
22    inner: FluentLoadQuery<'a, E>,
23}
24
25impl<'a, E> FluentLoadQuery<'a, E>
26where
27    E: PersistedRow,
28{
29    /// Execute the first typed cursor page with the requested page size.
30    ///
31    /// Cursor pagination requires:
32    /// - explicit `order_term(...)`
33    /// - no prior row-window cap
34    ///
35    /// Results are deterministic under canonical ordering, but continuation is
36    /// best-effort and forward-only over live state.
37    /// No snapshot/version is pinned across requests, so concurrent writes may
38    /// shift page boundaries.
39    pub fn page(self, limit: u32) -> Result<PagedLoadExecution<E>, QueryError>
40    where
41        E: EntityValue,
42    {
43        self.page_request(PageRequest::first(limit))?.execute()
44    }
45
46    /// Execute the next typed cursor page from a previous continuation cursor.
47    ///
48    /// This is the continuation counterpart to `page(limit)`. The cursor is an
49    /// opaque token returned by the previous page response.
50    pub fn next_page(
51        self,
52        limit: u32,
53        cursor: impl Into<String>,
54    ) -> Result<PagedLoadExecution<E>, QueryError>
55    where
56        E: EntityValue,
57    {
58        self.page_request(PageRequest::next(limit, cursor))?
59            .execute()
60    }
61
62    fn page_request(self, request: PageRequest) -> Result<PagedLoadQuery<'a, E>, QueryError> {
63        self.ensure_semantic_terminal_owns_limit(IntentError::raw_limit_before_page_terminal())?;
64        self.ensure_page_request_owns_cursor()?;
65
66        let limit = request.effective_limit();
67        let cursor = request.into_cursor();
68        let mut inner = self.map_query(|query| query.with_load_limit(limit));
69        if let Some(cursor) = cursor {
70            inner = inner.with_cursor_token(cursor);
71        }
72
73        inner.ensure_paged_mode_ready()?;
74
75        Ok(PagedLoadQuery { inner })
76    }
77
78    /// Execute a trusted/admin cursor batch with an engine-owned batch size.
79    ///
80    /// This terminal is intentionally unavailable on the normal public read
81    /// lane. Callers must opt into `trusted_read_unchecked()` before invoking
82    /// it, and a prior row-window cap is rejected because the batch size is
83    /// owned by IcyDB.
84    pub fn admin_batch(
85        self,
86        request: AdminBatchRequest,
87    ) -> Result<PagedLoadExecution<E>, QueryError>
88    where
89        E: PersistedRow + EntityValue,
90    {
91        self.ensure_semantic_terminal_owns_limit(
92            IntentError::raw_limit_before_admin_batch_terminal(),
93        )?;
94        self.ensure_page_request_owns_cursor()?;
95
96        if !self.trusted_read_unchecked_enabled() {
97            return Err(QueryError::intent(
98                IntentError::admin_batch_requires_trusted_read(),
99            ));
100        }
101
102        let cursor = request.into_cursor();
103        let mut inner = self.map_query(|query| query.with_load_limit(ADMIN_BATCH_ROWS));
104        if let Some(cursor) = cursor {
105            inner = inner.with_cursor_token(cursor);
106        }
107
108        inner.ensure_paged_mode_ready()?;
109
110        PagedLoadQuery { inner }
111            .execute()
112            .map(|execution| execution.with_read_intent(ReadIntentKind::TrustedAdminBatch))
113    }
114}
115
116impl<E> PagedLoadQuery<'_, E>
117where
118    E: PersistedRow,
119{
120    /// Execute in cursor-pagination mode and return items + next cursor.
121    ///
122    /// Continuation is best-effort and forward-only over live state:
123    /// deterministic per request under canonical ordering, with no
124    /// snapshot/version pinned across requests.
125    fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
126    where
127        E: PersistedRow + EntityValue,
128    {
129        self.execute_with_trace()
130            .map(PagedLoadExecutionWithTrace::into_execution)
131    }
132
133    /// Execute in cursor-pagination mode and return items, next cursor,
134    /// and optional execution trace details when session debug mode is enabled.
135    ///
136    /// Trace collection is opt-in via `DbSession::debug()` and does not
137    /// change query planning or result semantics.
138    fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
139    where
140        E: PersistedRow + EntityValue,
141    {
142        self.inner.ensure_default_read_admission()?;
143        self.execute_with_trace_unchecked()
144    }
145
146    fn execute_with_trace_unchecked(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
147    where
148        E: PersistedRow + EntityValue,
149    {
150        // `PagedLoadQuery` is only constructed by page terminals in this module,
151        // so paged-mode validation already happened before this wrapper existed.
152        self.inner
153            .session
154            .execute_load_query_paged_with_trace(
155                self.inner.query(),
156                self.inner.cursor_token.as_deref(),
157            )
158            .map(|execution| execution.with_read_intent(ReadIntentKind::PublicPage))
159    }
160}