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},
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 }.execute_trusted()
121 }
122}
123
124impl<E> PagedLoadQuery<'_, E>
125where
126 E: PersistedRow,
127{
128 // ------------------------------------------------------------------
129 // Intent inspection
130 // ------------------------------------------------------------------
131
132 #[must_use]
133 pub const fn query(&self) -> &Query<E> {
134 self.inner.query()
135 }
136
137 // ------------------------------------------------------------------
138 // Execution
139 // ------------------------------------------------------------------
140
141 /// Execute in cursor-pagination mode and return items + next cursor.
142 ///
143 /// Continuation is best-effort and forward-only over live state:
144 /// deterministic per request under canonical ordering, with no
145 /// snapshot/version pinned across requests.
146 pub fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
147 where
148 E: PersistedRow + EntityValue,
149 {
150 self.execute_with_trace()
151 .map(PagedLoadExecutionWithTrace::into_execution)
152 }
153
154 /// Execute in cursor-pagination mode without the default bounded read-admission gate.
155 ///
156 /// This is for trusted maintenance/admin code that has its own caller
157 /// authorization and resource policy. Application-facing reads should use
158 /// `execute`.
159 pub fn execute_trusted(self) -> Result<PagedLoadExecution<E>, QueryError>
160 where
161 E: PersistedRow + EntityValue,
162 {
163 self.execute_with_trace_trusted()
164 .map(PagedLoadExecutionWithTrace::into_execution)
165 }
166
167 /// Execute in cursor-pagination mode and return items, next cursor,
168 /// and optional execution trace details when session debug mode is enabled.
169 ///
170 /// Trace collection is opt-in via `DbSession::debug()` and does not
171 /// change query planning or result semantics.
172 pub fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
173 where
174 E: PersistedRow + EntityValue,
175 {
176 self.inner.ensure_default_read_admission()?;
177 self.execute_with_trace_trusted()
178 }
179
180 /// Execute in cursor-pagination mode with trace details and without the
181 /// default bounded read-admission gate.
182 ///
183 /// This is for trusted maintenance/admin code that has its own caller
184 /// authorization and resource policy. Application-facing reads should use
185 /// `execute_with_trace`.
186 pub fn execute_with_trace_trusted(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
187 where
188 E: PersistedRow + EntityValue,
189 {
190 // `PagedLoadQuery` can only be constructed through `FluentLoadQuery::page`,
191 // so the paged-mode validation already happened before this wrapper existed.
192 self.inner.session.execute_load_query_paged_with_trace(
193 self.inner.query(),
194 self.inner.cursor_token.as_deref(),
195 )
196 }
197}