@@ -9,6 +9,7 @@ use serde_json::Value as JsonValue;
99use sqlx_sqlite_conn_mgr:: AttachedSpec ;
1010
1111use crate :: Error ;
12+ use crate :: pagination:: { KeysetColumn , KeysetPage , build_paginated_query} ;
1213use crate :: wrapper:: { DatabaseWrapper , WriteQueryResult , bind_value} ;
1314
1415/// Builder for SELECT queries returning multiple rows
@@ -153,6 +154,188 @@ impl IntoFuture for FetchOneBuilder {
153154 }
154155}
155156
157+ /// Internal cursor position for forward vs backward pagination.
158+ enum CursorPosition {
159+ Forward ( Vec < JsonValue > ) ,
160+ Backward ( Vec < JsonValue > ) ,
161+ }
162+
163+ /// Builder for paginated SELECT queries using keyset (cursor-based) pagination
164+ pub struct FetchPageBuilder {
165+ db : Arc < sqlx_sqlite_conn_mgr:: SqliteDatabase > ,
166+ query : String ,
167+ values : Vec < JsonValue > ,
168+ keyset : Vec < KeysetColumn > ,
169+ page_size : usize ,
170+ cursor : Option < CursorPosition > ,
171+ attached : Vec < AttachedSpec > ,
172+ }
173+
174+ impl FetchPageBuilder {
175+ pub ( crate ) fn new (
176+ db : Arc < sqlx_sqlite_conn_mgr:: SqliteDatabase > ,
177+ query : String ,
178+ values : Vec < JsonValue > ,
179+ keyset : Vec < KeysetColumn > ,
180+ page_size : usize ,
181+ ) -> Self {
182+ Self {
183+ db,
184+ query,
185+ values,
186+ keyset,
187+ page_size,
188+ cursor : None ,
189+ attached : Vec :: new ( ) ,
190+ }
191+ }
192+
193+ /// Set the cursor for fetching the next page (forward pagination).
194+ ///
195+ /// Pass the `next_cursor` from a previous `KeysetPage` to fetch the page
196+ /// that follows it in the original sort order.
197+ pub fn after ( mut self , cursor : Vec < JsonValue > ) -> Self {
198+ self . cursor = Some ( CursorPosition :: Forward ( cursor) ) ;
199+ self
200+ }
201+
202+ /// Set the cursor for fetching the previous page (backward pagination).
203+ ///
204+ /// Pass a cursor to fetch the page that precedes it in the original sort
205+ /// order. Rows are returned in the original sort order (not reversed).
206+ pub fn before ( mut self , cursor : Vec < JsonValue > ) -> Self {
207+ self . cursor = Some ( CursorPosition :: Backward ( cursor) ) ;
208+ self
209+ }
210+
211+ /// Attach additional databases for this query
212+ pub fn attach ( mut self , attached : Vec < AttachedSpec > ) -> Self {
213+ self . attached = attached;
214+ self
215+ }
216+
217+ /// Execute the paginated query and return a page of results
218+ pub async fn execute ( self ) -> Result < KeysetPage , Error > {
219+ // Validate inputs
220+ if self . keyset . is_empty ( ) {
221+ return Err ( Error :: EmptyKeysetColumns ) ;
222+ }
223+ if self . page_size == 0 {
224+ return Err ( Error :: InvalidPageSize ) ;
225+ }
226+
227+ // Extract cursor values and direction
228+ let ( cursor_values, backward) = match self . cursor {
229+ Some ( CursorPosition :: Forward ( vals) ) => ( Some ( vals) , false ) ,
230+ Some ( CursorPosition :: Backward ( vals) ) => ( Some ( vals) , true ) ,
231+ None => ( None , false ) ,
232+ } ;
233+
234+ if let Some ( ref vals) = cursor_values
235+ && vals. len ( ) != self . keyset . len ( )
236+ {
237+ return Err ( Error :: CursorLengthMismatch {
238+ cursor_len : vals. len ( ) ,
239+ keyset_len : self . keyset . len ( ) ,
240+ } ) ;
241+ }
242+
243+ // Build paginated SQL — pass the user's bind count so cursor
244+ // placeholders are numbered $N+1, $N+2, … and never collide with
245+ // the user's $1, $2, … (or positional ?) parameters.
246+ let ( sql, cursor_bind_values) = build_paginated_query (
247+ & self . query ,
248+ & self . keyset ,
249+ cursor_values. as_deref ( ) ,
250+ self . page_size ,
251+ backward,
252+ self . values . len ( ) ,
253+ ) ?;
254+
255+ // Combine user values + cursor bind values
256+ let mut all_values = self . values ;
257+ all_values. extend ( cursor_bind_values) ;
258+
259+ // Execute query
260+ let rows = if self . attached . is_empty ( ) {
261+ let pool = self . db . read_pool ( ) ?;
262+ let mut q = sqlx:: query ( & sql) ;
263+ for value in all_values {
264+ q = bind_value ( q, value) ;
265+ }
266+ q. fetch_all ( pool) . await ?
267+ } else {
268+ let mut conn =
269+ sqlx_sqlite_conn_mgr:: acquire_reader_with_attached ( & self . db , self . attached ) . await ?;
270+
271+ let mut q = sqlx:: query ( & sql) ;
272+ for value in all_values {
273+ q = bind_value ( q, value) ;
274+ }
275+ let rows = sqlx:: Executor :: fetch_all ( & mut * conn, q) . await ?;
276+
277+ // Explicit cleanup
278+ conn. detach_all ( ) . await ?;
279+ rows
280+ } ;
281+
282+ // Decode rows
283+ let mut decoded = decode_rows ( rows) ?;
284+
285+ // Determine has_more by checking if we got more rows than page_size
286+ let has_more = decoded. len ( ) > self . page_size ;
287+ if has_more {
288+ decoded. truncate ( self . page_size ) ;
289+ }
290+
291+ // Reverse rows when paginating backward to restore original sort order
292+ if backward {
293+ decoded. reverse ( ) ;
294+ }
295+
296+ // Extract continuation cursor: first row if backward, last row if forward
297+ let cursor_row = if backward {
298+ decoded. first ( )
299+ } else {
300+ decoded. last ( )
301+ } ;
302+
303+ let next_cursor = if has_more {
304+ if let Some ( row) = cursor_row {
305+ let mut cursor_vals = Vec :: with_capacity ( self . keyset . len ( ) ) ;
306+ for col in & self . keyset {
307+ let value = row
308+ . get ( & col. name )
309+ . ok_or_else ( || Error :: CursorColumnNotFound {
310+ column : col. name . clone ( ) ,
311+ } ) ?;
312+ cursor_vals. push ( value. clone ( ) ) ;
313+ }
314+ Some ( cursor_vals)
315+ } else {
316+ None
317+ }
318+ } else {
319+ None
320+ } ;
321+
322+ Ok ( KeysetPage {
323+ rows : decoded,
324+ next_cursor,
325+ has_more,
326+ } )
327+ }
328+ }
329+
330+ impl IntoFuture for FetchPageBuilder {
331+ type Output = Result < KeysetPage , Error > ;
332+ type IntoFuture = Pin < Box < dyn Future < Output = Self :: Output > + Send > > ;
333+
334+ fn into_future ( self ) -> Self :: IntoFuture {
335+ Box :: pin ( self . execute ( ) )
336+ }
337+ }
338+
156339/// Builder for write queries (INSERT/UPDATE/DELETE)
157340pub struct ExecuteBuilder {
158341 db : DatabaseWrapper ,
0 commit comments