shiori/internal/database/migrations.go
Felipe Martin 876d27f337
refactor: remove gin and use stdlib http server (#1064)
* refactor: base http server stdlib

* refactor: swagger and frontend routes

* fix: use global middlewares

* refactor: removed gin from testutils

* fix: object references in legacy webserver

* refactor: legacy, swagger and system handlers

* fix: added verbs to handlers

* fix: server handlers ordering

* refactor: bookmarks handlers

* refactor: system api routes

* tests: bookmark handlers

* refactor: migrated api auth routes

* chore: remove unused middlewares

* docs: add swagger docs to refactored system api

* chore: remove old auth routes

* refactor: account apis

* chore: removed old handlers

* fix: api v1 handlers missing middlewares

* refactor: migrated tag list route

* refactor: bookmark routes

* refactor: remove gin

* chore: make styles

* test: fixed tests

* test: generate binary file without text

* fix: global middleware missing from system api handler

* fix: incorrect api handler

* chore: avoid logging screenshot contents

* tests: bookmarks domain

* tests: shortcuts

* test: missing tests

* tests: server tests

* test: remove test using syscall to avoid windows errors

* chore: added middlewares
2025-02-26 20:50:48 +01:00

100 lines
2.8 KiB
Go

// Package database implements database operations and migrations
package database
import (
"context"
"database/sql"
"embed"
"fmt"
"path"
"github.com/blang/semver"
"github.com/go-shiori/shiori/internal/model"
)
//go:embed migrations/*
var migrationFiles embed.FS
// migration represents a database schema migration
type migration struct {
fromVersion semver.Version
toVersion semver.Version
migrationFunc func(db *sql.DB) error
}
// txFn is a function that runs in a transaction.
type txFn func(tx *sql.Tx) error
// runInTransaction runs the given function in a transaction.
func runInTransaction(db *sql.DB, fn txFn) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return fmt.Errorf("failed to run transaction: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
// newFuncMigration creates a new migration from a function.
func newFuncMigration(fromVersion, toVersion string, migrationFunc func(db *sql.DB) error) migration {
return migration{
fromVersion: semver.MustParse(fromVersion),
toVersion: semver.MustParse(toVersion),
migrationFunc: migrationFunc,
}
}
// newFileMigration creates a new migration from a file.
func newFileMigration(fromVersion, toVersion, filename string) migration {
return newFuncMigration(fromVersion, toVersion, func(db *sql.DB) error {
return runInTransaction(db, func(tx *sql.Tx) error {
migrationSQL, err := migrationFiles.ReadFile(path.Join("migrations", filename+".up.sql"))
if err != nil {
return fmt.Errorf("failed to read migration file: %w", err)
}
if _, err := tx.Exec(string(migrationSQL)); err != nil {
return fmt.Errorf("failed to execute migration %s to %s: %w", fromVersion, toVersion, err)
}
return nil
})
})
}
// runMigrations runs the given migrations.
func runMigrations(ctx context.Context, db model.DB, migrations []migration) error {
currentVersion := semver.Version{}
// Get current database version
dbVersion, err := db.GetDatabaseSchemaVersion(ctx)
if err == nil && dbVersion != "" {
currentVersion = semver.MustParse(dbVersion)
}
for _, migration := range migrations {
if !currentVersion.EQ(migration.fromVersion) {
continue
}
if err := migration.migrationFunc(db.WriterDB().DB); err != nil {
return fmt.Errorf("failed to run migration from %s to %s: %w", migration.fromVersion, migration.toVersion, err)
}
currentVersion = migration.toVersion
if err := db.SetDatabaseSchemaVersion(ctx, currentVersion.String()); err != nil {
return fmt.Errorf("failed to store database version %s from %s to %s: %w", currentVersion.String(), migration.fromVersion, migration.toVersion, err)
}
}
return nil
}