93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
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
|
|
}
|