package database import ( "database/sql" "fmt" "os" "path/filepath" "sync" "time" "github.com/adrg/xdg" _ "github.com/mattn/go-sqlite3" ) // DB is the global database instance var ( db *sql.DB dbMu sync.RWMutex ) // Config holds database configuration options type Config struct { // Enabled determines if database persistence is active Enabled bool `json:"enabled"` // Path is the database file path (optional, uses default if empty) Path string `json:"path,omitempty"` // RetentionDays is how long to keep historical data (default 30) RetentionDays int `json:"retentionDays,omitempty"` } // defaultConfig returns the default database configuration func defaultConfig() Config { return Config{ Enabled: true, Path: "", RetentionDays: 30, } } // defaultDBPath returns the default database file path func defaultDBPath() (string, error) { dataDir := filepath.Join(xdg.DataHome, "lethean-desktop") if err := os.MkdirAll(dataDir, 0755); err != nil { return "", fmt.Errorf("failed to create data directory: %w", err) } return filepath.Join(dataDir, "mining.db"), nil } // Initialize opens the database connection and creates tables func Initialize(cfg Config) error { dbMu.Lock() defer dbMu.Unlock() if !cfg.Enabled { return nil } dbPath := cfg.Path if dbPath == "" { var err error dbPath, err = defaultDBPath() if err != nil { return err } } var err error db, err = sql.Open("sqlite3", dbPath+"?_journal=WAL&_timeout=5000") if err != nil { return fmt.Errorf("failed to open database: %w", err) } // Set connection pool settings db.SetMaxOpenConns(1) // SQLite only supports one writer db.SetMaxIdleConns(1) db.SetConnMaxLifetime(time.Hour) // Create tables if err := createTables(); err != nil { db.Close() db = nil return fmt.Errorf("failed to create tables: %w", err) } return nil } // Close closes the database connection func Close() error { dbMu.Lock() defer dbMu.Unlock() if db == nil { return nil } err := db.Close() db = nil return err } // isInitialized returns true if the database is ready func isInitialized() bool { dbMu.RLock() defer dbMu.RUnlock() return db != nil } // createTables creates all required database tables 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 := db.Exec(schema) return err } // Cleanup removes old data based on retention settings func Cleanup(retentionDays int) error { dbMu.RLock() defer dbMu.RUnlock() if db == nil { return nil } cutoff := time.Now().AddDate(0, 0, -retentionDays) _, err := db.Exec(` DELETE FROM hashrate_history WHERE timestamp < ? `, cutoff) return err } // vacuumDB optimizes the database file size func vacuumDB() error { dbMu.RLock() defer dbMu.RUnlock() if db == nil { return nil } _, err := db.Exec("VACUUM") return err }