This repository has been archived on 2026-09-05. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
gotoolkit/database/migrations.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
}