package db import ( "database/sql" "fmt" "log" _ "github.com/mattn/go-sqlite3" ) // New opens a SQLite database at the given path and runs schema migrations. func New(path string) (*sql.DB, error) { db, err := sql.Open("sqlite3", path+"?_journal_mode=WAL&_foreign_keys=on") if err != nil { return nil, fmt.Errorf("open db: %w", err) } if err := db.Ping(); err != nil { return nil, fmt.Errorf("ping db: %w", err) } if err := migrate(db); err != nil { return nil, fmt.Errorf("migrate db: %w", err) } log.Println("database ready:", path) return db, nil } func migrate(db *sql.DB) error { schema := ` CREATE TABLE IF NOT EXISTS players ( id INTEGER PRIMARY KEY AUTOINCREMENT, client_id TEXT NOT NULL UNIQUE, first_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), total_sessions INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_players_client_id ON players(client_id); CREATE TABLE IF NOT EXISTS player_ips ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE, ip_address TEXT NOT NULL, first_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), UNIQUE(player_id, ip_address) ); CREATE INDEX IF NOT EXISTS idx_player_ips_ip ON player_ips(ip_address); CREATE TABLE IF NOT EXISTS game_sessions ( id TEXT PRIMARY KEY, player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE, score INTEGER NOT NULL DEFAULT 0, level_reached INTEGER NOT NULL DEFAULT 1, lives_used INTEGER NOT NULL DEFAULT 0, duration_seconds INTEGER NOT NULL DEFAULT 0, end_reason TEXT NOT NULL DEFAULT 'death', started_at TEXT NOT NULL, ended_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_game_sessions_score ON game_sessions(score DESC); CREATE INDEX IF NOT EXISTS idx_game_sessions_player_ended ON game_sessions(player_id, ended_at DESC); CREATE TABLE IF NOT EXISTS device_infos ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL UNIQUE REFERENCES game_sessions(id) ON DELETE CASCADE, ip_address TEXT NOT NULL DEFAULT '', user_agent TEXT NOT NULL DEFAULT '', platform TEXT NOT NULL DEFAULT '', language TEXT NOT NULL DEFAULT '', screen_width INTEGER, screen_height INTEGER, device_pixel_ratio REAL, timezone TEXT NOT NULL DEFAULT '', webgl_renderer TEXT NOT NULL DEFAULT '', touch_support INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS high_scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL UNIQUE REFERENCES players(id) ON DELETE CASCADE, score INTEGER NOT NULL, session_id TEXT REFERENCES game_sessions(id) ON DELETE SET NULL, achieved_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_high_scores_score ON high_scores(score DESC); ` _, err := db.Exec(schema) return err }