mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-10 11:27:11 +02:00
Replace the 131 SQLite migrations with a single goose migration that creates the whole schema in PostgreSQL (the user table becomes user_account, json columns become jsonb, seed rows for the default library and transcodings). Port the repositories' SQL just far enough for Navidrome to start, scan, log in and browse artists and albums: json_agg/jsonb_* for the SQLite JSON functions, lower() for COLLATE NOCASE, ON CONFLICT DO NOTHING, coalesce on annotations, stricter GROUP BY, explicit casts under the simple protocol, sequential queries in library.RefreshStats. db/ is now Postgres-only, with backup/restore stubbed and DevExternalScanner forced off. Embed the PGlite module like go-taglib: pglite.wasi.gz is go:embed'ed, decompressed in memory and compiled by wazero with an on-disk compilation cache, so the module itself is never written to disk. The cluster is created by initdb at runtime on the first start; initdb runs in a throwaway wasm instance and the backend in a fresh one, otherwise the next start cannot find a valid checkpoint. Since initdb creates only template1, a short-lived backend then runs CREATE DATABASE navidrome and the real backend starts on it. The build recipe and patches for the -O2, no-wizer module live in db/pglite/build.
178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/db/pglite"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/utils/singleton"
|
|
"github.com/pressly/goose/v3"
|
|
)
|
|
|
|
var (
|
|
Dialect = "postgres"
|
|
Driver = "pgx"
|
|
Path string
|
|
)
|
|
|
|
//go:embed migrations/*.sql
|
|
var embedMigrations embed.FS
|
|
|
|
const migrationsFolder = "migrations"
|
|
|
|
// embedded is the in-process PGlite instance backing Db().
|
|
var embedded *pglite.PGlite
|
|
|
|
func Db() *sql.DB {
|
|
return singleton.GetInstance(func() *sql.DB {
|
|
Path = conf.Server.DbPath
|
|
dataDir, ok := strings.CutPrefix(Path, "pglite://")
|
|
if !ok {
|
|
log.Fatal("DbPath must be a pglite://<dir> location in this build", "dbPath", Path)
|
|
}
|
|
socketDir := filepath.Dir(dataDir)
|
|
logFile, _ := os.Create(filepath.Join(socketDir, "pglite.log"))
|
|
pg, err := pglite.New(context.Background(), pglite.Config{
|
|
DataDir: dataDir, Stderr: logFile, SocketDir: socketDir,
|
|
CacheDir: filepath.Join(conf.Server.CacheFolder.String(), "pglite"),
|
|
})
|
|
if err != nil {
|
|
log.Fatal("Error starting embedded PGlite", err)
|
|
}
|
|
embedded = pg
|
|
db, err := pg.OpenDB()
|
|
if err != nil {
|
|
log.Fatal("Error opening database", err)
|
|
}
|
|
// One PGlite backend means one PG session; the bridge serializes clients onto it.
|
|
db.SetMaxOpenConns(4)
|
|
abs, _ := filepath.Abs(socketDir)
|
|
log.Info("Embedded PGlite ready", "psql", "PGPASSWORD=x PGSSLMODE=disable psql -h "+abs+" -U postgres navidrome")
|
|
return db
|
|
})
|
|
}
|
|
|
|
func Close(ctx context.Context) {
|
|
// Ignore cancellations when closing the DB
|
|
ctx = context.WithoutCancel(ctx)
|
|
|
|
log.Info(ctx, "Closing Database")
|
|
if err := Db().Close(); err != nil {
|
|
log.Error(ctx, "Error closing Database", err)
|
|
}
|
|
if embedded != nil {
|
|
_ = embedded.Close()
|
|
}
|
|
}
|
|
|
|
func Init(ctx context.Context) func() {
|
|
db := Db()
|
|
|
|
goose.SetBaseFS(embedMigrations)
|
|
if err := goose.SetDialect(Dialect); err != nil {
|
|
log.Fatal(ctx, "Invalid DB driver", "driver", Driver, err)
|
|
}
|
|
schemaEmpty := isSchemaEmpty(ctx, db)
|
|
hasSchemaChanges := hasPendingMigrations(ctx, db, migrationsFolder)
|
|
if !schemaEmpty && hasSchemaChanges {
|
|
log.Info(ctx, "Upgrading DB Schema to latest version")
|
|
}
|
|
goose.SetLogger(&logAdapter{ctx: ctx, silent: schemaEmpty})
|
|
if err := goose.UpContext(ctx, db, migrationsFolder); err != nil {
|
|
log.Fatal(ctx, "Failed to apply new migrations", err)
|
|
}
|
|
|
|
if hasSchemaChanges {
|
|
log.Debug(ctx, "Running ANALYZE after schema changes")
|
|
if err := optimizeAt(ctx, db, time.Now()); err != nil {
|
|
log.Error(ctx, "Error running ANALYZE", err)
|
|
}
|
|
}
|
|
|
|
return func() {
|
|
Close(ctx)
|
|
}
|
|
}
|
|
|
|
// SQLState reports the PostgreSQL error code carried by err, if any.
|
|
func SQLState(err error) (string, bool) {
|
|
var pgErr *pgconn.PgError
|
|
if !errors.As(err, &pgErr) {
|
|
return "", false
|
|
}
|
|
return pgErr.Code, true
|
|
}
|
|
|
|
type statusLogger struct{ numPending int }
|
|
|
|
func (*statusLogger) Fatalf(format string, v ...any) { log.Fatal(fmt.Sprintf(format, v...)) }
|
|
func (l *statusLogger) Printf(format string, v ...any) {
|
|
if len(v) < 1 {
|
|
return
|
|
}
|
|
if v0, ok := v[0].(string); !ok {
|
|
return
|
|
} else if v0 == "Pending" {
|
|
l.numPending++
|
|
}
|
|
}
|
|
|
|
func hasPendingMigrations(ctx context.Context, db *sql.DB, folder string) bool {
|
|
l := &statusLogger{}
|
|
goose.SetLogger(l)
|
|
err := goose.StatusContext(ctx, db, folder)
|
|
if err != nil {
|
|
log.Fatal(ctx, "Failed to check for pending migrations", err)
|
|
}
|
|
return l.numPending > 0
|
|
}
|
|
|
|
func isSchemaEmpty(ctx context.Context, db *sql.DB) bool {
|
|
var found *string
|
|
err := db.QueryRowContext(ctx, "SELECT to_regclass('public.goose_db_version')::text").Scan(&found)
|
|
if err != nil {
|
|
log.Fatal(ctx, "Database could not be opened!", err)
|
|
}
|
|
return found == nil
|
|
}
|
|
|
|
type logAdapter struct {
|
|
ctx context.Context
|
|
silent bool
|
|
}
|
|
|
|
func (l *logAdapter) Fatal(v ...any) {
|
|
log.Fatal(l.ctx, fmt.Sprint(v...))
|
|
}
|
|
|
|
func (l *logAdapter) Fatalf(format string, v ...any) {
|
|
log.Fatal(l.ctx, fmt.Sprintf(format, v...))
|
|
}
|
|
|
|
func (l *logAdapter) Print(v ...any) {
|
|
if !l.silent {
|
|
log.Info(l.ctx, fmt.Sprint(v...))
|
|
}
|
|
}
|
|
|
|
func (l *logAdapter) Println(v ...any) {
|
|
if !l.silent {
|
|
log.Info(l.ctx, fmt.Sprintln(v...))
|
|
}
|
|
}
|
|
|
|
func (l *logAdapter) Printf(format string, v ...any) {
|
|
if !l.silent {
|
|
log.Info(l.ctx, fmt.Sprintf(format, v...))
|
|
}
|
|
}
|