autobrr/internal/database/sqlite.go
ze0s 604c7896bd
chore: add LICENSE GPLv2-or-later (#897)
* chore: add LICENSE

* chore: add LICENSE to README
2023-05-01 16:21:59 +02:00

210 lines
5.8 KiB
Go

// Copyright (c) 2021 - 2023, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later
package database
import (
"database/sql"
"fmt"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/lib/pq"
_ "modernc.org/sqlite"
)
func (db *DB) openSQLite() error {
if db.DSN == "" {
return errors.New("DSN required")
}
var err error
// open database connection
if db.handler, err = sql.Open("sqlite", db.DSN+"?_pragma=busy_timeout%3d1000"); err != nil {
db.log.Fatal().Err(err).Msg("could not open db connection")
return err
}
// Set busy timeout
//if _, err = db.handler.Exec(`PRAGMA busy_timeout = 5000;`); err != nil {
// return errors.New("busy timeout pragma: %w", err)
//}
// Enable WAL. SQLite performs better with the WAL because it allows
// multiple readers to operate while data is being written.
if _, err = db.handler.Exec(`PRAGMA journal_mode = wal;`); err != nil {
return errors.Wrap(err, "enable wal")
}
// When Autobrr does not cleanly shutdown, the WAL will still be present and not committed.
// This is a no-op if the WAL is empty, and a commit when the WAL is not to start fresh.
// When commits hit 1000, PRAGMA wal_checkpoint(PASSIVE); is invoked which tries its best
// to commit from the WAL (and can fail to commit all pending operations).
// Forcing a PRAGMA wal_checkpoint(RESTART); in the future on a "quiet period" could be
// considered.
if _, err = db.handler.Exec(`PRAGMA wal_checkpoint(TRUNCATE);`); err != nil {
return errors.Wrap(err, "commit wal")
}
// Enable foreign key checks. For historical reasons, SQLite does not check
// foreign key constraints by default. There's some overhead on inserts to
// verify foreign key integrity, but it's definitely worth it.
//if _, err = db.handler.Exec(`PRAGMA foreign_keys = ON;`); err != nil {
// return errors.New("foreign keys pragma: %w", err)
//}
// migrate db
if err = db.migrateSQLite(); err != nil {
db.log.Fatal().Err(err).Msg("could not migrate db")
return err
}
return nil
}
func (db *DB) migrateSQLite() error {
db.lock.Lock()
defer db.lock.Unlock()
var version int
if err := db.handler.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
return errors.Wrap(err, "failed to query schema version")
}
if version == len(sqliteMigrations) {
return nil
} else if version > len(sqliteMigrations) {
return errors.New("autobrr (version %d) older than schema (version: %d)", len(sqliteMigrations), version)
}
db.log.Info().Msgf("Beginning database schema upgrade from version %v to version: %v", version, len(sqliteMigrations))
tx, err := db.handler.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if version == 0 {
if _, err := tx.Exec(sqliteSchema); err != nil {
return errors.Wrap(err, "failed to initialize schema")
}
} else {
for i := version; i < len(sqliteMigrations); i++ {
db.log.Info().Msgf("Upgrading Database schema to version: %v", i)
if _, err := tx.Exec(sqliteMigrations[i]); err != nil {
return errors.Wrap(err, "failed to execute migration #%v", i)
}
}
}
// temp custom data migration
// get data from filter.sources, check if specific types, move to new table and clear
// if migration 6
// TODO 2022-01-30 remove this in future version
if version == 5 && len(sqliteMigrations) == 6 {
if err := customMigrateCopySourcesToMedia(tx); err != nil {
return errors.Wrap(err, "could not run custom data migration")
}
}
_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", len(sqliteMigrations)))
if err != nil {
return errors.Wrap(err, "failed to bump schema version")
}
db.log.Info().Msgf("Database schema upgraded to version: %v", len(sqliteMigrations))
return tx.Commit()
}
// customMigrateCopySourcesToMedia move music specific sources to media
func customMigrateCopySourcesToMedia(tx *sql.Tx) error {
rows, err := tx.Query(`
SELECT id, sources
FROM filter
WHERE sources LIKE '%"CD"%'
OR sources LIKE '%"WEB"%'
OR sources LIKE '%"DVD"%'
OR sources LIKE '%"Vinyl"%'
OR sources LIKE '%"Soundboard"%'
OR sources LIKE '%"DAT"%'
OR sources LIKE '%"Cassette"%'
OR sources LIKE '%"Blu-Ray"%'
OR sources LIKE '%"SACD"%'
;`)
if err != nil {
return errors.Wrap(err, "could not run custom data migration")
}
defer rows.Close()
type tmpDataStruct struct {
id int
sources []string
}
var tmpData []tmpDataStruct
// scan data
for rows.Next() {
var t tmpDataStruct
if err := rows.Scan(&t.id, pq.Array(&t.sources)); err != nil {
return err
}
tmpData = append(tmpData, t)
}
if err := rows.Err(); err != nil {
return err
}
// manipulate data
for _, d := range tmpData {
// create new slice with only music source if they exist in d.sources
mediaSources := []string{}
for _, source := range d.sources {
switch source {
case "CD":
mediaSources = append(mediaSources, source)
case "DVD":
mediaSources = append(mediaSources, source)
case "Vinyl":
mediaSources = append(mediaSources, source)
case "Soundboard":
mediaSources = append(mediaSources, source)
case "DAT":
mediaSources = append(mediaSources, source)
case "Cassette":
mediaSources = append(mediaSources, source)
case "Blu-Ray":
mediaSources = append(mediaSources, source)
case "SACD":
mediaSources = append(mediaSources, source)
}
}
_, err = tx.Exec(`UPDATE filter SET media = ? WHERE id = ?`, pq.Array(mediaSources), d.id)
if err != nil {
return err
}
// remove all music specific sources
cleanSources := []string{}
for _, source := range d.sources {
switch source {
case "CD", "WEB", "DVD", "Vinyl", "Soundboard", "DAT", "Cassette", "Blu-Ray", "SACD":
continue
}
cleanSources = append(cleanSources, source)
}
_, err := tx.Exec(`UPDATE filter SET sources = ? WHERE id = ?`, pq.Array(cleanSources), d.id)
if err != nil {
return err
}
}
return nil
}