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" ) //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 // 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 } 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 { return fmt.Errorf("error during auto-migration: %w", 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: %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() if errors.Is(err, migrate.ErrNilVersion) { return ErrDBNotVersioned } if err != nil { return fmt.Errorf("error while checking the database version: %w", err) } if dirty { 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, } } return nil }