package model import ( "database/sql" "fmt" "log" "os" "path/filepath" "strings" "time" _ "modernc.org/sqlite" ) // Database manages the app database. type Database struct { db *sql.DB } // NewDatabase creates a new database connection. func NewDatabase() (*Database, error) { dbPath := getDatabasePath() log.Printf("[db] init: path=%s", dbPath) // Ensure directory exists. dir := filepath.Dir(dbPath) if err := os.MkdirAll(dir, 0755); err != nil { log.Printf("[db] mkdir failed: dir=%s err=%v", dir, err) return nil, fmt.Errorf("create database dir failed: %w", err) } // Connect database. db, err := sql.Open("sqlite", dbPath) if err != nil { log.Printf("[db] open failed: path=%s err=%v", dbPath, err) return nil, fmt.Errorf("open database failed: %w", err) } // Test connection. if err := db.Ping(); err != nil { log.Printf("[db] ping failed: err=%v", err) return nil, fmt.Errorf("database ping failed: %w", err) } database := &Database{db: db} // Init tables. if err := database.initTables(); err != nil { log.Printf("[db] init tables failed: err=%v", err) return nil, fmt.Errorf("init tables failed: %w", err) } log.Printf("[db] init ok") return database, nil } // Close closes the database connection. func (d *Database) Close() error { return d.db.Close() } // getDatabasePath returns the database file path. func getDatabasePath() string { homeDir, err := os.UserHomeDir() if err != nil { return "equipment_analyzer.db" } return filepath.Join(homeDir, ".equipment-analyzer", "equipment_analyzer.db") } // initTables creates tables if not exist. func (d *Database) initTables() error { parsedDataTable := ` CREATE TABLE IF NOT EXISTS parsed_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_name TEXT NOT NULL, items_json TEXT NOT NULL, heroes_json TEXT NOT NULL, geartxt TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL );` settingsTable := ` CREATE TABLE IF NOT EXISTS app_settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL );` tables := []string{ parsedDataTable, settingsTable, } for _, table := range tables { if _, err := d.db.Exec(table); err != nil { return fmt.Errorf("create table failed: %w", err) } } if err := d.ensureParsedDataColumns(); err != nil { return err } return nil } func (d *Database) ensureParsedDataColumns() error { _, err := d.db.Exec("ALTER TABLE parsed_data ADD COLUMN geartxt TEXT NOT NULL DEFAULT ''") if err != nil { if strings.Contains(err.Error(), "duplicate column name") { return nil } return fmt.Errorf("add geartxt column failed: %w", err) } return nil } // SaveParsedData saves parsed items/heroes with raw gear txt. func (d *Database) SaveParsedData(sessionName string, itemsJSON, heroesJSON, gearTxt string) error { stmt := ` INSERT INTO parsed_data (session_name, items_json, heroes_json, geartxt, created_at) VALUES (?, ?, ?, ?, ?)` _, err := d.db.Exec(stmt, sessionName, itemsJSON, heroesJSON, gearTxt, time.Now().Unix()) return err } // GetLatestParsedData returns latest parsed data. func (d *Database) GetLatestParsedData() (string, string, string, error) { stmt := ` SELECT items_json, heroes_json, geartxt FROM parsed_data ORDER BY created_at DESC LIMIT 1` var itemsJSON, heroesJSON, gearTxt string err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON, &gearTxt) if err != nil { if err == sql.ErrNoRows { return "", "", "", nil } return "", "", "", err } return itemsJSON, heroesJSON, gearTxt, nil } // GetParsedSessions returns all parsed sessions. func (d *Database) GetParsedSessions() ([]ParsedSession, error) { stmt := ` SELECT id, session_name, created_at FROM parsed_data ORDER BY created_at DESC` rows, err := d.db.Query(stmt) if err != nil { return nil, err } defer rows.Close() sessions := make([]ParsedSession, 0) for rows.Next() { var s ParsedSession if err := rows.Scan(&s.ID, &s.SessionName, &s.CreatedAt); err != nil { return nil, err } sessions = append(sessions, s) } return sessions, nil } // GetParsedDataByID returns parsed data for a session. func (d *Database) GetParsedDataByID(id int64) (string, string, string, error) { stmt := ` SELECT items_json, heroes_json, geartxt FROM parsed_data WHERE id = ? LIMIT 1` var itemsJSON, heroesJSON, gearTxt string err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON, &gearTxt) if err != nil { if err == sql.ErrNoRows { return "", "", "", nil } return "", "", "", err } return itemsJSON, heroesJSON, gearTxt, nil } // UpdateParsedSessionName updates session name. func (d *Database) UpdateParsedSessionName(id int64, name string) error { stmt := ` UPDATE parsed_data SET session_name = ? WHERE id = ?` _, err := d.db.Exec(stmt, name, id) return err } // DeleteParsedSession deletes a parsed session. func (d *Database) DeleteParsedSession(id int64) error { stmt := ` DELETE FROM parsed_data WHERE id = ?` _, err := d.db.Exec(stmt, id) return err } // SaveSetting saves app setting. func (d *Database) SaveSetting(key, value string) error { stmt := "INSERT OR REPLACE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)" _, err := d.db.Exec(stmt, key, value, time.Now().Unix()) return err } // GetSetting returns app setting. func (d *Database) GetSetting(key string) (string, error) { stmt := "SELECT value FROM app_settings WHERE key = ?" var value string err := d.db.QueryRow(stmt, key).Scan(&value) if err != nil { return "", err } return value, nil } // GetAllSettings returns all settings. func (d *Database) GetAllSettings() (map[string]string, error) { stmt := "SELECT key, value FROM app_settings" rows, err := d.db.Query(stmt) if err != nil { return nil, err } defer rows.Close() settings := make(map[string]string) for rows.Next() { var key, value string if err := rows.Scan(&key, &value); err != nil { return nil, err } settings[key] = value } return settings, nil }