1+ use crate :: jstable;
12use crate :: log:: { LogEntry , Logger , Operation } ;
23use crate :: storage:: MemTable ;
3- use serde_json:: Value ;
4+ use serde_json:: { json, Value } ;
5+ use std:: fs;
46use uuid:: Uuid ;
57
68const MEMTABLE_THRESHOLD : usize = 10 ;
9+ const JSTABLE_THRESHOLD : u64 = 5 ;
710
811pub struct DB {
912 memtable : MemTable ,
@@ -14,7 +17,7 @@ pub struct DB {
1417
1518impl DB {
1619 pub fn new ( jstable_dir : & str ) -> Self {
17- std :: fs:: create_dir_all ( jstable_dir) . unwrap ( ) ;
20+ fs:: create_dir_all ( jstable_dir) . unwrap ( ) ;
1821 let log_path = format ! ( "{}/argus.log" , jstable_dir) ;
1922 let logger = Logger :: new ( & log_path, 1024 * 1024 ) . unwrap ( ) ;
2023 let mut memtable = MemTable :: new ( ) ;
@@ -84,6 +87,30 @@ impl DB {
8487 self . jstable_count += 1 ;
8588 self . memtable = MemTable :: new ( ) ;
8689 self . logger . rotate ( ) . unwrap ( ) ;
90+
91+ if self . jstable_count >= JSTABLE_THRESHOLD {
92+ self . compact ( ) ;
93+ }
94+ }
95+
96+ fn compact ( & mut self ) {
97+ let mut tables = Vec :: new ( ) ;
98+ for i in 0 ..self . jstable_count {
99+ let path = format ! ( "{}/jstable-{}" , self . jstable_dir, i) ;
100+ tables. push ( jstable:: read_jstable ( & path) . unwrap ( ) ) ;
101+ }
102+
103+ let merged_table = jstable:: merge_jstables ( & tables) ;
104+
105+ for i in 0 ..self . jstable_count {
106+ let path = format ! ( "{}/jstable-{}" , self . jstable_dir, i) ;
107+ fs:: remove_file ( path) . unwrap ( ) ;
108+ }
109+
110+ let new_path = format ! ( "{}/jstable-0" , self . jstable_dir) ;
111+ merged_table. write ( & new_path) . unwrap ( ) ;
112+
113+ self . jstable_count = 1 ;
87114 }
88115}
89116
@@ -170,4 +197,85 @@ mod tests {
170197 assert_eq ! ( db2. memtable. len( ) , 1 ) ;
171198 assert_eq ! ( * db2. memtable. documents. get( & id2) . unwrap( ) , doc2) ;
172199 }
200+
201+ #[ test]
202+ fn test_db_compaction ( ) {
203+ let dir = tempdir ( ) . unwrap ( ) ;
204+ let mut db = DB :: new ( dir. path ( ) . to_str ( ) . unwrap ( ) ) ;
205+
206+ for i in 0 ..( MEMTABLE_THRESHOLD * JSTABLE_THRESHOLD as usize ) {
207+ db. insert ( json ! ( { "a" : i } ) ) ;
208+ }
209+
210+ assert_eq ! ( db. jstable_count, JSTABLE_THRESHOLD - 1 ) ;
211+ db. insert ( json ! ( { "a" : 999 } ) ) ; // Trigger another flush, which triggers compaction
212+ assert_eq ! ( db. jstable_count, 1 ) ;
213+
214+ // Verify data is preserved
215+ // We inserted 0..50 (50 items) + 1 (999). 51 items total.
216+ // Item "0" should be in the compacted table.
217+ // We can't easily query DB yet (no read path implemented in DB),
218+ // so we manually check the file.
219+ let jstable_path = format ! ( "{}/jstable-0" , dir. path( ) . to_str( ) . unwrap( ) ) ;
220+ let table = jstable:: read_jstable ( & jstable_path) . unwrap ( ) ;
221+ assert ! ( table. documents. len( ) >= 50 ) ; // It should have the 50 items from the first 5 flushes (0-49)
222+ // The last inserted item (999) is in memtable, not in jstable-0 yet.
223+ }
224+
225+ #[ test]
226+ fn test_db_compaction_with_delete ( ) {
227+ let dir = tempdir ( ) . unwrap ( ) ;
228+ let mut db = DB :: new ( dir. path ( ) . to_str ( ) . unwrap ( ) ) ;
229+
230+ // 1. Insert doc to be deleted
231+ let id_to_delete = db. insert ( json ! ( { "a" : 100 } ) ) ;
232+
233+ // Fill memtable to force flush 1 (jstable-0)
234+ // 1 item already inserted. Insert 9 more to fill (total 10).
235+ for i in 0 ..9 {
236+ db. insert ( json ! ( { "fill" : i } ) ) ;
237+ }
238+ // 11th insert triggers flush of the first 10
239+ db. insert ( json ! ( { "trigger_1" : 1 } ) ) ;
240+ assert_eq ! ( db. jstable_count, 1 ) ;
241+
242+ // 2. Delete the doc
243+ // id_to_delete is in jstable-0.
244+ // delete adds tombstone to memtable.
245+ // memtable currently has "trigger_1" (1 item).
246+ // delete adds 1 item. len = 2.
247+ db. delete ( & id_to_delete) ;
248+
249+ // Fill memtable to force flush 2 (jstable-1)
250+ // Memtable len is 2. Need 8 more to fill (total 10).
251+ for i in 0 ..8 {
252+ db. insert ( json ! ( { "fill_2" : i } ) ) ;
253+ }
254+ // 11th insert (relative to this batch) triggers flush
255+ db. insert ( json ! ( { "trigger_2" : 1 } ) ) ;
256+ assert_eq ! ( db. jstable_count, 2 ) ;
257+
258+ // 3. Create 3 more tables to reach threshold 5
259+ for t in 0 ..3 {
260+ // Fill memtable (10 items)
261+ // Memtable has 1 item ("trigger_2" or previous trigger).
262+ // Need 9 more.
263+ for i in 0 ..9 {
264+ db. insert ( json ! ( { "fill_more" : t, "i" : i } ) ) ;
265+ }
266+ // Trigger flush
267+ db. insert ( json ! ( { "trigger_more" : t } ) ) ;
268+ }
269+ // After 3rd iteration (total 5th flush), compaction triggers.
270+ // jstable_count goes 4 -> 5 -> 1.
271+ assert_eq ! ( db. jstable_count, 1 ) ;
272+
273+ // 4. Verify id_to_delete is NOT in jstable-0
274+ let jstable_path = format ! ( "{}/jstable-0" , dir. path( ) . to_str( ) . unwrap( ) ) ;
275+ let table = jstable:: read_jstable ( & jstable_path) . unwrap ( ) ;
276+ assert ! ( !table. documents. contains_key( & id_to_delete) ) ;
277+
278+ // Verify other documents exist (e.g. from flush 1)
279+ assert ! ( table. documents. len( ) > 40 ) ;
280+ }
173281}
0 commit comments