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    read_intent: ReadIntentKind,
24}
25
26#[derive(Clone, Copy)]
27enum PagedTerminal {
28    PublicPage,
29    TrustedAdminBatch,
30}
31
32impl PagedTerminal {
33    const fn raw_limit_error(self) -> IntentError {
34        match self {
35            Self::PublicPage => IntentError::raw_limit_before_page_terminal(),
36            Self::TrustedAdminBatch => IntentError::raw_limit_before_admin_batch_terminal(),
37        }
38    }
39
40    const fn read_intent(self) -> ReadIntentKind {
41        match self {
42            Self::PublicPage => ReadIntentKind::PublicPage,
43            Self::TrustedAdminBatch => ReadIntentKind::TrustedAdminBatch,
44        }
45    }
46
47    const fn validate<E>(self, query: &FluentLoadQuery<'_, E>) -> Result<(), QueryError>
48    where
49        E: EntityKind,
50    {
51        match self {
52            Self::PublicPage => Ok(()),
53            Self::TrustedAdminBatch => {
54                if !query.trusted_read_unchecked_enabled() {
55                    return Err(QueryError::intent(
56                        IntentError::admin_batch_requires_trusted_read(),
57                    ));
58                }
59
60                Ok(())
61            }
62        }
63    }
64}
65
66impl<'a, E> FluentLoadQuery<'a, E>
67where
68    E: PersistedRow,
69{
70    /// Execute the first typed cursor page with the requested page size.
71    ///
72    /// Cursor pagination requires:
73    /// - explicit `order_term(...)`
74    /// - no prior row-window cap
75    ///
76    /// Results are deterministic under canonical ordering, but continuation is
77    /// best-effort and forward-only over live state.
78    /// No snapshot/version is pinned across requests, so concurrent writes may
79    /// shift page boundaries.
80    pub fn page(self, limit: u32) -> Result<PagedLoadExecution<E>, QueryError>
81    where
82        E: EntityValue,
83    {
84        self.page_request(PageRequest::first(limit))?.execute()
85    }
86
87    /// Execute the next typed cursor page from a previous continuation cursor.
88    ///
89    /// This is the continuation counterpart to `page(limit)`. The cursor is an
90    /// opaque token returned by the previous page response.
91    pub fn next_page(
92        self,
93        limit: u32,
94        cursor: impl Into<String>,
95    ) -> Result<PagedLoadExecution<E>, QueryError>
96    where
97        E: EntityValue,
98    {
99        self.page_request(PageRequest::next(limit, cursor))?
100            .execute()
101    }
102
103    fn page_request(self, request: PageRequest) -> Result<PagedLoadQuery<'a, E>, QueryError> {
104        let limit = request.effective_limit();
105        let cursor = request.into_cursor();
106
107        self.into_paged_query(limit, cursor, PagedTerminal::PublicPage)
108    }
109
110    /// Execute a trusted/admin cursor batch with an engine-owned batch size.
111    ///
112    /// This terminal is intentionally unavailable on the normal public read
113    /// lane. Callers must opt into `trusted_read_unchecked()` before invoking
114    /// it, and a prior row-window cap is rejected because the batch size is
115    /// owned by IcyDB.
116    pub fn admin_batch(
117        self,
118        request: AdminBatchRequest,
119    ) -> Result<PagedLoadExecution<E>, QueryError>
120    where
121        E: PersistedRow + EntityValue,
122    {
123        let cursor = request.into_cursor();
124        self.into_paged_query(ADMIN_BATCH_ROWS, cursor, PagedTerminal::TrustedAdminBatch)?
125            .execute()
126    }
127
128    fn into_paged_query(
129        self,
130        limit: u32,
131        cursor: Option<String>,
132        terminal: PagedTerminal,
133    ) -> Result<PagedLoadQuery<'a, E>, QueryError> {
134        self.ensure_semantic_terminal_owns_limit(terminal.raw_limit_error())?;
135        self.ensure_page_request_owns_cursor()?;
136        terminal.validate(&self)?;
137
138        let mut inner = self.map_query(|query| query.with_load_limit(limit));
139        if let Some(cursor) = cursor {
140            inner = inner.with_cursor_token(cursor);
141        }
142
143        inner.ensure_paged_mode_ready()?;
144
145        Ok(PagedLoadQuery {
146            inner,
147            read_intent: terminal.read_intent(),
148        })
149    }
150}
151
152impl<E> PagedLoadQuery<'_, E>
153where
154    E: PersistedRow,
155{
156    /// Execute in cursor-pagination mode and return items + next cursor.
157    ///
158    /// Continuation is best-effort and forward-only over live state:
159    /// deterministic per request under canonical ordering, with no
160    /// snapshot/version pinned across requests.
161    fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
162    where
163        E: PersistedRow + EntityValue,
164    {
165        self.execute_with_trace()
166            .map(PagedLoadExecutionWithTrace::into_execution)
167    }
168
169    /// Execute in cursor-pagination mode and return items, next cursor,
170    /// and optional execution trace details when session debug mode is enabled.
171    ///
172    /// Trace collection is opt-in via `DbSession::debug()` and does not
173    /// change query planning or result semantics.
174    fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
175    where
176        E: PersistedRow + EntityValue,
177    {
178        self.inner.ensure_default_read_admission()?;
179        self.execute_with_trace_unchecked()
180    }
181
182    fn execute_with_trace_unchecked(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
183    where
184        E: PersistedRow + EntityValue,
185    {
186        // `PagedLoadQuery` is only constructed by page terminals in this module,
187        // so paged-mode validation already happened before this wrapper existed.
188        let read_intent = self.read_intent;
189        self.inner
190            .session
191            .execute_load_query_paged_with_trace(
192                self.inner.query(),
193                self.inner.cursor_token.as_deref(),
194            )
195            .map(|execution| execution.with_read_intent(read_intent))
196    }
197}