Mining/pkg/database/database.go
Claude 132b69426a
ax(batch): replace prose comments with usage examples (AX Principle 2)
Convert "X returns the Y" / "X holds Y" / "X represents Y" style
comments to concrete usage examples across database/, logging/, and
mining/ packages. Comments now show how to call the function with
realistic values instead of restating what the signature already says.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-02 18:03:43 +01:00

205 lines
5 KiB
Go

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: <cause>"
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
}