123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"embed"
|
|
"fmt"
|
|
"path"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"git.nakama.town/fmartingr/gotoolkit/model"
|
|
)
|
|
|
|
// Version is a type alias for semver.Version for convenience
|
|
type Version = semver.Version
|
|
|
|
// ParseVersion parses a version string
|
|
func ParseVersion(v string) (Version, error) {
|
|
return semver.Parse(v)
|
|
}
|
|
|
|
// Migration represents a database schema migration
|
|
type Migration struct {
|
|
FromVersion Version
|
|
ToVersion 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, error) {
|
|
from, err := semver.Parse(fromVersion)
|
|
if err != nil {
|
|
return Migration{}, fmt.Errorf("invalid from version: %w", err)
|
|
}
|
|
|
|
to, err := semver.Parse(toVersion)
|
|
if err != nil {
|
|
return Migration{}, fmt.Errorf("invalid to version: %w", err)
|
|
}
|
|
|
|
return Migration{
|
|
FromVersion: from,
|
|
ToVersion: to,
|
|
MigrationFunc: migrationFunc,
|
|
}, nil
|
|
}
|
|
|
|
// NewFileMigration creates a new migration from an embedded file system.
|
|
func NewFileMigration(fromVersion, toVersion, filename string, migrationFiles embed.FS) (Migration, error) {
|
|
migration, err := 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
|
|
})
|
|
})
|
|
if err != nil {
|
|
return Migration{}, err
|
|
}
|
|
|
|
return migration, nil
|
|
}
|
|
|
|
// RunMigrations runs the given migrations on the database.
|
|
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, err = semver.Parse(dbVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse current database version %s: %w", dbVersion, err)
|
|
}
|
|
}
|
|
|
|
for _, migration := range migrations {
|
|
if !currentVersion.EQ(migration.FromVersion) {
|
|
continue
|
|
}
|
|
|
|
if err := migration.MigrationFunc(db.WriterDB()); 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: %w", currentVersion.String(), err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|