1
0
dwelling-radio/internal/statistics/db/sqlite/db.go

192 lines
3.9 KiB
Go

package sqlite
import (
"database/sql"
"dwelling-radio/internal/radio"
"dwelling-radio/internal/statistics"
_ "embed"
"fmt"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/errors"
)
const dbDateFormat = "2006-01-02 15:04:05.999"
var (
//go:embed queries/schema.sql
querySchema string
//go:embed queries/song_add.sql
querySongAdd string
//go:embed queries/history_add.sql
queryHistoryAdd string
//go:embed queries/last_n_songs.sql
queryLastNSongs string
//go:embed queries/most_popular_songs.sql
queryMostPopularSongs string
//go:embed queries/most_simultaneous_listeners.sql
queryMostSimultaneousListeners string
)
var (
stmtSongAdd *sql.Stmt
stmtHistoryAdd *sql.Stmt
stmtLastNSongs *sql.Stmt
stmtMostPopularSongs *sql.Stmt
stmtMostSimultaneousListeners *sql.Stmt
)
type SQLiteStatistics struct {
db *sql.DB
}
func initDBStatements(db *sql.DB) error {
db.Exec("PRAGMA foreign_keys = ON;")
_, err := db.Exec(querySchema)
if err != nil {
return errors.Wrap(err, "failed to init schema")
}
stmtSongAdd, err = db.Prepare(querySongAdd)
if err != nil {
return errors.Wrap(err, "failed to prepare querySongAdd")
}
stmtHistoryAdd, err = db.Prepare(queryHistoryAdd)
if err != nil {
return errors.Wrap(err, "failed to prepare queryHistoryAdd")
}
stmtLastNSongs, err = db.Prepare(queryLastNSongs)
if err != nil {
return errors.Wrap(err, "failed to prepare queryLastNSongs")
}
stmtMostPopularSongs, err = db.Prepare(queryMostPopularSongs)
if err != nil {
return errors.Wrap(err, "failed to prepare queryMostPopularSongs")
}
stmtMostSimultaneousListeners, err = db.Prepare(queryMostSimultaneousListeners)
if err != nil {
return errors.Wrap(err, "failed to prepare queryMostSimultaneousListeners")
}
return nil
}
func New(path string) (statistics.Statistics, error) {
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_journal=WAL&_mutex=full", path))
if err != nil {
return nil, err
}
if err := initDBStatements(db); err != nil {
return nil, err
}
return &SQLiteStatistics{db: db}, nil
}
func (s *SQLiteStatistics) Add(song *radio.Song) error {
if song == nil || song.Artist == "" || song.Title == "" {
return errors.New("No song or an empty one was passed.")
}
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
row := tx.Stmt(stmtSongAdd).QueryRow(song.Artist, song.Title)
if row.Err() != nil {
return row.Err()
}
var songID int64
if err := row.Scan(&songID); err != nil {
return err
}
res, err := tx.Stmt(stmtHistoryAdd).Exec(song.StartAt.UTC().Format(dbDateFormat),
songID, song.Listeners, song.MaxListeners)
if err != nil {
return err
} else if ra, err := res.RowsAffected(); ra == 0 || err != nil {
return errors.New("a song wasn't added to history, but there were no errors")
}
tx.Commit()
return nil
}
func (s *SQLiteStatistics) LastNSongs(n int64) ([]radio.Song, error) {
if n == 0 {
return nil, nil
}
tx, err := s.db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()
rows, err := tx.Stmt(stmtLastNSongs).Query(n)
if err != nil {
return nil, err
}
songs := make([]radio.Song, n)
i := 0
for rows.Next() {
var startAt string
if err := rows.Scan(&startAt, &songs[i].Artist, &songs[i].Title,
&songs[i].Listeners, &songs[i].MaxListeners); err != nil {
return nil, err
}
songs[i].StartAt, err = time.Parse(dbDateFormat, startAt)
if err != nil {
return nil, err
}
i++
}
tx.Commit()
if i == 0 {
return nil, nil
}
lst := make([]radio.Song, i)
copy(lst, songs[:])
return lst, nil
}
func (s *SQLiteStatistics) MostNPopularSongs(n int64) ([]radio.Song, error) {
if n == 0 {
return nil, nil
}
return nil, nil
}
func (s *SQLiteStatistics) MostSimultaneousListeners() (radio.Song, error) {
return radio.Song{}, nil
}
func (s *SQLiteStatistics) Close() error {
return s.db.Close()
}