Applies AX principle 2 (Comments as Usage Examples) — removes prose
descriptions that restate the function signature ("returns", "retrieves",
"creates", "wraps", etc.) and keeps or replaces with concrete usage
examples showing real calls with realistic values.
Co-Authored-By: Charon <charon@lethean.io>
201 lines
4.7 KiB
Go
201 lines
4.7 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/adrg/xdg"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|