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::intent::{Query, QueryError},
11 },
12 traits::{EntityKind, EntityValue},
13};
14
15///
16/// PagedLoadQuery
17///
18/// Session-bound cursor pagination wrapper.
19/// This wrapper only exposes cursor continuation and paged execution.
20///
21
22pub struct PagedLoadQuery<'a, E>
23where
24 E: EntityKind,
25{
26 inner: FluentLoadQuery<'a, E>,
27}
28
29impl<'a, E> FluentLoadQuery<'a, E>
30where
31 E: PersistedRow,
32{
33 /// Enter typed cursor-pagination mode for this query.
34 ///
35 /// Cursor pagination requires:
36 /// - explicit `order_by(...)`
37 /// - explicit `limit(...)`
38 ///
39 /// Requests are deterministic under canonical ordering, but continuation is
40 /// best-effort and forward-only over live state.
41 /// No snapshot/version is pinned across requests, so concurrent writes may
42 /// shift page boundaries.
43 pub fn page(self) -> Result<PagedLoadQuery<'a, E>, QueryError> {
44 self.ensure_paged_mode_ready()?;
45
46 Ok(PagedLoadQuery::from_inner(self))
47 }
48
49 /// Execute this query as cursor pagination and return items + next cursor.
50 ///
51 /// The returned cursor token is opaque and must be passed back via `.cursor(...)`.
52 pub fn execute_paged(self) -> Result<PagedLoadExecution<E>, QueryError>
53 where
54 E: PersistedRow + EntityValue,
55 {
56 self.page()?.execute()
57 }
58}
59
60impl<'a, E> PagedLoadQuery<'a, E>
61where
62 E: PersistedRow,
63{
64 // Rebind one already-validated fluent load query to the paged wrapper.
65 const fn from_inner(inner: FluentLoadQuery<'a, E>) -> Self {
66 Self { inner }
67 }
68
69 // Apply one immutable fluent-load transformation while preserving the
70 // paged-query wrapper that already owns the paged-mode invariant.
71 fn map_inner(
72 mut self,
73 map: impl FnOnce(FluentLoadQuery<'a, E>) -> FluentLoadQuery<'a, E>,
74 ) -> Self {
75 self.inner = map(self.inner);
76 self
77 }
78
79 // ------------------------------------------------------------------
80 // Intent inspection
81 // ------------------------------------------------------------------
82
83 #[must_use]
84 pub const fn query(&self) -> &Query<E> {
85 self.inner.query()
86 }
87
88 // ------------------------------------------------------------------
89 // Cursor continuation
90 // ------------------------------------------------------------------
91
92 /// Attach an opaque continuation token for the next page.
93 #[must_use]
94 pub fn cursor(self, token: impl Into<String>) -> Self {
95 self.map_inner(|query| query.cursor(token))
96 }
97
98 // ------------------------------------------------------------------
99 // Execution
100 // ------------------------------------------------------------------
101
102 /// Execute in cursor-pagination mode and return items + next cursor.
103 ///
104 /// Continuation is best-effort and forward-only over live state:
105 /// deterministic per request under canonical ordering, with no
106 /// snapshot/version pinned across requests.
107 pub fn execute(self) -> Result<PagedLoadExecution<E>, QueryError>
108 where
109 E: PersistedRow + EntityValue,
110 {
111 self.execute_with_trace()
112 .map(PagedLoadExecutionWithTrace::into_execution)
113 }
114
115 /// Execute in cursor-pagination mode and return items, next cursor,
116 /// and optional execution trace details when session debug mode is enabled.
117 ///
118 /// Trace collection is opt-in via `DbSession::debug()` and does not
119 /// change query planning or result semantics.
120 pub fn execute_with_trace(self) -> Result<PagedLoadExecutionWithTrace<E>, QueryError>
121 where
122 E: PersistedRow + EntityValue,
123 {
124 // `PagedLoadQuery` can only be constructed through `FluentLoadQuery::page`,
125 // so the paged-mode validation already happened before this wrapper existed.
126 self.inner.session.execute_load_query_paged_with_trace(
127 self.inner.query(),
128 self.inner.cursor_token.as_deref(),
129 )
130 }
131}