@@ -26,7 +26,7 @@ use pagedb::vfs::traits::Vfs;
2626use pagedb:: { Db , RealmId } ;
2727
2828use crate :: error:: LiteError ;
29- use crate :: storage:: engine:: { KvPair , StorageEngine , WriteOp } ;
29+ use crate :: storage:: engine:: { CompactionOutcome , KvPair , StorageEngine , WriteOp } ;
3030use nodedb_types:: Namespace ;
3131
3232// ─── VFS aliases ─────────────────────────────────────────────────────────────
@@ -453,6 +453,15 @@ where
453453 . collect ( ) )
454454 }
455455
456+ async fn compact ( & self ) -> Result < CompactionOutcome , LiteError > {
457+ let stats = self . db . compact_now ( ) . await . map_err ( LiteError :: from) ?;
458+ Ok ( CompactionOutcome {
459+ reclaimed_pages : stats. main_db_pages_reclaimed ,
460+ segments_repacked : stats. segments_repacked ,
461+ file_bytes_freed : stats. bytes_truncated ,
462+ } )
463+ }
464+
456465 fn as_vector_segment_ext (
457466 & self ,
458467 ) -> Option < & dyn crate :: storage:: vector_segment_ext:: VectorSegmentExt > {
@@ -744,6 +753,15 @@ impl<V: Vfs + Clone + 'static> StorageEngine for PagedbStorage<V> {
744753 . map ( |( k, v) | ( strip_prefix ( & k) . to_vec ( ) , v) )
745754 . collect ( ) )
746755 }
756+
757+ async fn compact ( & self ) -> Result < CompactionOutcome , LiteError > {
758+ let stats = self . db . compact_now ( ) . await . map_err ( LiteError :: from) ?;
759+ Ok ( CompactionOutcome {
760+ reclaimed_pages : stats. main_db_pages_reclaimed ,
761+ segments_repacked : stats. segments_repacked ,
762+ file_bytes_freed : stats. bytes_truncated ,
763+ } )
764+ }
747765}
748766
749767// ─── VectorSegmentExt impl ────────────────────────────────────────────────────
@@ -1227,6 +1245,65 @@ mod tests {
12271245 assert_eq ! ( results[ 2 ] . 0 , & [ 4u8 ] ) ;
12281246 }
12291247
1248+ /// In-memory engine: `compact()` is a successful no-op (nothing to reclaim).
1249+ #[ tokio:: test]
1250+ async fn compact_mem_is_ok_noop ( ) {
1251+ let s = make_storage ( ) . await ;
1252+ s. put ( Namespace :: Vector , b"v1" , b"hello" ) . await . unwrap ( ) ;
1253+ s. put ( Namespace :: Graph , b"g1" , b"world" ) . await . unwrap ( ) ;
1254+ let outcome = s. compact ( ) . await . unwrap ( ) ;
1255+ // Data still readable after compaction.
1256+ assert_eq ! (
1257+ s. get( Namespace :: Vector , b"v1" ) . await . unwrap( ) . as_deref( ) ,
1258+ Some ( b"hello" . as_slice( ) )
1259+ ) ;
1260+ // MemVfs has no file truncation, but the call must succeed regardless.
1261+ let _ = outcome. reclaimed_pages ;
1262+ }
1263+
1264+ /// Disk-backed engine on a tempdir: write rows (including churn that leaves
1265+ /// dead pages), then `compact()` must succeed and report a non-negative
1266+ /// outcome. Data must remain intact afterward.
1267+ #[ cfg( not( target_arch = "wasm32" ) ) ]
1268+ #[ tokio:: test]
1269+ async fn compact_default_disk_is_ok ( ) {
1270+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
1271+ let path = dir. path ( ) . join ( "compact-test.db" ) ;
1272+ let s = PagedbStorage :: < DefaultVfs > :: open (
1273+ & path,
1274+ crate :: storage:: encryption:: Encryption :: Plaintext ,
1275+ )
1276+ . await
1277+ . unwrap ( ) ;
1278+
1279+ // Churn: write then overwrite/delete a batch of keys so the
1280+ // deferred-free list has pages to reclaim.
1281+ for i in 0u32 ..200 {
1282+ let key = i. to_be_bytes ( ) ;
1283+ s. put ( Namespace :: Meta , & key, & vec ! [ 0xCDu8 ; 512 ] )
1284+ . await
1285+ . unwrap ( ) ;
1286+ }
1287+ for i in 0u32 ..150 {
1288+ let key = i. to_be_bytes ( ) ;
1289+ s. delete ( Namespace :: Meta , & key) . await . unwrap ( ) ;
1290+ }
1291+
1292+ let outcome = s. compact ( ) . await . unwrap ( ) ;
1293+
1294+ // Surviving keys still readable.
1295+ let survivor = 175u32 . to_be_bytes ( ) ;
1296+ assert ! ( s. get( Namespace :: Meta , & survivor) . await . unwrap( ) . is_some( ) ) ;
1297+
1298+ // Outcome fields are well-formed (u64/u32 — always >= 0); just touch
1299+ // them so the assertion documents the reported shape.
1300+ let _ = (
1301+ outcome. reclaimed_pages ,
1302+ outcome. segments_repacked ,
1303+ outcome. file_bytes_freed ,
1304+ ) ;
1305+ }
1306+
12301307 /// Keys in namespace N must not appear in a scan of namespace N+1, and
12311308 /// vice versa. Verifies the single-byte prefix boundary.
12321309 #[ tokio:: test]
0 commit comments