mirror of https://github.com/pressly/goose.git
Add the ability to apply missing (out-of-order) migrations (#280)
parent
9f8813339a
commit
8ed5f6370b
9
Makefile
9
Makefile
|
@ -8,15 +8,6 @@ dist:
|
|||
GOOS=windows GOARCH=amd64 go build -o ./bin/goose-windows64.exe ./cmd/goose
|
||||
GOOS=windows GOARCH=386 go build -o ./bin/goose-windows386.exe ./cmd/goose
|
||||
|
||||
.PHONY: vendor
|
||||
vendor:
|
||||
mv _go.mod go.mod
|
||||
mv _go.sum go.sum
|
||||
GO111MODULE=on go build -o ./bin/goose ./cmd/goose
|
||||
GO111MODULE=on go mod vendor && GO111MODULE=on go mod tidy
|
||||
mv go.mod _go.mod
|
||||
mv go.sum _go.sum
|
||||
|
||||
test-packages:
|
||||
go test -v $$(go list ./... | grep -v -e /tests -e /bin -e /cmd -e /examples)
|
||||
|
||||
|
|
|
@ -11,14 +11,15 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
flags = flag.NewFlagSet("goose", flag.ExitOnError)
|
||||
dir = flags.String("dir", ".", "directory with migration files")
|
||||
table = flags.String("table", "goose_db_version", "migrations table name")
|
||||
verbose = flags.Bool("v", false, "enable verbose mode")
|
||||
help = flags.Bool("h", false, "print help")
|
||||
version = flags.Bool("version", false, "print version")
|
||||
certfile = flags.String("certfile", "", "file path to root CA's certificates in pem format (only support on mysql)")
|
||||
sequential = flags.Bool("s", false, "use sequential numbering for new migrations")
|
||||
flags = flag.NewFlagSet("goose", flag.ExitOnError)
|
||||
dir = flags.String("dir", ".", "directory with migration files")
|
||||
table = flags.String("table", "goose_db_version", "migrations table name")
|
||||
verbose = flags.Bool("v", false, "enable verbose mode")
|
||||
help = flags.Bool("h", false, "print help")
|
||||
version = flags.Bool("version", false, "print version")
|
||||
certfile = flags.String("certfile", "", "file path to root CA's certificates in pem format (only support on mysql)")
|
||||
sequential = flags.Bool("s", false, "use sequential numbering for new migrations")
|
||||
allowMissing = flags.Bool("allow-missing", false, "applies missing (out-of-order) migrations")
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -86,7 +87,17 @@ func main() {
|
|||
arguments = append(arguments, args[3:]...)
|
||||
}
|
||||
|
||||
if err := goose.Run(command, db, *dir, arguments...); err != nil {
|
||||
options := []goose.OptionsFunc{}
|
||||
if *allowMissing {
|
||||
options = append(options, goose.WithAllowMissing())
|
||||
}
|
||||
if err := goose.RunWithOptions(
|
||||
command,
|
||||
db,
|
||||
*dir,
|
||||
arguments,
|
||||
options...,
|
||||
); err != nil {
|
||||
log.Fatalf("goose run: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
15
goose.go
15
goose.go
|
@ -38,13 +38,22 @@ func SetBaseFS(fsys fs.FS) {
|
|||
|
||||
// Run runs a goose command.
|
||||
func Run(command string, db *sql.DB, dir string, args ...string) error {
|
||||
return run(command, db, dir, args)
|
||||
}
|
||||
|
||||
// Run runs a goose command with options.
|
||||
func RunWithOptions(command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {
|
||||
return run(command, db, dir, args, options...)
|
||||
}
|
||||
|
||||
func run(command string, db *sql.DB, dir string, args []string, options ...OptionsFunc) error {
|
||||
switch command {
|
||||
case "up":
|
||||
if err := Up(db, dir); err != nil {
|
||||
if err := Up(db, dir, options...); err != nil {
|
||||
return err
|
||||
}
|
||||
case "up-by-one":
|
||||
if err := UpByOne(db, dir); err != nil {
|
||||
if err := UpByOne(db, dir, options...); err != nil {
|
||||
return err
|
||||
}
|
||||
case "up-to":
|
||||
|
@ -56,7 +65,7 @@ func Run(command string, db *sql.DB, dir string, args ...string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("version must be a number (got '%s')", args[0])
|
||||
}
|
||||
if err := UpTo(db, dir, version); err != nil {
|
||||
if err := UpTo(db, dir, version, options...); err != nil {
|
||||
return err
|
||||
}
|
||||
case "create":
|
||||
|
|
|
@ -0,0 +1,339 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func TestNotAllowMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
// Developer A and B check out the "main" branch which is currently
|
||||
// on version 5. Developer A mistakenly creates migration 7 and commits.
|
||||
// Developer B did not pull the latest changes and commits migration 6. Oops.
|
||||
|
||||
// Developer A - migration 7 (mistakenly applied)
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, 7)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
|
||||
// Developer B - migration 6 (missing) and 8 (new)
|
||||
// This should raise an error. By default goose does not allow missing (out-of-order)
|
||||
// migrations, which means halt if a missing migration is detected.
|
||||
err = goose.Up(db, migrationsDir)
|
||||
is.True(err != nil) // error: found 1 missing migrations
|
||||
is.True(strings.Contains(err.Error(), "missing migrations"))
|
||||
// Confirm db version is unchanged.
|
||||
current, err = goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
}
|
||||
|
||||
func TestAllowMissingUpWithRedo(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, goose.MaxVersion)
|
||||
is.NoErr(err)
|
||||
is.True(len(migrations) != 0)
|
||||
|
||||
// Migration 7
|
||||
{
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, 7)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
|
||||
// Redo the previous Up migration and re-apply it.
|
||||
err = goose.Redo(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
currentVersion, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.True(currentVersion == migrations[6].Version)
|
||||
}
|
||||
// Migration 6
|
||||
{
|
||||
err = goose.UpByOne(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err)
|
||||
currentVersion, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(currentVersion, int64(6))
|
||||
|
||||
err = goose.Redo(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
currentVersion, err = goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(currentVersion, int64(6))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNowAllowMissingUpByOne(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
/*
|
||||
Developer A and B simultaneously check out the "main" currently on version 5.
|
||||
Developer A mistakenly creates migration 7 and commits.
|
||||
Developer B did not pull the latest changes and commits migration 6. Oops.
|
||||
|
||||
If goose is set to allow missing migrations, then 6 should be applied
|
||||
after 7.
|
||||
*/
|
||||
|
||||
// Developer A - migration 7 (mistakenly applied)
|
||||
{
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, 7)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
}
|
||||
// Developer B - migration 6
|
||||
{
|
||||
// By default, this should raise an error.
|
||||
err := goose.UpByOne(db, migrationsDir)
|
||||
is.True(err != nil) // error: found 1 missing migrations
|
||||
is.True(strings.Contains(err.Error(), "missing migrations"))
|
||||
|
||||
count, err := getGooseVersionCount(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(count, int64(6)) // Expecting count of migrations to be 6
|
||||
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7)) // Expecting max(version_id) to be 7
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowMissingUpWithReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
/*
|
||||
Developer A and B simultaneously check out the "main" currently on version 5.
|
||||
Developer A mistakenly creates migration 7 and commits.
|
||||
Developer B did not pull the latest changes and commits migration 6. Oops.
|
||||
|
||||
If goose is set to allow missing migrations, then 6 should be applied
|
||||
after 7.
|
||||
*/
|
||||
|
||||
// Developer A - migration 7 (mistakenly applied)
|
||||
{
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, 7)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
}
|
||||
// Developer B - migration 6 (missing) and 8 (new)
|
||||
{
|
||||
// By default, attempting to apply this migration will raise an error.
|
||||
// If goose is set to "allow missing" migrations then it should get applied.
|
||||
err := goose.Up(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err) // Applying missing migration should return no error when allow-missing=true
|
||||
|
||||
// Avoid hard-coding total and max, instead resolve it from the testdata migrations.
|
||||
// In other words, we applied 1..5,7,6,8 and this test shouldn't care
|
||||
// about migration 9 and onwards.
|
||||
allMigrations, err := goose.CollectMigrations(migrationsDir, 0, goose.MaxVersion)
|
||||
is.NoErr(err)
|
||||
maxVersionID := allMigrations[len(allMigrations)-1].Version
|
||||
|
||||
count, err := getGooseVersionCount(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(count, int64(len(allMigrations))) // Count should be all testdata migrations (all applied)
|
||||
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, maxVersionID) // Expecting max(version_id) to be highest version in testdata
|
||||
}
|
||||
|
||||
// Migrate everything down using Reset.
|
||||
err := goose.Reset(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
currentVersion, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(currentVersion, int64(0))
|
||||
}
|
||||
|
||||
func TestAllowMissingUpByOne(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
/*
|
||||
Developer A and B simultaneously check out the "main" currently on version 5.
|
||||
Developer A mistakenly creates migration 7 and commits.
|
||||
Developer B did not pull the latest changes and commits migration 6. Oops.
|
||||
|
||||
If goose is set to allow missing migrations, then 6 should be applied
|
||||
after 7.
|
||||
*/
|
||||
|
||||
// Developer A - migration 7 (mistakenly applied)
|
||||
{
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, 7)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7))
|
||||
}
|
||||
// Developer B - migration 6
|
||||
{
|
||||
err := goose.UpByOne(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err)
|
||||
|
||||
count, err := getGooseVersionCount(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(count, int64(7)) // Expecting count of migrations to be 7
|
||||
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(6)) // Expecting max(version_id) to be 6
|
||||
}
|
||||
// Developer B - migration 8
|
||||
{
|
||||
// By default, this should raise an error.
|
||||
err := goose.UpByOne(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err)
|
||||
|
||||
count, err := getGooseVersionCount(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(count, int64(8)) // Expecting count of migrations to be 8
|
||||
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(8)) // Expecting max(version_id) to be 8
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateAllowMissingDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
const (
|
||||
maxVersion = 8
|
||||
)
|
||||
// Create and apply first 5 migrations.
|
||||
db := setupTestDB(t, 5)
|
||||
|
||||
// Developer A - migration 7 (mistakenly applied)
|
||||
{
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, maxVersion-1)
|
||||
is.NoErr(err)
|
||||
err = migrations[6].Up(db)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(maxVersion-1))
|
||||
}
|
||||
// Developer B - migration 6 (missing) and 8 (new)
|
||||
{
|
||||
// 6
|
||||
err := goose.UpByOne(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err)
|
||||
// 8
|
||||
err = goose.UpByOne(db, migrationsDir, goose.WithAllowMissing())
|
||||
is.NoErr(err)
|
||||
|
||||
count, err := getGooseVersionCount(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(count, int64(maxVersion)) // Expecting count of migrations to be 8
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(maxVersion)) // Expecting max(version_id) to be 8
|
||||
}
|
||||
// The order in the database is expected to be:
|
||||
// 1,2,3,4,5,7,6,8
|
||||
// So migrating down should be the reverse order:
|
||||
// 8,6,7,5,4,3,2,1
|
||||
//
|
||||
// Migrate down by one. Expecting 6.
|
||||
{
|
||||
err := goose.Down(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(6)) // Expecting max(version) to be 6
|
||||
}
|
||||
// Migrate down by one. Expecting 7.
|
||||
{
|
||||
err := goose.Down(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(7)) // Expecting max(version) to be 7
|
||||
}
|
||||
// Migrate down by one. Expecting 5.
|
||||
{
|
||||
err := goose.Down(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
current, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(current, int64(5)) // Expecting max(version) to be 5
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestDB is helper to setup a DB and apply migrations
|
||||
// up to the specified version.
|
||||
func setupTestDB(t *testing.T, version int64) *sql.DB {
|
||||
is := is.New(t)
|
||||
db, err := newDockerDB(t)
|
||||
is.NoErr(err)
|
||||
|
||||
goose.SetDialect(*dialect)
|
||||
|
||||
// Create goose table.
|
||||
current, err := goose.EnsureDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.True(current == int64(0))
|
||||
// Collect first 5 migrations.
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, version)
|
||||
is.NoErr(err)
|
||||
is.True(int64(len(migrations)) == version)
|
||||
// Apply n migrations manually.
|
||||
for _, m := range migrations {
|
||||
err := m.Up(db)
|
||||
is.NoErr(err)
|
||||
}
|
||||
// Verify the current DB version is the Nth migration. This will only
|
||||
// work for sqeuentially applied migrations.
|
||||
current, err = goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.True(current == int64(version))
|
||||
|
||||
return db
|
||||
}
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func TestMigrateUp(t *testing.T) {
|
||||
func TestMigrateUpWithReset(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
|
@ -30,6 +30,48 @@ func TestMigrateUp(t *testing.T) {
|
|||
gotVersion, err := getCurrentGooseVersion(db, goose.TableName())
|
||||
is.NoErr(err)
|
||||
is.Equal(gotVersion, currentVersion) // incorrect database version
|
||||
|
||||
// Migrate everything down using Reset.
|
||||
err = goose.Reset(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
currentVersion, err = goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(currentVersion, int64(0))
|
||||
}
|
||||
|
||||
func TestMigrateUpWithRedo(t *testing.T) {
|
||||
t.Parallel()
|
||||
is := is.New(t)
|
||||
|
||||
db, err := newDockerDB(t)
|
||||
is.NoErr(err)
|
||||
goose.SetDialect(*dialect)
|
||||
migrations, err := goose.CollectMigrations(migrationsDir, 0, goose.MaxVersion)
|
||||
is.NoErr(err)
|
||||
is.True(len(migrations) != 0)
|
||||
startingVersion, err := goose.EnsureDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(startingVersion, int64(0))
|
||||
// Migrate all
|
||||
for _, migration := range migrations {
|
||||
err = migration.Up(db)
|
||||
is.NoErr(err)
|
||||
currentVersion, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.True(currentVersion == migration.Version)
|
||||
|
||||
// Redo the previous Up migration and re-apply it.
|
||||
err = goose.Redo(db, migrationsDir)
|
||||
is.NoErr(err)
|
||||
currentVersion, err = goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.True(currentVersion == migration.Version)
|
||||
}
|
||||
// Once everything is tested the version should match the highest testdata version
|
||||
maxVersion := migrations[len(migrations)-1].Version
|
||||
currentVersion, err := goose.GetDBVersion(db)
|
||||
is.NoErr(err)
|
||||
is.Equal(currentVersion, maxVersion)
|
||||
}
|
||||
|
||||
func TestMigrateUpTo(t *testing.T) {
|
||||
|
@ -68,7 +110,7 @@ func TestMigrateUpByOne(t *testing.T) {
|
|||
migrations, err := goose.CollectMigrations(migrationsDir, 0, goose.MaxVersion)
|
||||
is.NoErr(err)
|
||||
is.True(len(migrations) != 0)
|
||||
// Migrate up to the second migration
|
||||
// Apply all migrations one-by-one.
|
||||
var counter int
|
||||
for {
|
||||
err := goose.UpByOne(db, migrationsDir)
|
||||
|
@ -161,6 +203,16 @@ func getCurrentGooseVersion(db *sql.DB, gooseTable string) (int64, error) {
|
|||
return gotVersion, nil
|
||||
}
|
||||
|
||||
func getGooseVersionCount(db *sql.DB, gooseTable string) (int64, error) {
|
||||
var gotVersion int64
|
||||
if err := db.QueryRow(
|
||||
fmt.Sprintf("SELECT count(*) FROM %s WHERE version_id > 0", gooseTable),
|
||||
).Scan(&gotVersion); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return gotVersion, nil
|
||||
}
|
||||
|
||||
func getTableNames(db *sql.DB) ([]string, error) {
|
||||
var query string
|
||||
switch *dialect {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- This migration intentionally depends on 00006_f.sql
|
||||
ALTER TABLE stargazers DROP COLUMN stargazer_location;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE stargazers ADD COLUMN stargazer_location text NOT NULL;
|
||||
-- +goose StatementEnd
|
|
@ -0,0 +1,10 @@
|
|||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- This migration intentionally depends on 00006_f.sql
|
||||
ALTER TABLE stargazers DROP COLUMN stargazer_location;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
ALTER TABLE stargazers ADD COLUMN stargazer_location text NOT NULL;
|
||||
-- +goose StatementEnd
|
241
up.go
241
up.go
|
@ -2,64 +2,231 @@ package goose
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
allowMissing bool
|
||||
applyUpByOne bool
|
||||
}
|
||||
|
||||
type OptionsFunc func(o *options)
|
||||
|
||||
func WithAllowMissing() OptionsFunc {
|
||||
return func(o *options) { o.allowMissing = true }
|
||||
}
|
||||
|
||||
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) error {
|
||||
migrations, err := CollectMigrations(dir, minVersion, version)
|
||||
func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
||||
if _, err := EnsureDBVersion(db); err != nil {
|
||||
return err
|
||||
}
|
||||
option := &options{}
|
||||
for _, f := range opts {
|
||||
f(option)
|
||||
}
|
||||
foundMigrations, err := CollectMigrations(dir, minVersion, version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbMigrations, err := listAllDBVersions(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
missingMigrations := findMissingMigrations(dbMigrations, foundMigrations)
|
||||
|
||||
// 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:\n\t%s",
|
||||
len(missingMigrations), strings.Join(collected, "\n\t"))
|
||||
}
|
||||
|
||||
if option.allowMissing {
|
||||
return upWithMissing(
|
||||
db,
|
||||
missingMigrations,
|
||||
foundMigrations,
|
||||
dbMigrations,
|
||||
option,
|
||||
)
|
||||
}
|
||||
|
||||
var current int64
|
||||
for {
|
||||
var err error
|
||||
current, err = GetDBVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
next, err := foundMigrations.Next(current)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNoNextVersion) {
|
||||
break
|
||||
}
|
||||
return fmt.Errorf("failed to find next migration: %v", err)
|
||||
}
|
||||
if err := next.Up(db); err != nil {
|
||||
return err
|
||||
}
|
||||
if option.applyUpByOne {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// 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
|
||||
log.Printf("goose: no migrations to run. current version: %d\n", current)
|
||||
if option.applyUpByOne {
|
||||
return ErrNoNextVersion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upWithMissing(
|
||||
db *sql.DB,
|
||||
missingMigrations Migrations,
|
||||
foundMigrations Migrations,
|
||||
dbMigrations Migrations,
|
||||
option *options,
|
||||
) error {
|
||||
lookupApplied := make(map[int64]bool)
|
||||
for _, found := range dbMigrations {
|
||||
lookupApplied[found.Version] = true
|
||||
}
|
||||
|
||||
// Apply all missing migrations first.
|
||||
for _, missing := range missingMigrations {
|
||||
if err := missing.Up(db); err != nil {
|
||||
return err
|
||||
}
|
||||
// Apply one migration and return early.
|
||||
if option.applyUpByOne {
|
||||
return nil
|
||||
}
|
||||
// TODO(mf): do we need this check? It's a bit redundant, but we may
|
||||
// want to keep it as a safe-guard. Maybe we should instead have
|
||||
// the underlying query (if possible) return the current version as
|
||||
// part of the same transaction.
|
||||
current, err := GetDBVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if current == missing.Version {
|
||||
lookupApplied[missing.Version] = true
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("error: missing migration:%d does not match current db version:%d",
|
||||
current, missing.Version)
|
||||
}
|
||||
|
||||
next, err := migrations.Next(current)
|
||||
if err != nil {
|
||||
if err == ErrNoNextVersion {
|
||||
log.Printf("goose: no migrations to run. current version: %d\n", current)
|
||||
return nil
|
||||
}
|
||||
// We can no longer rely on the database version_id to be sequential because
|
||||
// missing (out-of-order) migrations get applied before newer migrations.
|
||||
|
||||
for _, found := range foundMigrations {
|
||||
// TODO(mf): instead of relying on this lookup, consider hitting
|
||||
// the database directly?
|
||||
// Alternatively, we can skip a bunch migrations and start the cursor
|
||||
// at a version that represents 100% applied migrations. But this is
|
||||
// risky, and we should aim to keep this logic simple.
|
||||
if lookupApplied[found.Version] {
|
||||
continue
|
||||
}
|
||||
if err := found.Up(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = next.Up(db); err != nil {
|
||||
return err
|
||||
if option.applyUpByOne {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
current, err := GetDBVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 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
|
||||
log.Printf("goose: no migrations to run. current version: %d\n", current)
|
||||
if option.applyUpByOne {
|
||||
return ErrNoNextVersion
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Up applies all available migrations.
|
||||
func Up(db *sql.DB, dir string) error {
|
||||
return UpTo(db, dir, maxVersion)
|
||||
func Up(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
||||
return UpTo(db, dir, maxVersion, opts...)
|
||||
}
|
||||
|
||||
// UpByOne migrates up by a single version.
|
||||
func UpByOne(db *sql.DB, dir string) error {
|
||||
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentVersion, err := GetDBVersion(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
next, err := migrations.Next(currentVersion)
|
||||
if err != nil {
|
||||
if err == ErrNoNextVersion {
|
||||
log.Printf("goose: no migrations to run. current version: %d\n", currentVersion)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err = next.Up(db); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
func UpByOne(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
||||
opts = append(opts, withApplyUpByOne())
|
||||
return UpTo(db, dir, maxVersion, opts...)
|
||||
}
|
||||
|
||||
// listAllDBVersions returns a list of all migrations, ordered ascending.
|
||||
// TODO(mf): fairly cheap, but a nice-to-have is pagination support.
|
||||
func listAllDBVersions(db *sql.DB) (Migrations, error) {
|
||||
rows, err := GetDialect().dbVersionQuery(db)
|
||||
if err != nil {
|
||||
return nil, createVersionTable(db)
|
||||
}
|
||||
var all Migrations
|
||||
for rows.Next() {
|
||||
var versionID int64
|
||||
var isApplied bool
|
||||
if err := rows.Scan(&versionID, &isApplied); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, &Migration{
|
||||
Version: versionID,
|
||||
})
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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) Migrations {
|
||||
max := knownMigrations[len(knownMigrations)-1].Version
|
||||
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 < max {
|
||||
missing = append(missing, new)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(missing, func(i, j int) bool {
|
||||
return missing[i].Version < missing[j].Version
|
||||
})
|
||||
return missing
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package goose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestFindMissingMigrations(t *testing.T) {
|
||||
is := is.New(t)
|
||||
known := Migrations{
|
||||
{Version: 1},
|
||||
{Version: 3},
|
||||
{Version: 4},
|
||||
{Version: 5},
|
||||
{Version: 7},
|
||||
}
|
||||
new := Migrations{
|
||||
{Version: 1},
|
||||
{Version: 2}, // missing migration
|
||||
{Version: 3},
|
||||
{Version: 4},
|
||||
{Version: 5},
|
||||
{Version: 6}, // missing migration
|
||||
{Version: 7}, // <-- database max version_id
|
||||
{Version: 8}, // new migration
|
||||
}
|
||||
got := findMissingMigrations(known, new)
|
||||
is.Equal(len(got), int(2))
|
||||
is.Equal(got[0].Version, int64(2)) // Expecting first missing migration
|
||||
is.Equal(got[1].Version, int64(6)) // Expecting second missing migration
|
||||
}
|
Loading…
Reference in New Issue