Add the ability to apply missing (out-of-order) migrations (#280)

pull/285/head
Michael Fridman 2021-10-24 14:49:24 -04:00 committed by GitHub
parent 9f8813339a
commit 8ed5f6370b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 681 additions and 60 deletions

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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":

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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
View File

@ -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
}

32
up_test.go Normal file
View File

@ -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
}