@@ -61,6 +61,7 @@ without Tauri:
6161| -------------------- | --------------- | ---------------- | ------------------- |
6262| SELECT (multiple) | ` fetchAll() ` | Read pool | Multiple concurrent |
6363| SELECT (single) | ` fetchOne() ` | Read pool | Multiple concurrent |
64+ | SELECT (paginated) | ` fetchPage() ` | Read pool | Multiple concurrent |
6465| INSERT/UPDATE/DELETE | ` execute() ` | Write connection | Serialized |
6566| DDL (CREATE, etc.) | ` execute() ` | Write connection | Serialized |
6667
@@ -268,6 +269,64 @@ if (user) {
268269}
269270```
270271
272+ ### Pagination
273+
274+ When working with large result sets, loading all rows at once can cause
275+ performance degradation and excessive memory usage on both the Rust and
276+ TypeScript sides. The plugin provides built-in pagination to fetch data in
277+ fixed-size pages, keeping memory usage bounded and queries fast regardless
278+ of total row count.
279+
280+ #### Why Keyset Pagination
281+
282+ The plugin uses keyset (cursor-based) pagination rather than traditional
283+ OFFSET-based pagination. With OFFSET, the database must scan and discard
284+ all skipped rows on every page request, making deeper pages progressively
285+ slower. Keyset pagination uses indexed column values from the last row of
286+ the current page to seek directly to the next page, keeping query time
287+ constant no matter how far you paginate.
288+
289+ ``` typescript
290+ import type { KeysetColumn } from ' @silvermine/tauri-plugin-sqlite' ;
291+
292+ type Post = { id: number ; title: string ; category: string ; score: number };
293+
294+ const keyset: KeysetColumn [] = [
295+ { name: ' category' , direction: ' asc' },
296+ { name: ' score' , direction: ' desc' },
297+ { name: ' id' , direction: ' asc' },
298+ ];
299+
300+ // First page
301+ const page = await db .fetchPage <Post >(
302+ ' SELECT id, title, category, score FROM posts' ,
303+ [],
304+ keyset ,
305+ 25 ,
306+ );
307+
308+ // Next page (forward) — pass the cursor from the previous page
309+ if (page .nextCursor ) {
310+ const nextPage = await db .fetchPage <Post >(
311+ ' SELECT id, title, category, score FROM posts' ,
312+ [],
313+ keyset ,
314+ 25 ,
315+ ).after (page .nextCursor );
316+
317+ // Previous page (backward) — rows are returned in original sort order
318+ const prevPage = await db .fetchPage <Post >(
319+ ' SELECT id, title, category, score FROM posts' ,
320+ [],
321+ keyset ,
322+ 25 ,
323+ ).before (page .nextCursor );
324+ }
325+ ```
326+
327+ The base query must not contain ` ORDER BY ` or ` LIMIT ` clauses — the builder
328+ appends these automatically based on the keyset definition.
329+
271330### Transactions
272331
273332For most cases, use ` executeTransaction() ` to run multiple statements atomically:
@@ -338,8 +397,8 @@ Each attached database gets a schema name that acts as a namespace for its
338397tables.
339398
340399** Builder Pattern:** All query methods (` execute ` , ` executeTransaction ` ,
341- ` fetchAll ` , ` fetchOne ` ) return builders that support ` .attach() ` for
342- cross-database operations.
400+ ` fetchAll ` , ` fetchOne ` , ` fetchPage ` ) return builders that support ` .attach() `
401+ for cross-database operations.
343402
344403``` typescript
345404// Join data from multiple databases
@@ -509,6 +568,7 @@ await db.remove(); // Close and DELETE database file(s) - irreversible
509568| ` beginInterruptibleTransaction(statements) ` | Begin interruptible transaction, returns ` InterruptibleTransaction ` |
510569| ` fetchAll<T>(query, values?) ` | Execute SELECT, return all rows |
511570| ` fetchOne<T>(query, values?) ` | Execute SELECT, return single row or ` undefined ` |
571+ | ` fetchPage<T>(query, values, keyset, pageSize) ` | Keyset pagination, returns ` FetchPageBuilder ` |
512572| ` close() ` | Close connection, returns ` true ` if was loaded |
513573| ` remove() ` | Close and delete database file(s), returns ` true ` if was loaded |
514574| ` observe(tables, config?) ` | Enable change observation for tables |
@@ -517,12 +577,15 @@ await db.remove(); // Close and DELETE database file(s) - irreversible
517577
518578### Builder Methods
519579
520- All query methods (` execute ` , ` executeTransaction ` , ` fetchAll ` , ` fetchOne ` )
521- return builders that are directly awaitable and support method chaining:
580+ All query methods (` execute ` , ` executeTransaction ` , ` fetchAll ` , ` fetchOne ` ,
581+ ` fetchPage ` ) return builders that are directly awaitable and support method
582+ chaining:
522583
523584| Method | Description |
524585| ------ | ----------- |
525586| ` attach(specs) ` | Attach databases for cross-database queries, returns ` this ` |
587+ | ` after(cursor) ` | Set cursor for forward pagination (` FetchPageBuilder ` only), returns ` this ` |
588+ | ` before(cursor) ` | Set cursor for backward pagination (` FetchPageBuilder ` only), returns ` this ` |
526589| ` await builder ` | Execute the query (builders implement ` PromiseLike ` ) |
527590
528591### InterruptibleTransaction Methods
@@ -569,6 +632,19 @@ interface ObserverConfig {
569632 captureValues? : boolean ; // default: true
570633}
571634
635+ type SortDirection = ' asc' | ' desc' ;
636+
637+ interface KeysetColumn {
638+ name: string ; // Column name in the query result set
639+ direction: SortDirection ;
640+ }
641+
642+ interface KeysetPage <T = Record <string , SqlValue >> {
643+ rows: T [];
644+ nextCursor: SqlValue [] | null ; // Cursor to continue pagination, null when no more pages
645+ hasMore: boolean ;
646+ }
647+
572648type ChangeOperation = ' insert' | ' update' | ' delete' ;
573649
574650type ColumnValue =
@@ -647,6 +723,47 @@ if let Some(user_data) = user {
647723}
648724```
649725
726+ ### Pagination (Rust)
727+
728+ See [ Pagination] ( #pagination ) above for background on why the plugin uses
729+ keyset pagination. The Rust API works the same way via ` fetch_page ` :
730+
731+ ``` rust
732+ use sqlx_sqlite_toolkit :: pagination :: KeysetColumn ;
733+
734+ let keyset = vec! [
735+ KeysetColumn :: asc (" category" ),
736+ KeysetColumn :: desc (" score" ),
737+ KeysetColumn :: asc (" id" ),
738+ ];
739+
740+ // First page
741+ let page = db . fetch_page (
742+ " SELECT id, title, category, score FROM posts" . into (),
743+ vec! [],
744+ keyset . clone (),
745+ 25 ,
746+ ). await ? ;
747+
748+ // Next page (forward)
749+ if let Some (cursor ) = page . next_cursor {
750+ let next = db . fetch_page (
751+ " SELECT id, title, category, score FROM posts" . into (),
752+ vec! [],
753+ keyset . clone (),
754+ 25 ,
755+ ). after (cursor . clone ()). await ? ;
756+
757+ // Previous page (backward) — rows returned in original sort order
758+ let prev = db . fetch_page (
759+ " SELECT id, title, category, score FROM posts" . into (),
760+ vec! [],
761+ keyset ,
762+ 25 ,
763+ ). before (cursor ). await ? ;
764+ }
765+ ```
766+
650767### Simple Transactions
651768
652769Use ` execute_transaction() ` for atomic execution of multiple statements:
@@ -771,6 +888,7 @@ db.remove().await?; // Close and DELETE database file(s)
771888| ` begin_interruptible_transaction() ` | Begin interruptible transaction (builder) |
772889| ` fetch_all(query, values) ` | Fetch all rows |
773890| ` fetch_one(query, values) ` | Fetch single row |
891+ | ` fetch_page(query, values, keyset, page_size) ` | Keyset pagination (builder, supports ` .after() ` , ` .before() ` , ` .attach() ` ) |
774892| ` close() ` | Close connection |
775893| ` remove() ` | Close and delete database file(s) |
776894
@@ -818,6 +936,18 @@ fn main() {
818936}
819937```
820938
939+ ## Examples
940+
941+ Working Tauri demo apps are in the [ ` examples/ ` ] ( examples ) directory:
942+
943+ * ** [ ` observer-demo ` ] ( examples/observer-demo ) ** — Real-time change
944+ notifications with live streaming of inserts, updates, and deletes
945+ * ** [ ` pagination-demo ` ] ( examples/pagination-demo ) ** — Keyset pagination
946+ with a virtualized list and performance metrics
947+
948+ See the [ toolkit crate README] ( crates/sqlx-sqlite-toolkit/README.md#examples )
949+ for setup instructions.
950+
821951## Development
822952
823953This project follows
0 commit comments