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, Query, QueryError},
12            read_intent::{ADMIN_BATCH_ROWS, AdminBatchRequest, PageRequest, ReadIntentKind},
13        },
14    },
15    traits::{EntityKind, EntityValue},
16};
17
18///
19/// PagedLoadQuery
20///
21/// Session-bound cursor pagination wrapper.
22/// This wrapper only exposes cursor continuation and paged execution.
23///
24
25pub struct PagedLoadQuery<'a, E>
26where
27    E: EntityKind,
28{
29    inner: FluentLoadQuery<'a, E>,
30}
31
32impl<'a, E> FluentLoadQuery<'a, E>
33where
34    E: PersistedRow,
35{
36    /// Enter typed cursor-pagination mode for this request-owned page.
37    ///
38    /// Cursor pagination requires:
39    /// - explicit `order_term(...)`
40    /// - no prior raw `limit(...)`
41    ///
42    /// Requests are deterministic under canonical ordering, but continuation is
43    /// best-effort and forward-only over live state.
44    /// No snapshot/version is pinned across requests, so concurrent writes may
45    /// shift page boundaries.
46    pub fn page(self, request: PageRequest) -> Result<PagedLoadQuery<'a, E>, QueryError> {
47        self.ensure_semantic_terminal_owns_limit(IntentError::raw_limit_before_page_terminal())?;
48        self.ensure_page_request_owns_cursor()?;
49
50        let limit = request.effective_limit();
51        let cursor = request.into_cursor();
52        let mut inner = self.map_query(|query| query.with_load_limit(limit));
53        if let Some(cursor) = cursor {
54            inner = inner.with_cursor_token(cursor);
55        }
56
57        inner.ensure_paged_mode_ready()?;
58
59        Ok(PagedLoadQuery { inner })
60    }
61
62    /// Execute this query as cursor pagination and return items + next cursor.
63    ///
64    /// The returned cursor token is opaque and must be passed back through
65    /// `PageRequest`.
66    pub fn execute_paged(self, request: PageRequest) -> Result<PagedLoadExecution<E>, QueryError>
67    where
68        E: PersistedRow + EntityValue,
69    {
70        self.page(request)?.execute()
71    }
72
73    /// Execute cursor pagination without the default bounded read-admission gate.
74    ///
75    /// This is for trusted maintenance/admin code that has its own caller
76    /// authorization and resource policy. Application-facing reads should use
77    /// `execute_paged`.
78    pub fn execute_paged_trusted(
79        self,
80        request: PageRequest,
81    ) -> Result<PagedLoadExecution<E>, QueryError>
82    where
83        E: PersistedRow + EntityValue,
84    {
85        self.page(request)?.execute_trusted()
86    }
87
88    /// Execute a trusted/admin cursor batch with an engine-owned batch size.
89    ///
90    /// This terminal is intentionally unavailable on the normal public read
91    /// lane. Callers must opt into `trusted_read_unchecked()` before invoking
92    /// it, and a prior raw `limit(...)` is rejected because the batch size is
93    /// owned by IcyDB.
94    pub fn admin_batch(
95        self,
96        request: AdminBatchRequest,
97    ) -> Result<PagedLoadExecution<E>, QueryError>
98    where
99        E: PersistedRow + EntityValue,
100    {
101        self.ensure_semantic_terminal_owns_limit(
102            IntentError::raw_limit_before_admin_batch_terminal(),
103        )?;
104        self.ensure_page_request_owns_cursor()?;
105
106        if !self.trusted_read_unchecked_enabled() {
107            return Err(QueryError::intent(
108                IntentError::admin_batch_requires_trusted_read(),
109            ));
110        }
111
112        let cursor = request.into_cursor();
113        let mut inner = self.map_query(|query| query.with_load_limit(ADMIN_BATCH_ROWS));
114        if let Some(cursor) = cursor {
115            inner = inner.with_cursor_token(cursor);
116        }
117
118        inner.ensure_paged_mode_ready()?;
119
120        PagedLoadQuery { inner }
121            .execute_trusted()
122            .map(|execution| execution.with_read_intent(ReadIntentKind::TrustedAdminBatch))
123    }
124}
125
126impl<E> PagedLoadQuery<'_, E>
127where
128    E: PersistedRow,
129{
130    // ------------------------------------------------------------------
131    // Intent inspection
132    // ------------------------------------------------------------------
133
134    #[must_use]
135    pub const fn query(&self) -> &Query<E> {
136        self.inner.query()
137    }
138
139    // ------------------------------------------------------------------
140    // Execution
141    // ------------------------------------------------------------------
142
143    /// Execute in cursor-pagination mode and return items + next cursor.
144    ///
145    /// Continuation is best-effort and forward-only over live state:
146    /// deterministic per request under canonical ordering, with no
147    /// snapshot/version pinned across requests.
148    pub fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
149    where
150        E: PersistedRow + EntityValue,
151    {
152        self.execute_with_trace()
153            .map(PagedLoadExecutionWithTrace::into_execution)
154    }
155
156    /// Execute in cursor-pagination mode without the default bounded read-admission gate.
157    ///
158    /// This is for trusted maintenance/admin code that has its own caller
159    /// authorization and resource policy. Application-facing reads should use
160    /// `execute`.
161    pub fn execute_trusted(self) -> Result<PagedLoadExecution<E>, QueryError>
162    where
163        E: PersistedRow + EntityValue,
164    {
165        self.execute_with_trace_trusted()
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    pub 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_trusted()
180    }
181
182    /// Execute in cursor-pagination mode with trace details and without the
183    /// default bounded read-admission gate.
184    ///
185    /// This is for trusted maintenance/admin code that has its own caller
186    /// authorization and resource policy. Application-facing reads should use
187    /// `execute_with_trace`.
188    pub fn execute_with_trace_trusted(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
189    where
190        E: PersistedRow + EntityValue,
191    {
192        // `PagedLoadQuery` can only be constructed through `FluentLoadQuery::page`,
193        // so the paged-mode validation already happened before this wrapper existed.
194        self.inner
195            .session
196            .execute_load_query_paged_with_trace(
197                self.inner.query(),
198                self.inner.cursor_token.as_deref(),
199            )
200            .map(|execution| execution.with_read_intent(ReadIntentKind::PublicPage))
201    }
202}