repo_description: A database migration tool. Supports SQL migrations and Go functions. 
repo_topics: database database-migrations go golang migration migrations mysql postgres postgresql schema sql sqlite
code: package goose
  
  import (
  	"io/fs"
  	"math"
  	"os"
  	"path/filepath"
  	"testing"
  
  	"github.com/stretchr/testify/require"
  )
  
  func TestMigrationSort(t *testing.T) {
  	t.Parallel()
  
  	ms := Migrations{}
  
  	// insert in any order
  	ms = append(ms, newMigration(20120000, "test"))
  	ms = append(ms, newMigration(20128000, "test"))
  	ms = append(ms, newMigration(20129000, "test"))
  	ms = append(ms, newMigration(20127000, "test"))
  
  	ms = sortAndConnectMigrations(ms)
  
  	sorted := []int64{20120000, 20127000, 20128000, 20129000}
  
  	validateMigrationSort(t, ms, sorted)
  }
  
  func newMigration(v int64, src string) *Migration {
  	return &Migration{Version: v, Previous: -1, Next: -1, Source: src}
  }
  
  func validateMigrationSort(t *testing.T, ms Migrations, sorted []int64) {
  	t.Helper()
  	for i, m := range ms {
  		if sorted[i] != m.Version {
  			t.Error("incorrect sorted version")
  		}
  
  		var next, prev int64
  
  		if i == 0 {
  			prev = -1
  			next = ms[i+1].Version
  		} else if i == len(ms)-1 {
  			prev = ms[i-1].Version
  			next = -1
  		} else {
  			prev = ms[i-1].Version
  			next = ms[i+1].Version
  		}
  
  		if m.Next != next {
  			t.Errorf("mismatched Next. v: %v, got %v, wanted %v\n", m, m.Next, next)
  		}
  
  		if m.Previous != prev {
  			t.Errorf("mismatched Previous v: %v, got %v, wanted %v\n", m, m.Previous, prev)
  		}
  	}
  
  	t.Log(ms)
  }
  
  func TestCollectMigrations(t *testing.T) {
  	// Not safe to run in parallel
  	t.Run("no_migration_files_found", func(t *testing.T) {
  		tmp := t.TempDir()
  		err := os.MkdirAll(filepath.Join(tmp, "migrations-test"), 0755)
  		require.NoError(t, err)
  		_, err = collectMigrationsFS(os.DirFS(tmp), "migrations-test", 0, math.MaxInt64, nil)
  		require.Error(t, err)
  		require.Contains(t, err.Error(), "no migration files found")
  	})
  	t.Run("filesystem_registered_with_single_dirpath", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		file1, file2 := "09081_a.go", "09082_b.go"
  		file3, file4 := "19081_a.go", "19082_b.go"
  		AddNamedMigrationContext(file1, nil, nil)
  		AddNamedMigrationContext(file2, nil, nil)
  		require.Len(t, registeredGoMigrations, 2)
  		tmp := t.TempDir()
  		dir := filepath.Join(tmp, "migrations", "dir1")
  		err := os.MkdirAll(dir, 0755)
  		require.NoError(t, err)
  		createEmptyFile(t, dir, file1)
  		createEmptyFile(t, dir, file2)
  		createEmptyFile(t, dir, file3)
  		createEmptyFile(t, dir, file4)
  		fsys := os.DirFS(tmp)
  		files, err := fs.ReadDir(fsys, "migrations/dir1")
  		require.NoError(t, err)
  		require.Len(t, files, 4)
  		all, err := collectMigrationsFS(fsys, "migrations/dir1", 0, math.MaxInt64, registeredGoMigrations)
  		require.NoError(t, err)
  		require.Len(t, all, 4)
  		require.EqualValues(t, 9081, all[0].Version)
  		require.EqualValues(t, 9082, all[1].Version)
  		require.EqualValues(t, 19081, all[2].Version)
  		require.EqualValues(t, 19082, all[3].Version)
  	})
  	t.Run("filesystem_registered_with_multiple_dirpath", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		file1, file2, file3 := "00001_a.go", "00002_b.go", "01111_c.go"
  		AddNamedMigrationContext(file1, nil, nil)
  		AddNamedMigrationContext(file2, nil, nil)
  		AddNamedMigrationContext(file3, nil, nil)
  		require.Len(t, registeredGoMigrations, 3)
  		tmp := t.TempDir()
  		dir1 := filepath.Join(tmp, "migrations", "dir1")
  		dir2 := filepath.Join(tmp, "migrations", "dir2")
  		err := os.MkdirAll(dir1, 0755)
  		require.NoError(t, err)
  		err = os.MkdirAll(dir2, 0755)
  		require.NoError(t, err)
  		createEmptyFile(t, dir1, file1)
  		createEmptyFile(t, dir1, file2)
  		createEmptyFile(t, dir2, file3)
  		fsys := os.DirFS(tmp)
  		// Validate if dirpath 1 is specified we get the two Go migrations in migrations/dir1 folder
  		// even though 3 Go migrations have been registered.
  		{
  			all, err := collectMigrationsFS(fsys, "migrations/dir1", 0, math.MaxInt64, registeredGoMigrations)
  			require.NoError(t, err)
  			require.Len(t, all, 2)
  			require.EqualValues(t, 1, all[0].Version)
  			require.EqualValues(t, 2, all[1].Version)
  		}
  		// Validate if dirpath 2 is specified we only get the one Go migration in migrations/dir2 folder
  		// even though 3 Go migrations have been registered.
  		{
  			all, err := collectMigrationsFS(fsys, "migrations/dir2", 0, math.MaxInt64, registeredGoMigrations)
  			require.NoError(t, err)
  			require.Len(t, all, 1)
  			require.EqualValues(t, 1111, all[0].Version)
  		}
  	})
  	t.Run("empty_filesystem_registered_manually", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		AddNamedMigrationContext("00101_a.go", nil, nil)
  		AddNamedMigrationContext("00102_b.go", nil, nil)
  		require.Len(t, registeredGoMigrations, 2)
  		tmp := t.TempDir()
  		err := os.MkdirAll(filepath.Join(tmp, "migrations"), 0755)
  		require.NoError(t, err)
  		all, err := collectMigrationsFS(os.DirFS(tmp), "migrations", 0, math.MaxInt64, registeredGoMigrations)
  		require.NoError(t, err)
  		require.Len(t, all, 2)
  		require.EqualValues(t, 101, all[0].Version)
  		require.EqualValues(t, 102, all[1].Version)
  	})
  	t.Run("unregistered_go_migrations", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		file1, file2, file3 := "00001_a.go", "00998_b.go", "00999_c.go"
  		// Only register file1 and file3, somehow user forgot to init in the
  		// valid looking file2 Go migration
  		AddNamedMigrationContext(file1, nil, nil)
  		AddNamedMigrationContext(file3, nil, nil)
  		require.Len(t, registeredGoMigrations, 2)
  		tmp := t.TempDir()
  		dir1 := filepath.Join(tmp, "migrations", "dir1")
  		err := os.MkdirAll(dir1, 0755)
  		require.NoError(t, err)
  		// Include the valid file2 with file1, file3. But remember, it has NOT been
  		// registered.
  		createEmptyFile(t, dir1, file1)
  		createEmptyFile(t, dir1, file2)
  		createEmptyFile(t, dir1, file3)
  		all, err := collectMigrationsFS(os.DirFS(tmp), "migrations/dir1", 0, math.MaxInt64, registeredGoMigrations)
  		require.NoError(t, err)
  		require.Len(t, all, 3)
  		require.EqualValues(t, 1, all[0].Version)
  		require.True(t, all[0].Registered)
  		require.EqualValues(t, 998, all[1].Version)
  		// This migrations is marked unregistered and will lazily raise an error if/when this
  		// migration is run
  		require.False(t, all[1].Registered)
  		require.EqualValues(t, 999, all[2].Version)
  		require.True(t, all[2].Registered)
  	})
  	t.Run("with_skipped_go_files", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		file1, file2, file3, file4 := "00001_a.go", "00002_b.sql", "00999_c_test.go", "embed.go"
  		AddNamedMigrationContext(file1, nil, nil)
  		require.Len(t, registeredGoMigrations, 1)
  		tmp := t.TempDir()
  		dir1 := filepath.Join(tmp, "migrations", "dir1")
  		err := os.MkdirAll(dir1, 0755)
  		require.NoError(t, err)
  		createEmptyFile(t, dir1, file1)
  		createEmptyFile(t, dir1, file2)
  		createEmptyFile(t, dir1, file3)
  		createEmptyFile(t, dir1, file4)
  		all, err := collectMigrationsFS(os.DirFS(tmp), "migrations/dir1", 0, math.MaxInt64, registeredGoMigrations)
  		require.NoError(t, err)
  		require.Len(t, all, 2)
  		require.EqualValues(t, 1, all[0].Version)
  		require.True(t, all[0].Registered)
  		require.EqualValues(t, 2, all[1].Version)
  		require.False(t, all[1].Registered)
  	})
  	t.Run("current_and_target", func(t *testing.T) {
  		t.Cleanup(func() { clearMap(registeredGoMigrations) })
  		file1, file2, file3 := "01001_a.go", "01002_b.sql", "01003_c.go"
  		AddNamedMigrationContext(file1, nil, nil)
  		AddNamedMigrationContext(file3, nil, nil)
  		require.Len(t, registeredGoMigrations, 2)
  		tmp := t.TempDir()
  		dir1 := filepath.Join(tmp, "migrations", "dir1")
  		err := os.MkdirAll(dir1, 0755)
  		require.NoError(t, err)
  		createEmptyFile(t, dir1, file1)
  		createEmptyFile(t, dir1, file2)
  		createEmptyFile(t, dir1, file3)
  		all, err := collectMigrationsFS(os.DirFS(tmp), "migrations/dir1", 1001, 1003, registeredGoMigrations)
  		require.NoError(t, err)
  		require.Len(t, all, 2)
  		require.EqualValues(t, 1002, all[0].Version)
  		require.EqualValues(t, 1003, all[1].Version)
  	})
  }
  
  func TestVersionFilter(t *testing.T) {
  	t.Parallel()
  
  	tests := []struct {
  		v       int64
  		current int64
  		target  int64
  		want    bool
  	}{
  		{2, 1, 3, true},  // v is within the range
  		{4, 1, 3, false}, // v is outside the range
  		{2, 3, 1, true},  // v is within the reversed range
  		{4, 3, 1, false}, // v is outside the reversed range
  		{3, 1, 3, true},  // v is equal to target
  		{1, 1, 3, false}, // v is equal to current, not within the range
  		{1, 3, 1, false}, // v is equal to current, not within the reversed range
  		// Always return false if current equal target
  		{1, 2, 2, false},
  		{2, 2, 2, false},
  		{3, 2, 2, false},
  	}
  	for _, tc := range tests {
  		t.Run("", func(t *testing.T) {
  			got := versionFilter(tc.v, tc.current, tc.target)
  			if got != tc.want {
  				t.Errorf("versionFilter(%d, %d, %d) = %v, want %v", tc.v, tc.current, tc.target, got, tc.want)
  			}
  		})
  	}
  }
  func createEmptyFile(t *testing.T, dir, name string) {
  	t.Helper()
  	path := filepath.Join(dir, name)
  	f, err := os.Create(path)
  	require.NoError(t, err)
  	defer f.Close()
  }
  
  func clearMap(m map[int64]*Migration) {
  	for k := range m {
  		delete(m, k)
  	}
  }
  