Skip to content
Snippets Groups Projects
migrations.go 3.9 KiB
Newer Older
package database

import (
	"database/sql"
	"errors"
	"fmt"
	"os"

	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database/postgres"
	"github.com/golang-migrate/migrate/v4/source"
	"github.com/rs/zerolog"
)

Axel Prel's avatar
Axel Prel committed
//nolint:dupword
//go:generate go-bindata -pkg database -prefix migrations migrations
//go:generate sed -i "1s;^;// Code generated by go-bindata. DO NOT EDIT.\\n\\n;" bindata.go

// ErrDBNotVersioned is returned by IsUptodate if the database is not versionned
var ErrDBNotVersioned = errors.New(
	"the database is not versionned, please run the 'migrate' command")

// ErrDBNeedUpgrade is returned if the database is not up-to-date with the
type ErrDBNeedUpgrade struct {
	CurrentVersion  uint
	RequiredVersion uint
}

// Error returns the formatted error message.
func (e ErrDBNeedUpgrade) Error() string {
	return fmt.Sprintf("Database version is too old. Please run '%s migrate'."+
		" Required version: %d, current version: %d",
		os.Args[0], e.RequiredVersion, e.CurrentVersion,
	)
}

// ErrDBFutureVersion is returned if the database is more recent than the server.
// It generally means the server is not at the right version.
type ErrDBFutureVersion struct {
	CurrentVersion  uint
	RequiredVersion uint
}

// Error returns the formatted error message.
func (e ErrDBFutureVersion) Error() string {
	return fmt.Sprintf("Database version is too new. It probably means your "+
		"version of '%s' is too old."+
		" Required version: %d, current version: %d",
		os.Args[0], e.RequiredVersion, e.CurrentVersion,
	)
}

// NewMigrate initializes a github.com/golang-migrate/migrate/v4.Migrate for
// the given database options.
func NewMigrate(dsn string, sourceDriver source.Driver) (*migrate.Migrate, error) {
	db, err := sql.Open("postgres", dsn)
	if err != nil {
		panic(err)
	}

	driver, err := postgres.WithInstance(db, &postgres.Config{})
	if err != nil {
		return nil, err
	}
	m, err := migrate.NewWithInstance(
		"go-bindata", sourceDriver,
		"postgres", driver,
	)
	if err != nil {
		return nil, err
	}
Axel Prel's avatar
Axel Prel committed

	return m, nil
}

// AutoMigrate brings the db up-to-date and logs a warning if it needed some
func AutoMigrate(dsn string, sourceDriver source.Driver, log zerolog.Logger) error {
	err := IsUptodate(dsn, sourceDriver)
	if err == nil {
		return nil
	}
Axel Prel's avatar
Axel Prel committed
	var upgradeErr ErrDBNeedUpgrade
	if ok := errors.As(err, &upgradeErr); !errors.Is(err, ErrDBNotVersioned) == !ok {
		return err
	}
	log.Warn().Msg("Database is not up-to-date, it will be migrated automatically")
	m, err := NewMigrate(dsn, sourceDriver)
	if err != nil {
Axel Prel's avatar
Axel Prel committed
		return fmt.Errorf("failed to init migration engine: %w", err)
	}
	defer m.Close()

	if err := m.Up(); err != nil {
Axel Prel's avatar
Axel Prel committed
		return fmt.Errorf("error during auto-migration: %w", err)
	}
	log.Info().Msg("Successfully upgraded database")
Axel Prel's avatar
Axel Prel committed

// IsUptodate returns nil if the database version is up to date.
func IsUptodate(dsn string, sourceDriver source.Driver) error {
	m, err := NewMigrate(dsn, sourceDriver)
	if err != nil {
Axel Prel's avatar
Axel Prel committed
		return fmt.Errorf("failed to check database version: %w", err)
	}

	// Lookup the last available db version
	lastVersion, err := sourceDriver.First()
	if err != nil {
		return err
	}
	for {
		next, err := sourceDriver.Next(lastVersion)
Axel Prel's avatar
Axel Prel committed
		var pathErr *os.PathError
		if errors.As(err, &pathErr) && errors.Is(err, os.ErrNotExist) {
			break
		} else if err != nil {
			return err
		}
		lastVersion = next
	}

	version, dirty, err := m.Version()
Axel Prel's avatar
Axel Prel committed
	if errors.Is(err, migrate.ErrNilVersion) {
		return ErrDBNotVersioned
	}
	if err != nil {
Axel Prel's avatar
Axel Prel committed
		return fmt.Errorf("error while checking the database version: %w", err)
Axel Prel's avatar
Axel Prel committed
		return errors.New("database is marked 'dirty'")
	}

	if version < lastVersion {
		return ErrDBNeedUpgrade{
			CurrentVersion:  version,
			RequiredVersion: lastVersion,
		}
	}
	if version > lastVersion {
		return ErrDBFutureVersion{
			CurrentVersion:  version,
			RequiredVersion: lastVersion,
		}
	}
Axel Prel's avatar
Axel Prel committed