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"
)
//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
// 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
func AutoMigrate(dsn string, sourceDriver source.Driver, log zerolog.Logger) error {
err := IsUptodate(dsn, sourceDriver)
if err == nil {
return nil
}
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 {
return fmt.Errorf("failed to init migration engine: %w", err)
}
defer m.Close()
if err := m.Up(); err != nil {
}
log.Info().Msg("Successfully upgraded database")
// 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: %w", err)
}
// Lookup the last available db version
lastVersion, err := sourceDriver.First()
if err != nil {
return err
}
for {
next, err := sourceDriver.Next(lastVersion)
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()
return ErrDBNotVersioned
}
if err != nil {
return fmt.Errorf("error while checking the database version: %w", err)
}
if version < lastVersion {
return ErrDBNeedUpgrade{
CurrentVersion: version,
RequiredVersion: lastVersion,
}
}
if version > lastVersion {
return ErrDBFutureVersion{
CurrentVersion: version,
RequiredVersion: lastVersion,
}
}