@@ -14,7 +14,9 @@ use std::sync::atomic::{AtomicU64, Ordering};
1414use std:: sync:: { Arc , Mutex } ;
1515
1616use lightning:: io;
17- use lightning:: util:: persist:: { KVStore , KVStoreSync } ;
17+ use lightning:: util:: persist:: {
18+ KVStore , KVStoreSync , PageToken , PaginatedKVStore , PaginatedKVStoreSync , PaginatedListResponse ,
19+ } ;
1820use lightning_types:: string:: PrintableString ;
1921use rusqlite:: { named_params, Connection } ;
2022
@@ -34,7 +36,10 @@ pub const DEFAULT_SQLITE_DB_FILE_NAME: &str = "ldk_data.sqlite";
3436pub const DEFAULT_KV_TABLE_NAME : & str = "ldk_data" ;
3537
3638// The current SQLite `user_version`, which we can use if we'd ever need to do a schema migration.
37- const SCHEMA_USER_VERSION : u16 = 2 ;
39+ const SCHEMA_USER_VERSION : u16 = 3 ;
40+
41+ // The default page size for paginated list operations.
42+ const DEFAULT_PAGE_SIZE : u32 = 100 ;
3843
3944/// A [`KVStoreSync`] implementation that writes to and reads from an [SQLite] database.
4045///
@@ -222,6 +227,33 @@ impl KVStoreSync for SqliteStore {
222227 }
223228}
224229
230+ impl PaginatedKVStoreSync for SqliteStore {
231+ fn list_paginated (
232+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
233+ ) -> io:: Result < PaginatedListResponse > {
234+ self . inner . list_paginated_internal ( primary_namespace, secondary_namespace, page_token)
235+ }
236+ }
237+
238+ impl PaginatedKVStore for SqliteStore {
239+ fn list_paginated (
240+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
241+ ) -> impl Future < Output = Result < PaginatedListResponse , io:: Error > > + ' static + Send {
242+ let primary_namespace = primary_namespace. to_string ( ) ;
243+ let secondary_namespace = secondary_namespace. to_string ( ) ;
244+ let inner = Arc :: clone ( & self . inner ) ;
245+ let fut = tokio:: task:: spawn_blocking ( move || {
246+ inner. list_paginated_internal ( & primary_namespace, & secondary_namespace, page_token)
247+ } ) ;
248+ async move {
249+ fut. await . unwrap_or_else ( |e| {
250+ let msg = format ! ( "Failed to IO operation due join error: {}" , e) ;
251+ Err ( io:: Error :: new ( io:: ErrorKind :: Other , msg) )
252+ } )
253+ }
254+ }
255+ }
256+
225257struct SqliteStoreInner {
226258 connection : Arc < Mutex < Connection > > ,
227259 data_dir : PathBuf ,
@@ -289,7 +321,9 @@ impl SqliteStoreInner {
289321 primary_namespace TEXT NOT NULL,
290322 secondary_namespace TEXT DEFAULT \" \" NOT NULL,
291323 key TEXT NOT NULL CHECK (key <> ''),
292- value BLOB, PRIMARY KEY ( primary_namespace, secondary_namespace, key )
324+ value BLOB,
325+ created_at INTEGER DEFAULT (strftime('%s','now')),
326+ PRIMARY KEY ( primary_namespace, secondary_namespace, key )
293327 );" ,
294328 kv_table_name
295329 ) ;
@@ -299,6 +333,17 @@ impl SqliteStoreInner {
299333 io:: Error :: new ( io:: ErrorKind :: Other , msg)
300334 } ) ?;
301335
336+ // Create index for efficient paginated queries ordered by creation time, then key
337+ let index_sql = format ! (
338+ "CREATE INDEX IF NOT EXISTS {}_created_at_idx ON {} (primary_namespace, secondary_namespace, created_at DESC, key ASC);" ,
339+ kv_table_name, kv_table_name
340+ ) ;
341+
342+ connection. execute ( & index_sql, [ ] ) . map_err ( |e| {
343+ let msg = format ! ( "Failed to create index on {}: {}" , kv_table_name, e) ;
344+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
345+ } ) ?;
346+
302347 let connection = Arc :: new ( Mutex :: new ( connection) ) ;
303348 let write_version_locks = Mutex :: new ( HashMap :: new ( ) ) ;
304349 Ok ( Self { connection, data_dir, kv_table_name, write_version_locks } )
@@ -472,6 +517,119 @@ impl SqliteStoreInner {
472517 Ok ( keys)
473518 }
474519
520+ fn list_paginated_internal (
521+ & self , primary_namespace : & str , secondary_namespace : & str , page_token : Option < PageToken > ,
522+ ) -> io:: Result < PaginatedListResponse > {
523+ check_namespace_key_validity (
524+ primary_namespace,
525+ secondary_namespace,
526+ None ,
527+ "list_paginated" ,
528+ ) ?;
529+
530+ let locked_conn = self . connection . lock ( ) . unwrap ( ) ;
531+
532+ // Extract the last_key from the page token
533+ let last_key = page_token. map ( |t| t. 0 ) ;
534+
535+ // If last_key is provided, we need to get the created_at value for that key to use as a cursor
536+ let last_created_at: Option < i64 > = if let Some ( ref key) = last_key {
537+ let sql = format ! (
538+ "SELECT created_at FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key" ,
539+ self . kv_table_name
540+ ) ;
541+ let mut stmt = locked_conn. prepare_cached ( & sql) . map_err ( |e| {
542+ let msg = format ! ( "Failed to prepare statement: {}" , e) ;
543+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
544+ } ) ?;
545+
546+ stmt. query_row (
547+ named_params ! {
548+ ":primary_namespace" : primary_namespace,
549+ ":secondary_namespace" : secondary_namespace,
550+ ":key" : key,
551+ } ,
552+ |row| row. get ( 0 ) ,
553+ )
554+ . ok ( )
555+ } else {
556+ None
557+ } ;
558+
559+ // Query for keys, ordered by created_at DESC, key ASC (newest first, with key as tiebreaker)
560+ // For pagination with composite sort, we need: (created_at < last) OR (created_at = last AND key > last_key)
561+ //
562+ // We fetch DEFAULT_PAGE_SIZE + 1 rows as an optimization to detect if more pages exist.
563+ // If we get more than DEFAULT_PAGE_SIZE rows, we know there are additional results beyond
564+ // this page. We then discard the extra row and use the last returned key as the next page
565+ // token. This avoids needing a separate COUNT(*) query to determine pagination state.
566+ let ( sql, params) : ( String , Vec < ( & str , & dyn rusqlite:: ToSql ) > ) = if let (
567+ Some ( ref created_at) ,
568+ Some ( ref key) ,
569+ ) =
570+ ( & last_created_at, & last_key)
571+ {
572+ (
573+ format ! (
574+ "SELECT key FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace \
575+ AND (created_at < :last_created_at OR (created_at = :last_created_at AND key > :last_key)) \
576+ ORDER BY created_at DESC, key ASC LIMIT :limit",
577+ self . kv_table_name
578+ ) ,
579+ vec ! [
580+ ( ":primary_namespace" , & primary_namespace as & dyn rusqlite:: ToSql ) ,
581+ ( ":secondary_namespace" , & secondary_namespace as & dyn rusqlite:: ToSql ) ,
582+ ( ":last_created_at" , created_at as & dyn rusqlite:: ToSql ) ,
583+ ( ":last_key" , key as & dyn rusqlite:: ToSql ) ,
584+ ( ":limit" , & ( DEFAULT_PAGE_SIZE + 1 ) as & dyn rusqlite:: ToSql ) ,
585+ ] ,
586+ )
587+ } else {
588+ (
589+ format ! (
590+ "SELECT key FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace \
591+ ORDER BY created_at DESC, key ASC LIMIT :limit",
592+ self . kv_table_name
593+ ) ,
594+ vec ! [
595+ ( ":primary_namespace" , & primary_namespace as & dyn rusqlite:: ToSql ) ,
596+ ( ":secondary_namespace" , & secondary_namespace as & dyn rusqlite:: ToSql ) ,
597+ ( ":limit" , & ( DEFAULT_PAGE_SIZE + 1 ) as & dyn rusqlite:: ToSql ) ,
598+ ] ,
599+ )
600+ } ;
601+
602+ let mut stmt = locked_conn. prepare_cached ( & sql) . map_err ( |e| {
603+ let msg = format ! ( "Failed to prepare statement: {}" , e) ;
604+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
605+ } ) ?;
606+
607+ let mut keys = Vec :: with_capacity ( ( DEFAULT_PAGE_SIZE + 1 ) as usize ) ;
608+
609+ let rows_iter = stmt. query_map ( params. as_slice ( ) , |row| row. get ( 0 ) ) . map_err ( |e| {
610+ let msg = format ! ( "Failed to retrieve queried rows: {}" , e) ;
611+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
612+ } ) ?;
613+
614+ for k in rows_iter {
615+ keys. push ( k. map_err ( |e| {
616+ let msg = format ! ( "Failed to retrieve queried rows: {}" , e) ;
617+ io:: Error :: new ( io:: ErrorKind :: Other , msg)
618+ } ) ?) ;
619+ }
620+
621+ // Check if we have more pages by seeing if we got the extra "peek" row.
622+ // If so, remove it and generate a page token from the last actual result.
623+ let next_page_token = if keys. len ( ) > DEFAULT_PAGE_SIZE as usize {
624+ keys. pop ( ) ; // Remove the extra "peek" row used to detect more pages
625+ keys. last ( ) . cloned ( ) . map ( PageToken )
626+ } else {
627+ None
628+ } ;
629+
630+ Ok ( PaginatedListResponse { keys, next_page_token } )
631+ }
632+
475633 fn execute_locked_write < F : FnOnce ( ) -> Result < ( ) , lightning:: io:: Error > > (
476634 & self , inner_lock_ref : Arc < Mutex < u64 > > , locking_key : String , version : u64 , callback : F ,
477635 ) -> Result < ( ) , lightning:: io:: Error > {
0 commit comments