@@ -7,25 +7,35 @@ import (
77 "io"
88 "os"
99 "path/filepath"
10+ "slices"
11+ "strconv"
12+ "strings"
1013 "sync"
1114
1215 "github.com/aalhour/beachdb/internal/keys"
1316 "github.com/aalhour/beachdb/internal/memtable"
17+ "github.com/aalhour/beachdb/internal/sstable"
1418 "github.com/aalhour/beachdb/internal/wal"
1519)
1620
17- var (
18- // walFileName specifies the name of the WAL file
21+ const (
22+ // walFileName specifies the name of the WAL file.
1923 walFileName = "beachdb.wal"
24+
25+ // sstableFileExt specifies the extension of SSTable files.
26+ sstableFileExt = ".sst"
2027)
2128
2229// DB defines the database struct wrapping the public APIs.
2330type DB struct {
24- mu sync.RWMutex // synchronization for safe concurrency.
25- dir string // path on disk to write data into.
31+ closed bool // Flag indicating whether db is closed or not
32+ mu sync.RWMutex // Synchronization for safe concurrency.
33+ dir string // Path on disk to write data into.
2634 wal * wal.Writer // Writer for WAL file.
2735 mem memtable.Memtable // Memory table data structure
2836 seqno uint64 // Monotonic sequence counter
37+ ssts []* sstable.Reader // Open SSTable readers, newest-first
38+ nextSSTID uint64 // Counter for SST file naming (new files)
2939 syncOnWrite bool // Option: Whether the DB should fsync writes or not.
3040}
3141
@@ -41,6 +51,7 @@ func Open(dir string, opts ...Option) (*DB, error) {
4151
4252 // Initialize the DB struct
4353 db := & DB {
54+ closed : false ,
4455 dir : dir ,
4556 mem : memtable .NewSkipList (),
4657 seqno : 0 ,
@@ -72,6 +83,30 @@ func Open(dir string, opts ...Option) (*DB, error) {
7283 return nil , err
7384 }
7485
86+ // Discover SSTables and create readers for them
87+ sortedFileNames , nextSSTID , err := discoverSSTables (dir )
88+ if err != nil {
89+ _ = db .Close ()
90+ return nil , fmt .Errorf ("beachdb: error discovering SSTables, %w" , err )
91+ }
92+
93+ // Iterate over discovered sstable files and open readers for them
94+ for _ , fileName := range sortedFileNames {
95+ fullPath := filepath .Join (dir , fileName )
96+ sstableFile , err := os .Open (fullPath ) //nolint:gosec // trusted dir + discovered filename
97+ if err != nil {
98+ _ = db .Close ()
99+ return nil , fmt .Errorf ("beachdb: opening SSTable %s: %w" , fileName , err )
100+ }
101+ sstReader , err := sstable .OpenReader (sstableFile )
102+ if err != nil {
103+ _ = db .Close ()
104+ return nil , fmt .Errorf ("beachdb: reading SSTable %s: %w" , fileName , err )
105+ }
106+ db .ssts = append (db .ssts , sstReader )
107+ }
108+ db .nextSSTID = nextSSTID
109+
75110 return db , nil
76111}
77112
@@ -80,16 +115,16 @@ func (db *DB) Write(ctx context.Context, b *Batch) error {
80115 db .mu .Lock ()
81116 defer db .mu .Unlock ()
82117
118+ // Check if the database was closed
119+ if db .closed {
120+ return ErrDBClosed
121+ }
122+
83123 // Check if already canceled before doing any work
84124 if err := ctx .Err (); err != nil {
85125 return fmt .Errorf ("beachdb: Write call canceled: %w" , err )
86126 }
87127
88- // Check if the database was closed
89- if db .wal == nil {
90- return ErrDBClosed
91- }
92-
93128 // Encode the Batch and append it to the WAL
94129 if b == nil {
95130 return nil
@@ -124,16 +159,54 @@ func (db *DB) Get(ctx context.Context, key []byte) (value []byte, err error) {
124159 db .mu .RLock ()
125160 defer db .mu .RUnlock ()
126161
162+ // Check if the database was closed
163+ if db .closed {
164+ return nil , ErrDBClosed
165+ }
166+
127167 // Check if the call was canceled
128168 if err := ctx .Err (); err != nil {
129169 return nil , fmt .Errorf ("beachdb: Get call canceled: %w" , err )
130170 }
131171
172+ // Search the memtable
132173 value , found := db .mem .Get (key , db .seqno )
133- if ! found {
134- return nil , ErrKeyNotFound
174+
175+ // Check if the key was found in the membtable
176+ if found {
177+ if value == nil {
178+ // Tombestone (latest update in memtable was a delete operation)
179+ return nil , ErrKeyNotFound
180+ }
181+ return value , nil
135182 }
136- return value , nil
183+
184+ // Otherwise, search for key in SSTables in reverse order, because
185+ // they are sorted lexicographically from oldest to newest (e.g.: 001 --> 123)
186+ for _ , reader := range slices .Backward (db .ssts ) {
187+ value , err := reader .Get (key , db .seqno )
188+
189+ // Value found
190+ if err == nil {
191+ return value , nil
192+ }
193+
194+ // Tombstone: key was explicitly deleted at this level, stop searching
195+ if errors .Is (err , sstable .ErrKeyDeleted ) {
196+ return nil , ErrKeyNotFound
197+ }
198+
199+ // Key absent from this SSTable, try the next one
200+ if errors .Is (err , sstable .ErrKeyNotFound ) {
201+ continue
202+ }
203+
204+ // Real error (corruption, I/O failure)
205+ return nil , fmt .Errorf ("beachdb: reading SSTable: %w" , err )
206+ }
207+
208+ // Scanned all SSTables and found nothing, return an error.
209+ return nil , ErrKeyNotFound
137210}
138211
139212// Put writes the key-value pair in the database.
@@ -161,20 +234,35 @@ func (db *DB) Close() error {
161234 db .mu .Lock ()
162235 defer db .mu .Unlock ()
163236
164- // Check if already closed or not
165- if db .wal == nil {
237+ // Check if the database was closed
238+ if db .closed {
166239 return ErrDBClosed
167240 }
168241
242+ var firstError error
243+
244+ // Close all SSTable readers before closing the WAL writer
245+ for _ , reader := range db .ssts {
246+ err := reader .Close ()
247+ if err != nil && firstError == nil {
248+ firstError = err
249+ }
250+ }
251+
169252 // Close the WAL writer
170253 err := db .wal .Close ()
254+ if err != nil && firstError == nil {
255+ firstError = err
256+ }
171257
172258 // Mark it as closed
259+ db .ssts = nil
173260 db .wal = nil
174261 db .mem = nil
262+ db .closed = true
175263
176- if err != nil {
177- return fmt .Errorf ("beachdb: closing WAL : %w" , err )
264+ if firstError != nil {
265+ return fmt .Errorf ("beachdb: error closing DB : %w" , firstError )
178266 }
179267 return nil
180268}
@@ -255,3 +343,143 @@ func replayWAL(db *DB, walFilePath string) error {
255343
256344 return nil
257345}
346+
347+ // flushMemtable writes the contents in db.mem to a new SST file and replaces the
348+ // memtable with a new one
349+ func (db * DB ) flushMemtable () error {
350+ // -----------------------------------
351+ // Phase 1: swap mutable under a lock
352+ // -----------------------------------
353+ db .mu .Lock ()
354+
355+ // Check if db was closed
356+ if db .closed {
357+ db .mu .Unlock ()
358+ return ErrDBClosed
359+ }
360+
361+ // Grab a pointer to the previous db.mem
362+ immutableMem := db .mem
363+ // Replace the memtable with a new one
364+ db .mem = memtable .NewSkipList ()
365+ // Get path of next sstable
366+ sstPath := db .nextSSTPath ()
367+ // Increment next SSTID (for future flushes)
368+ db .nextSSTID ++
369+ db .mu .Unlock () // release!
370+
371+ // ----------------------------------------------
372+ // Phase 2: operate over (old) immutable memtable
373+ // doesn't require locking
374+ // ----------------------------------------------
375+ // Create the new sstable file
376+ sstFile , err := os .Create (sstPath ) //nolint:gosec // path constructed from trusted db.dir + formatted ID
377+ if err != nil {
378+ return ErrCreatingSSTFile
379+ }
380+ defer sstFile .Close ()
381+
382+ // Create the sstable writer
383+ writer , err := sstable .NewWriter (sstFile , sstable .WithSync (true ))
384+ if err != nil {
385+ _ = os .Remove (sstPath )
386+ return fmt .Errorf ("beachdb: creating SSTable writer: %w" , err )
387+ }
388+
389+ // Iterate the immutable memtable and write entries to the SSTable
390+ iter := immutableMem .NewIterator ()
391+ iter .SeekToFirst ()
392+ for iter .Valid () {
393+ if err := writer .Add (iter .Key (), iter .Value ()); err != nil {
394+ _ = writer .Close ()
395+ _ = iter .Close ()
396+ _ = os .Remove (sstPath )
397+ return fmt .Errorf ("beachdb: writing entry to SSTable: %w" , err )
398+ }
399+ iter .Next ()
400+ }
401+
402+ _ = iter .Close ()
403+ if err = writer .Close (); err != nil {
404+ _ = os .Remove (sstPath )
405+ return fmt .Errorf ("beachdb: closing SSTable writer: %w" , err )
406+ }
407+
408+ // Sync parent directory so the new file's directory entry is durable
409+ if err = syncDir (db .dir ); err != nil {
410+ return fmt .Errorf ("beachdb: syncing directory after flush: %w" , err )
411+ }
412+
413+ // Re-open the file for reading
414+ sstFileReadMode , err := os .Open (sstPath ) //nolint:gosec // path constructed from trusted db.dir + formatted ID
415+ if err != nil {
416+ return fmt .Errorf ("beachdb: opening SSTable for reading: %w" , err )
417+ }
418+ sstReader , err := sstable .OpenReader (sstFileReadMode )
419+ if err != nil {
420+ return fmt .Errorf ("beachdb: reading flushed SSTable: %w" , err )
421+ }
422+
423+ // -------------------------------------------------------
424+ // Phase 3 — Publish the new sstable reader (under a lock)
425+ // -------------------------------------------------------
426+ db .mu .Lock ()
427+ db .ssts = append (db .ssts , sstReader )
428+ db .mu .Unlock ()
429+
430+ // Synccess! :>
431+ return nil
432+ }
433+
434+ // nextSSTPath returns a full path for the SSTable file from
435+ // the internal nextSSTID
436+ func (db * DB ) nextSSTPath () string {
437+ return filepath .Join (db .dir , buildSSTFileName (db .nextSSTID ))
438+ }
439+
440+ // Helper function for discovering SSTable files on disk
441+ func discoverSSTables (dir string ) ([]string , uint64 , error ) {
442+ dirEntries , err := os .ReadDir (dir )
443+ if err != nil {
444+ return nil , 0 , fmt .Errorf ("beachdb: reading directory: %w" , err )
445+ }
446+
447+ sstableFiles := make ([]string , 0 , len (dirEntries ))
448+
449+ for _ , entry := range dirEntries {
450+ if ! entry .Type ().IsRegular () {
451+ continue
452+ }
453+
454+ fileName := entry .Name ()
455+ if filepath .Ext (fileName ) != sstableFileExt {
456+ continue
457+ }
458+
459+ sstableFiles = append (sstableFiles , fileName )
460+ }
461+
462+ slices .Sort (sstableFiles )
463+
464+ var maxID uint64
465+ n := len (sstableFiles )
466+ if n > 0 {
467+ biggestName := sstableFiles [n - 1 ]
468+ strID := strings .TrimSuffix (biggestName , filepath .Ext (biggestName ))
469+
470+ parsedID , err := strconv .ParseUint (strID , 10 , 64 )
471+ if err != nil {
472+ return sstableFiles , 0 , fmt .Errorf ("beachdb: parsing SSTable ID %q: %w" , biggestName , err )
473+ }
474+
475+ maxID = parsedID + 1
476+ }
477+
478+ return sstableFiles , maxID , nil
479+ }
480+
481+ // Helper function for building an SSTable file name from a
482+ // file ID number, e.g.: 1 --> 000001.sst
483+ func buildSSTFileName (id uint64 ) string {
484+ return fmt .Sprintf ("%06d.sst" , id )
485+ }
0 commit comments