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