mirror of https://github.com/pressly/goose.git
222 lines
6.4 KiB
Go
222 lines
6.4 KiB
Go
package goose
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type options struct {
|
|
allowMissing bool
|
|
applyUpByOne bool
|
|
noVersioning bool
|
|
}
|
|
|
|
type OptionsFunc func(o *options)
|
|
|
|
func WithAllowMissing() OptionsFunc {
|
|
return func(o *options) { o.allowMissing = true }
|
|
}
|
|
|
|
func WithNoVersioning() OptionsFunc {
|
|
return func(o *options) { o.noVersioning = true }
|
|
}
|
|
|
|
func WithNoColor(b bool) OptionsFunc {
|
|
return func(o *options) { noColor = b }
|
|
}
|
|
|
|
func withApplyUpByOne() OptionsFunc {
|
|
return func(o *options) { o.applyUpByOne = true }
|
|
}
|
|
|
|
// UpTo migrates up to a specific version.
|
|
func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
|
ctx := context.Background()
|
|
return UpToContext(ctx, db, dir, version, opts...)
|
|
}
|
|
|
|
func UpToContext(ctx context.Context, db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
|
option := &options{}
|
|
for _, f := range opts {
|
|
f(option)
|
|
}
|
|
foundMigrations, err := CollectMigrations(dir, minVersion, version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if option.noVersioning {
|
|
if len(foundMigrations) == 0 {
|
|
return nil
|
|
}
|
|
if option.applyUpByOne {
|
|
// For up-by-one this means keep re-applying the first
|
|
// migration over and over.
|
|
version = foundMigrations[0].Version
|
|
}
|
|
return upToNoVersioning(ctx, db, foundMigrations, version)
|
|
}
|
|
|
|
if _, err := EnsureDBVersionContext(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
dbMigrations, err := listAllDBVersions(ctx, db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbMaxVersion := dbMigrations[len(dbMigrations)-1].Version
|
|
// lookupAppliedInDB is a map of all applied migrations in the database.
|
|
lookupAppliedInDB := make(map[int64]bool)
|
|
for _, m := range dbMigrations {
|
|
lookupAppliedInDB[m.Version] = true
|
|
}
|
|
|
|
missingMigrations := findMissingMigrations(dbMigrations, foundMigrations, dbMaxVersion)
|
|
|
|
// feature(mf): It is very possible someone may want to apply ONLY new migrations
|
|
// and skip missing migrations altogether. At the moment this is not supported,
|
|
// but leaving this comment because that's where that logic will be handled.
|
|
if len(missingMigrations) > 0 && !option.allowMissing {
|
|
var collected []string
|
|
for _, m := range missingMigrations {
|
|
output := fmt.Sprintf("version %d: %s", m.Version, m.Source)
|
|
collected = append(collected, output)
|
|
}
|
|
return fmt.Errorf("error: found %d missing migrations before current version %d:\n\t%s",
|
|
len(missingMigrations), dbMaxVersion, strings.Join(collected, "\n\t"))
|
|
}
|
|
var migrationsToApply Migrations
|
|
if option.allowMissing {
|
|
migrationsToApply = missingMigrations
|
|
}
|
|
// filter all migrations with a version greater than the supplied version (min) and less than or
|
|
// equal to the requested version (max). Note, we do not need to filter out missing migrations
|
|
// because we are only appending "new" migrations that have a higher version than the current
|
|
// database max version, which inevitably means they are not "missing".
|
|
for _, m := range foundMigrations {
|
|
if lookupAppliedInDB[m.Version] {
|
|
continue
|
|
}
|
|
if m.Version > dbMaxVersion && m.Version <= version {
|
|
migrationsToApply = append(migrationsToApply, m)
|
|
}
|
|
}
|
|
|
|
var current int64
|
|
for _, m := range migrationsToApply {
|
|
if err := m.UpContext(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
if option.applyUpByOne {
|
|
return nil
|
|
}
|
|
current = m.Version
|
|
}
|
|
|
|
if len(migrationsToApply) == 0 {
|
|
current, err = GetDBVersionContext(ctx, db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Printf("goose: no migrations to run. current version: %d", current)
|
|
} else {
|
|
log.Printf("goose: successfully migrated database to version: %d", current)
|
|
}
|
|
|
|
// At this point there are no more migrations to apply. But we need to maintain
|
|
// the following behaviour:
|
|
// UpByOne returns an error to signifying there are no more migrations.
|
|
// Up and UpTo return nil
|
|
|
|
if option.applyUpByOne {
|
|
return ErrNoNextVersion
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// upToNoVersioning applies up migrations up to, and including, the
|
|
// target version.
|
|
func upToNoVersioning(ctx context.Context, db *sql.DB, migrations Migrations, version int64) error {
|
|
var finalVersion int64
|
|
for _, current := range migrations {
|
|
if current.Version > version {
|
|
break
|
|
}
|
|
current.noVersioning = true
|
|
if err := current.UpContext(ctx, db); err != nil {
|
|
return err
|
|
}
|
|
finalVersion = current.Version
|
|
}
|
|
log.Printf("goose: up to current file version: %d", finalVersion)
|
|
return nil
|
|
}
|
|
|
|
// Up applies all available migrations.
|
|
func Up(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
|
ctx := context.Background()
|
|
return UpContext(ctx, db, dir, opts...)
|
|
}
|
|
|
|
// UpContext applies all available migrations.
|
|
func UpContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
|
|
return UpToContext(ctx, db, dir, maxVersion, opts...)
|
|
}
|
|
|
|
// UpByOne migrates up by a single version.
|
|
func UpByOne(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
|
ctx := context.Background()
|
|
return UpByOneContext(ctx, db, dir, opts...)
|
|
}
|
|
|
|
// UpByOneContext migrates up by a single version.
|
|
func UpByOneContext(ctx context.Context, db *sql.DB, dir string, opts ...OptionsFunc) error {
|
|
opts = append(opts, withApplyUpByOne())
|
|
return UpToContext(ctx, db, dir, maxVersion, opts...)
|
|
}
|
|
|
|
// listAllDBVersions returns a list of all migrations, ordered ascending.
|
|
func listAllDBVersions(ctx context.Context, db *sql.DB) (Migrations, error) {
|
|
dbMigrations, err := store.ListMigrations(ctx, db, TableName())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all := make(Migrations, 0, len(dbMigrations))
|
|
for _, m := range dbMigrations {
|
|
all = append(all, &Migration{
|
|
Version: m.VersionID,
|
|
})
|
|
}
|
|
// ListMigrations returns migrations in descending order by id.
|
|
// But we want to return them in ascending order by version_id, so we re-sort.
|
|
sort.SliceStable(all, func(i, j int) bool {
|
|
return all[i].Version < all[j].Version
|
|
})
|
|
return all, nil
|
|
}
|
|
|
|
// findMissingMigrations migrations returns all missing migrations.
|
|
// A migrations is considered missing if it has a version less than the
|
|
// current known max version.
|
|
func findMissingMigrations(knownMigrations, newMigrations Migrations, dbMaxVersion int64) Migrations {
|
|
existing := make(map[int64]bool)
|
|
for _, known := range knownMigrations {
|
|
existing[known.Version] = true
|
|
}
|
|
var missing Migrations
|
|
for _, new := range newMigrations {
|
|
if !existing[new.Version] && new.Version < dbMaxVersion {
|
|
missing = append(missing, new)
|
|
}
|
|
}
|
|
sort.SliceStable(missing, func(i, j int) bool {
|
|
return missing[i].Version < missing[j].Version
|
|
})
|
|
return missing
|
|
}
|