heroagent/pkg/data/ourdb/db.go
2025-04-23 04:18:28 +02:00

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)
}