package database import ( "database/sql" "os" "path/filepath" "sync" "time" "github.com/adrg/xdg" _ "github.com/mattn/go-sqlite3" ) // databaseError wraps an error with a scope message for consistent error reporting. // databaseError("open database", err) // => "database: open database: " func databaseError(scope string, cause error) error { if cause == nil { return nil } return &databaseErr{scope: scope, cause: cause} } type databaseErr struct { scope string cause error } func (e *databaseErr) Error() string { return "database: " + e.scope + ": " + e.cause.Error() } func (e *databaseErr) Unwrap() error { return e.cause } // globalDatabase is the global database instance // db := globalDatabase // check before use; nil means not initialised var ( globalDatabase *sql.DB databaseMutex sync.RWMutex ) // database.Config{Enabled: true, Path: "/data/mining.db", RetentionDays: 30} type Config struct { Enabled bool `json:"enabled"` Path string `json:"path,omitempty"` RetentionDays int `json:"retentionDays,omitempty"` } // configuration := defaultConfig() // Config{Enabled: true, RetentionDays: 30} func defaultConfig() Config { return Config{ Enabled: true, Path: "", RetentionDays: 30, } } // path, err := defaultDBPath() // "~/.local/share/lethean-desktop/mining.db" func defaultDBPath() (string, error) { dataDir := filepath.Join(xdg.DataHome, "lethean-desktop") if err := os.MkdirAll(dataDir, 0755); err != nil { return "", databaseError("create data directory", err) } return filepath.Join(dataDir, "mining.db"), nil } // Initialize opens the SQLite database and creates all required tables. // database.Initialize(database.Config{Enabled: true, Path: "/data/mining.db", RetentionDays: 30}) func Initialize(config Config) error { databaseMutex.Lock() defer databaseMutex.Unlock() if !config.Enabled { return nil } dbPath := config.Path if dbPath == "" { var err error dbPath, err = defaultDBPath() if err != nil { return err } } var err error globalDatabase, err = sql.Open("sqlite3", dbPath+"?_journal=WAL&_timeout=5000") if err != nil { return databaseError("open database", err) } // Set connection pool settings globalDatabase.SetMaxOpenConns(1) // SQLite only supports one writer globalDatabase.SetMaxIdleConns(1) globalDatabase.SetConnMaxLifetime(time.Hour) // Create tables if err := createTables(); err != nil { // Nil out global before closing to prevent use of closed connection closingDB := globalDatabase globalDatabase = nil closingDB.Close() return databaseError("create tables", err) } return nil } // if err := database.Close(); err != nil { logging.Warn("close failed", ...) } func Close() error { databaseMutex.Lock() defer databaseMutex.Unlock() if globalDatabase == nil { return nil } err := globalDatabase.Close() globalDatabase = nil return err } // isInitialized reports whether the global database connection is open. // if isInitialized() { database.Cleanup(30) } func isInitialized() bool { databaseMutex.RLock() defer databaseMutex.RUnlock() return globalDatabase != nil } // if err := createTables(); err != nil { return databaseError("create tables", err) } func createTables() error { schema := ` -- Hashrate history table for storing miner performance data CREATE TABLE IF NOT EXISTS hashrate_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, miner_name TEXT NOT NULL, miner_type TEXT NOT NULL, timestamp DATETIME NOT NULL, hashrate INTEGER NOT NULL, resolution TEXT NOT NULL DEFAULT 'high', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Index for efficient queries by miner and time range CREATE INDEX IF NOT EXISTS idx_hashrate_miner_time ON hashrate_history(miner_name, timestamp DESC); -- Index for cleanup queries CREATE INDEX IF NOT EXISTS idx_hashrate_resolution_time ON hashrate_history(resolution, timestamp); -- Miner sessions table for tracking uptime CREATE TABLE IF NOT EXISTS miner_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, miner_name TEXT NOT NULL, miner_type TEXT NOT NULL, started_at DATETIME NOT NULL, stopped_at DATETIME, total_shares INTEGER DEFAULT 0, rejected_shares INTEGER DEFAULT 0, average_hashrate INTEGER DEFAULT 0 ); -- Index for session queries CREATE INDEX IF NOT EXISTS idx_sessions_miner ON miner_sessions(miner_name, started_at DESC); ` _, err := globalDatabase.Exec(schema) return err } // database.Cleanup(30) // remove hashrate rows older than 30 days func Cleanup(retentionDays int) error { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == nil { return nil } cutoff := time.Now().AddDate(0, 0, -retentionDays) _, err := globalDatabase.Exec(` DELETE FROM hashrate_history WHERE timestamp < ? `, cutoff) return err } // if err := vacuumDB(); err != nil { logging.Warn("vacuum failed", ...) } func vacuumDB() error { databaseMutex.RLock() defer databaseMutex.RUnlock() if globalDatabase == nil { return nil } _, err := globalDatabase.Exec("VACUUM") return err }