Skip to content
Snippets Groups Projects
migrations.go 3.83 KiB
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"
)

//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
// at all
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
// server version
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
	}
	return m, nil
}

// AutoMigrate brings the db up-to-date and logs a warning if it needed some
// changes
func AutoMigrate(dsn string, sourceDriver source.Driver, log zerolog.Logger) error {
	err := IsUptodate(dsn, sourceDriver)
	if err == nil {
		return nil
	}
	if _, ok := err.(ErrDBNeedUpgrade); 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 {
		return fmt.Errorf("failed to init migration engine: %s", err)
	}
	defer m.Close()

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

// 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 {
		return fmt.Errorf("failed to check database version: %s", err)
	}

	// Lookup the last available db version
	lastVersion, err := sourceDriver.First()
	if err != nil {
		return err
	}
	for {
		next, err := sourceDriver.Next(lastVersion)
		if pathError, ok := err.(*os.PathError); err == os.ErrNotExist || ok && pathError.Err == os.ErrNotExist {
			break
		} else if err != nil {
			return err
		}
		lastVersion = next
	}

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

	if version < lastVersion {
		return ErrDBNeedUpgrade{
			CurrentVersion:  version,
			RequiredVersion: lastVersion,
		}
	}
	if version > lastVersion {
		return ErrDBFutureVersion{
			CurrentVersion:  version,
			RequiredVersion: lastVersion,
		}
	}
	return nil
}