174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
// Package ourdb provides a simple key-value database implementation with history tracking
|
|
package ourdb
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// OurDB represents a binary database with variable-length records
|
|
type OurDB struct {
|
|
lookup *LookupTable
|
|
path string // Directory in which we will have the lookup db as well as all the backend
|
|
incrementalMode bool
|
|
fileSize uint32
|
|
file *os.File
|
|
fileNr uint16 // The file which is open
|
|
lastUsedFileNr uint16
|
|
}
|
|
|
|
const headerSize = 12
|
|
|
|
// OurDBSetArgs contains the parameters for the Set method
|
|
type OurDBSetArgs struct {
|
|
ID *uint32
|
|
Data []byte
|
|
}
|
|
|
|
// Set stores data at the specified key position
|
|
// The data is stored with a CRC32 checksum for integrity verification
|
|
// and maintains a linked list of previous values for history tracking
|
|
// Returns the ID used (either x if specified, or auto-incremented if x=0)
|
|
func (db *OurDB) Set(args OurDBSetArgs) (uint32, error) {
|
|
if db.incrementalMode {
|
|
// If ID points to an empty location, return an error
|
|
// else, overwrite data
|
|
if args.ID != nil {
|
|
// This is an update
|
|
location, err := db.lookup.Get(*args.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if location.Position == 0 {
|
|
return 0, errors.New("cannot set id for insertions when incremental mode is enabled")
|
|
}
|
|
|
|
if err := db.set_(*args.ID, location, args.Data); err != nil {
|
|
return 0, err
|
|
}
|
|
return *args.ID, nil
|
|
}
|
|
|
|
// This is an insert
|
|
id, err := db.lookup.GetNextID()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := db.set_(id, Location{}, args.Data); err != nil {
|
|
return 0, err
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// Using key-value mode
|
|
if args.ID == nil {
|
|
return 0, errors.New("id must be provided when incremental is disabled")
|
|
}
|
|
location, err := db.lookup.Get(*args.ID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err := db.set_(*args.ID, location, args.Data); err != nil {
|
|
return 0, err
|
|
}
|
|
return *args.ID, nil
|
|
}
|
|
|
|
// Get retrieves data stored at the specified key position
|
|
// Returns error if the key doesn't exist or data is corrupted
|
|
func (db *OurDB) Get(x uint32) ([]byte, error) {
|
|
location, err := db.lookup.Get(x)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return db.get_(location)
|
|
}
|
|
|
|
// GetHistory retrieves a list of previous values for the specified key
|
|
// depth parameter controls how many historical values to retrieve (max)
|
|
// Returns error if key doesn't exist or if there's an issue accessing the data
|
|
func (db *OurDB) GetHistory(x uint32, depth uint8) ([][]byte, error) {
|
|
result := make([][]byte, 0)
|
|
currentLocation, err := db.lookup.Get(x)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Traverse the history chain up to specified depth
|
|
for i := uint8(0); i < depth; i++ {
|
|
// Get current value
|
|
data, err := db.get_(currentLocation)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, data)
|
|
|
|
// Try to get previous location
|
|
prevLocation, err := db.getPrevPos_(currentLocation)
|
|
if err != nil {
|
|
break
|
|
}
|
|
if prevLocation.Position == 0 {
|
|
break
|
|
}
|
|
currentLocation = prevLocation
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Delete removes the data at the specified key position
|
|
// This operation zeros out the record but maintains the space in the file
|
|
// Use condense() to reclaim space from deleted records (happens in step after)
|
|
func (db *OurDB) Delete(x uint32) error {
|
|
location, err := db.lookup.Get(x)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := db.delete_(x, location); err != nil {
|
|
return err
|
|
}
|
|
return db.lookup.Delete(x)
|
|
}
|
|
|
|
// GetNextID returns the next id which will be used when storing
|
|
func (db *OurDB) GetNextID() (uint32, error) {
|
|
if !db.incrementalMode {
|
|
return 0, errors.New("incremental mode is not enabled")
|
|
}
|
|
return db.lookup.GetNextID()
|
|
}
|
|
|
|
// lookupDumpPath returns the path to the lookup dump file
|
|
func (db *OurDB) lookupDumpPath() string {
|
|
return filepath.Join(db.path, "lookup_dump.db")
|
|
}
|
|
|
|
// Load metadata if exists
|
|
func (db *OurDB) Load() error {
|
|
if _, err := os.Stat(db.lookupDumpPath()); err == nil {
|
|
return db.lookup.ImportSparse(db.lookupDumpPath())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Save ensures we have the metadata stored on disk
|
|
func (db *OurDB) Save() error {
|
|
return db.lookup.ExportSparse(db.lookupDumpPath())
|
|
}
|
|
|
|
// Close closes the database file
|
|
func (db *OurDB) Close() error {
|
|
if err := db.Save(); err != nil {
|
|
return err
|
|
}
|
|
return db.close_()
|
|
}
|
|
|
|
// Destroy closes and removes the database
|
|
func (db *OurDB) Destroy() error {
|
|
_ = db.Close()
|
|
return os.RemoveAll(db.path)
|
|
}
|