package goose_test import ( "context" "database/sql" "errors" "fmt" "math" "math/rand" "os" "path/filepath" "reflect" "sync" "testing" "testing/fstest" "github.com/pressly/goose/v3" "github.com/pressly/goose/v3/database" "github.com/stretchr/testify/require" ) func TestProviderRun(t *testing.T) { t.Parallel() t.Run("closed_db", func(t *testing.T) { p, db := newProviderWithDB(t) require.NoError(t, db.Close()) _, err := p.Up(context.Background()) require.Error(t, err) require.Equal(t, "failed to initialize: sql: database is closed", err.Error()) }) t.Run("ping_and_close", func(t *testing.T) { p, _ := newProviderWithDB(t) t.Cleanup(func() { require.NoError(t, p.Close()) }) require.NoError(t, p.Ping(context.Background())) }) t.Run("apply_unknown_version", func(t *testing.T) { p, _ := newProviderWithDB(t) _, err := p.ApplyVersion(context.Background(), 999, true) require.Error(t, err) require.ErrorIs(t, err, goose.ErrVersionNotFound) _, err = p.ApplyVersion(context.Background(), 999, false) require.Error(t, err) require.ErrorIs(t, err, goose.ErrVersionNotFound) }) t.Run("run_zero", func(t *testing.T) { p, _ := newProviderWithDB(t) _, err := p.UpTo(context.Background(), 0) require.Error(t, err) require.Equal(t, "version must be greater than 0", err.Error()) _, err = p.DownTo(context.Background(), -1) require.Error(t, err) require.Equal(t, "invalid version: must be a valid number or zero: -1", err.Error()) _, err = p.ApplyVersion(context.Background(), 0, true) require.Error(t, err) require.Equal(t, "version must be greater than 0", err.Error()) }) t.Run("up_and_down_all", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) const ( numCount = 7 ) sources := p.ListSources() require.Len(t, sources, numCount) // Ensure only SQL migrations are returned for _, s := range sources { require.Equal(t, goose.TypeSQL, s.Type) } // Test Up res, err := p.Up(ctx) require.NoError(t, err) require.Len(t, res, numCount) assertResult(t, res[0], newSource(goose.TypeSQL, "00001_users_table.sql", 1), "up", false) assertResult(t, res[1], newSource(goose.TypeSQL, "00002_posts_table.sql", 2), "up", false) assertResult(t, res[2], newSource(goose.TypeSQL, "00003_comments_table.sql", 3), "up", false) assertResult(t, res[3], newSource(goose.TypeSQL, "00004_insert_data.sql", 4), "up", false) assertResult(t, res[4], newSource(goose.TypeSQL, "00005_posts_view.sql", 5), "up", false) assertResult(t, res[5], newSource(goose.TypeSQL, "00006_empty_up.sql", 6), "up", true) assertResult(t, res[6], newSource(goose.TypeSQL, "00007_empty_up_down.sql", 7), "up", true) // Test Down res, err = p.DownTo(ctx, 0) require.NoError(t, err) require.Len(t, res, numCount) assertResult(t, res[0], newSource(goose.TypeSQL, "00007_empty_up_down.sql", 7), "down", true) assertResult(t, res[1], newSource(goose.TypeSQL, "00006_empty_up.sql", 6), "down", true) assertResult(t, res[2], newSource(goose.TypeSQL, "00005_posts_view.sql", 5), "down", false) assertResult(t, res[3], newSource(goose.TypeSQL, "00004_insert_data.sql", 4), "down", false) assertResult(t, res[4], newSource(goose.TypeSQL, "00003_comments_table.sql", 3), "down", false) assertResult(t, res[5], newSource(goose.TypeSQL, "00002_posts_table.sql", 2), "down", false) assertResult(t, res[6], newSource(goose.TypeSQL, "00001_users_table.sql", 1), "down", false) }) t.Run("up_and_down_by_one", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) maxVersion := len(p.ListSources()) // Apply all migrations one-by-one. var counter int for { res, err := p.UpByOne(ctx) counter++ if counter > maxVersion { if !errors.Is(err, goose.ErrNoNextVersion) { t.Fatalf("incorrect error: got:%v want:%v", err, goose.ErrNoNextVersion) } break } require.NoError(t, err) require.NotNil(t, res) require.Equal(t, res.Source.Version, int64(counter)) } currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.Equal(t, currentVersion, int64(maxVersion)) // Reset counter counter = 0 // Rollback all migrations one-by-one. for { res, err := p.Down(ctx) counter++ if counter > maxVersion { if !errors.Is(err, goose.ErrNoNextVersion) { t.Fatalf("incorrect error: got:%v want:%v", err, goose.ErrNoNextVersion) } break } require.NoError(t, err) require.NotNil(t, res) require.Equal(t, res.Source.Version, int64(maxVersion-counter+1)) } // Once everything is tested the version should match the highest testdata version currentVersion, err = p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 0, currentVersion) }) t.Run("up_to", func(t *testing.T) { ctx := context.Background() p, db := newProviderWithDB(t) const ( upToVersion int64 = 2 ) results, err := p.UpTo(ctx, upToVersion) require.NoError(t, err) require.Len(t, results, int(upToVersion)) assertResult(t, results[0], newSource(goose.TypeSQL, "00001_users_table.sql", 1), "up", false) assertResult(t, results[1], newSource(goose.TypeSQL, "00002_posts_table.sql", 2), "up", false) // Fetch the goose version from DB currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.Equal(t, upToVersion, currentVersion) // Validate the version actually matches what goose claims it is gotVersion, err := getMaxVersionID(db, goose.DefaultTablename) require.NoError(t, err) require.Equal(t, upToVersion, gotVersion) }) t.Run("sql_connections", func(t *testing.T) { tt := []struct { name string maxOpenConns int maxIdleConns int useDefaults bool }{ // Single connection ensures goose is able to function correctly when multiple // connections are not available. {name: "single_conn", maxOpenConns: 1, maxIdleConns: 1}, {name: "defaults", useDefaults: true}, } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() // Start a new database for each test case. p, db := newProviderWithDB(t) if !tc.useDefaults { db.SetMaxOpenConns(tc.maxOpenConns) db.SetMaxIdleConns(tc.maxIdleConns) } sources := p.ListSources() require.NotEmpty(t, sources) currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 0, currentVersion) { // Apply all up migrations upResult, err := p.Up(ctx) require.NoError(t, err) require.Len(t, sources, len(upResult)) currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.Equal(t, currentVersion, p.ListSources()[len(sources)-1].Version) // Validate the db migration version actually matches what goose claims it is gotVersion, err := getMaxVersionID(db, goose.DefaultTablename) require.NoError(t, err) require.Equal(t, gotVersion, currentVersion) tables, err := getTableNames(db) require.NoError(t, err) if !reflect.DeepEqual(tables, knownTables) { t.Logf("got tables: %v", tables) t.Logf("known tables: %v", knownTables) t.Fatal("failed to match tables") } } { // Apply all down migrations downResult, err := p.DownTo(ctx, 0) require.NoError(t, err) require.Len(t, sources, len(downResult)) gotVersion, err := getMaxVersionID(db, goose.DefaultTablename) require.NoError(t, err) require.EqualValues(t, 0, gotVersion) // Should only be left with a single table, the default goose table tables, err := getTableNames(db) require.NoError(t, err) knownTables := []string{goose.DefaultTablename, "sqlite_sequence"} if !reflect.DeepEqual(tables, knownTables) { t.Logf("got tables: %v", tables) t.Logf("known tables: %v", knownTables) t.Fatal("failed to match tables") } } }) } }) t.Run("apply", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) sources := p.ListSources() // Apply all migrations in the up direction. for _, s := range sources { res, err := p.ApplyVersion(ctx, s.Version, true) require.NoError(t, err) // Round-trip the migration result through the database to ensure it's valid. var empty bool if s.Version == 6 || s.Version == 7 { empty = true } assertResult(t, res, s, "up", empty) } // Apply all migrations in the down direction. for i := len(sources) - 1; i >= 0; i-- { s := sources[i] res, err := p.ApplyVersion(ctx, s.Version, false) require.NoError(t, err) // Round-trip the migration result through the database to ensure it's valid. var empty bool if s.Version == 6 || s.Version == 7 { empty = true } assertResult(t, res, s, "down", empty) } // Try apply version 1 multiple times _, err := p.ApplyVersion(ctx, 1, true) require.NoError(t, err) _, err = p.ApplyVersion(ctx, 1, true) require.Error(t, err) require.ErrorIs(t, err, goose.ErrAlreadyApplied) require.Contains(t, err.Error(), "version 1: migration already applied") }) t.Run("status", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) numCount := len(p.ListSources()) // Before any migrations are applied, the status should be empty. status, err := p.Status(ctx) require.NoError(t, err) require.Equal(t, len(status), numCount) assertStatus(t, status[0], goose.StatePending, newSource(goose.TypeSQL, "00001_users_table.sql", 1), true) assertStatus(t, status[1], goose.StatePending, newSource(goose.TypeSQL, "00002_posts_table.sql", 2), true) assertStatus(t, status[2], goose.StatePending, newSource(goose.TypeSQL, "00003_comments_table.sql", 3), true) assertStatus(t, status[3], goose.StatePending, newSource(goose.TypeSQL, "00004_insert_data.sql", 4), true) assertStatus(t, status[4], goose.StatePending, newSource(goose.TypeSQL, "00005_posts_view.sql", 5), true) assertStatus(t, status[5], goose.StatePending, newSource(goose.TypeSQL, "00006_empty_up.sql", 6), true) assertStatus(t, status[6], goose.StatePending, newSource(goose.TypeSQL, "00007_empty_up_down.sql", 7), true) // Apply all migrations _, err = p.Up(ctx) require.NoError(t, err) status, err = p.Status(ctx) require.NoError(t, err) require.Equal(t, len(status), numCount) assertStatus(t, status[0], goose.StateApplied, newSource(goose.TypeSQL, "00001_users_table.sql", 1), false) assertStatus(t, status[1], goose.StateApplied, newSource(goose.TypeSQL, "00002_posts_table.sql", 2), false) assertStatus(t, status[2], goose.StateApplied, newSource(goose.TypeSQL, "00003_comments_table.sql", 3), false) assertStatus(t, status[3], goose.StateApplied, newSource(goose.TypeSQL, "00004_insert_data.sql", 4), false) assertStatus(t, status[4], goose.StateApplied, newSource(goose.TypeSQL, "00005_posts_view.sql", 5), false) assertStatus(t, status[5], goose.StateApplied, newSource(goose.TypeSQL, "00006_empty_up.sql", 6), false) assertStatus(t, status[6], goose.StateApplied, newSource(goose.TypeSQL, "00007_empty_up_down.sql", 7), false) }) t.Run("tx_partial_errors", func(t *testing.T) { countOwners := func(db *sql.DB) (int, error) { q := `SELECT count(*)FROM owners` var count int if err := db.QueryRow(q).Scan(&count); err != nil { return 0, err } return count, nil } ctx := context.Background() db := newDB(t) mapFS := fstest.MapFS{ "00001_users_table.sql": newMapFile(` -- +goose Up CREATE TABLE owners ( owner_name TEXT NOT NULL ); `), "00002_partial_error.sql": newMapFile(` -- +goose Up INSERT INTO invalid_table (invalid_table) VALUES ('invalid_value'); `), "00003_insert_data.sql": newMapFile(` -- +goose Up INSERT INTO owners (owner_name) VALUES ('seed-user-1'); INSERT INTO owners (owner_name) VALUES ('seed-user-2'); INSERT INTO owners (owner_name) VALUES ('seed-user-3'); `), } p, err := goose.NewProvider(goose.DialectSQLite3, db, mapFS) require.NoError(t, err) _, err = p.Up(ctx) require.Error(t, err) require.Contains(t, err.Error(), "partial migration error (type:sql,version:2)") expected := new(goose.PartialError) require.ErrorAs(t, err, &expected) // Check Err field require.Contains(t, expected.Err.Error(), "SQL logic error: no such table: invalid_table (1)") // Check Results field require.Len(t, expected.Applied, 1) assertResult(t, expected.Applied[0], newSource(goose.TypeSQL, "00001_users_table.sql", 1), "up", false) // Check Failed field require.NotNil(t, expected.Failed) assertSource(t, expected.Failed.Source, goose.TypeSQL, "00002_partial_error.sql", 2) require.False(t, expected.Failed.Empty) require.Error(t, expected.Failed.Error) require.Contains(t, expected.Failed.Error.Error(), "SQL logic error: no such table: invalid_table (1)") require.Equal(t, "up", expected.Failed.Direction) require.Positive(t, expected.Failed.Duration) // Ensure the partial error did not affect the database. count, err := countOwners(db) require.NoError(t, err) require.Equal(t, 0, count) status, err := p.Status(ctx) require.NoError(t, err) require.Len(t, status, 3) assertStatus(t, status[0], goose.StateApplied, newSource(goose.TypeSQL, "00001_users_table.sql", 1), false) assertStatus(t, status[1], goose.StatePending, newSource(goose.TypeSQL, "00002_partial_error.sql", 2), true) assertStatus(t, status[2], goose.StatePending, newSource(goose.TypeSQL, "00003_insert_data.sql", 3), true) }) } func TestConcurrentProvider(t *testing.T) { t.Parallel() t.Run("up", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) maxVersion := len(p.ListSources()) ch := make(chan int64) var wg sync.WaitGroup for i := 0; i < maxVersion; i++ { wg.Add(1) go func() { defer wg.Done() res, err := p.UpByOne(ctx) if err != nil { t.Error(err) return } if res == nil { t.Errorf("expected non-nil result, got nil") return } ch <- res.Source.Version }() } go func() { wg.Wait() close(ch) }() var versions []int64 for version := range ch { versions = append(versions, version) } // Fail early if any of the goroutines failed. if t.Failed() { return } require.Equal(t, len(versions), maxVersion) for i := 0; i < maxVersion; i++ { require.Equal(t, versions[i], int64(i+1)) } currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, currentVersion, maxVersion) }) t.Run("down", func(t *testing.T) { ctx := context.Background() p, _ := newProviderWithDB(t) maxVersion := len(p.ListSources()) // Apply all migrations _, err := p.Up(ctx) require.NoError(t, err) currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, currentVersion, maxVersion) ch := make(chan []*goose.MigrationResult) var wg sync.WaitGroup for i := 0; i < maxVersion; i++ { wg.Add(1) go func() { defer wg.Done() res, err := p.DownTo(ctx, 0) if err != nil { t.Error(err) return } ch <- res }() } go func() { wg.Wait() close(ch) }() var ( valid [][]*goose.MigrationResult empty [][]*goose.MigrationResult ) for results := range ch { if len(results) == 0 { empty = append(empty, results) continue } valid = append(valid, results) } // Fail early if any of the goroutines failed. if t.Failed() { return } require.Len(t, valid, 1) require.Equal(t, len(empty), maxVersion-1) // Ensure the valid result is correct. require.Equal(t, len(valid[0]), maxVersion) }) } func TestNoVersioning(t *testing.T) { t.Parallel() countSeedOwners := func(db *sql.DB) (int, error) { q := `SELECT count(*)FROM owners WHERE owner_name LIKE'seed-user-%'` var count int if err := db.QueryRow(q).Scan(&count); err != nil { return 0, err } return count, nil } countOwners := func(db *sql.DB) (int, error) { q := `SELECT count(*)FROM owners` var count int if err := db.QueryRow(q).Scan(&count); err != nil { return 0, err } return count, nil } ctx := context.Background() dbName := fmt.Sprintf("test_%s.db", randomAlphaNumeric(8)) db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), dbName)) require.NoError(t, err) fsys := os.DirFS(filepath.Join("testdata", "no-versioning", "migrations")) const ( // Total owners created by the seed files. wantSeedOwnerCount = 250 // These are owners created by migration files. wantOwnerCount = 4 ) p, err := goose.NewProvider(goose.DialectSQLite3, db, fsys, goose.WithVerbose(testing.Verbose()), goose.WithDisableVersioning(false), // This is the default. ) require.Len(t, p.ListSources(), 3) require.NoError(t, err) _, err = p.Up(ctx) require.NoError(t, err) baseVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 3, baseVersion) t.Run("seed-up-down-to-zero", func(t *testing.T) { fsys := os.DirFS(filepath.Join("testdata", "no-versioning", "seed")) p, err := goose.NewProvider(goose.DialectSQLite3, db, fsys, goose.WithVerbose(testing.Verbose()), goose.WithDisableVersioning(true), // Provider with no versioning. ) require.NoError(t, err) require.Len(t, p.ListSources(), 2) // Run (all) up migrations from the seed dir { upResult, err := p.Up(ctx) require.NoError(t, err) require.Len(t, upResult, 2) // When versioning is disabled, we cannot track the version of the seed files. _, err = p.GetDBVersion(ctx) require.Error(t, err) seedOwnerCount, err := countSeedOwners(db) require.NoError(t, err) require.Equal(t, wantSeedOwnerCount, seedOwnerCount) } // Run (all) down migrations from the seed dir { downResult, err := p.DownTo(ctx, 0) require.NoError(t, err) require.Len(t, downResult, 2) // When versioning is disabled, we cannot track the version of the seed files. _, err = p.GetDBVersion(ctx) require.Error(t, err) seedOwnerCount, err := countSeedOwners(db) require.NoError(t, err) require.Equal(t, 0, seedOwnerCount) } // The migrations added 4 non-seed owners, they must remain in the database afterwards ownerCount, err := countOwners(db) require.NoError(t, err) require.Equal(t, wantOwnerCount, ownerCount) }) } func TestAllowMissing(t *testing.T) { t.Parallel() ctx := context.Background() // Developer A and B check out the "main" branch which is currently on version 3. Developer A // mistakenly creates migration 5 and commits. Developer B did not pull the latest changes and // commits migration 4. Oops -- now the migrations are out of order. // // When goose is set to allow missing migrations, then 5 is applied after 4 with no error. // Otherwise it's expected to be an error. t.Run("missing_now_allowed", func(t *testing.T) { db := newDB(t) p, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(), goose.WithAllowOutofOrder(false), ) require.NoError(t, err) // Create and apply first 3 migrations. _, err = p.UpTo(ctx, 3) require.NoError(t, err) currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 3, currentVersion) // Developer A - migration 5 (mistakenly applied) result, err := p.ApplyVersion(ctx, 5, true) require.NoError(t, err) require.EqualValues(t, 5, result.Source.Version) current, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 5, current) // The database has migrations 1,2,3,5 applied. // Developer B is on version 3 (e.g., never pulled the latest changes). Adds migration 4. By // default goose does not allow missing (out-of-order) migrations, which means halt if a // missing migration is detected. _, err = p.Up(ctx) require.Error(t, err) // found 1 missing (out-of-order) migration: [00004_insert_data.sql] require.Contains(t, err.Error(), "missing (out-of-order) migration") // Confirm db version is unchanged. current, err = p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 5, current) _, err = p.UpByOne(ctx) require.Error(t, err) // found 1 missing (out-of-order) migration: [00004_insert_data.sql] require.Contains(t, err.Error(), "missing (out-of-order) migration") // Confirm db version is unchanged. current, err = p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 5, current) _, err = p.UpTo(ctx, math.MaxInt64) require.Error(t, err) // found 1 missing (out-of-order) migration: [00004_insert_data.sql] require.Contains(t, err.Error(), "missing (out-of-order) migration") // Confirm db version is unchanged. current, err = p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 5, current) }) t.Run("missing_allowed", func(t *testing.T) { db := newDB(t) p, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(), goose.WithAllowOutofOrder(true), ) require.NoError(t, err) // Create and apply first 3 migrations. _, err = p.UpTo(ctx, 3) require.NoError(t, err) currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 3, currentVersion) // Developer A - migration 5 (mistakenly applied) { _, err = p.ApplyVersion(ctx, 5, true) require.NoError(t, err) current, err := p.GetDBVersion(ctx) require.NoError(t, err) require.EqualValues(t, 5, current) } // Developer B - migration 4 (missing) and 6 (new) { // 4 upResult, err := p.UpByOne(ctx) require.NoError(t, err) require.NotNil(t, upResult) require.EqualValues(t, 4, upResult.Source.Version) // 6 upResult, err = p.UpByOne(ctx) require.NoError(t, err) require.NotNil(t, upResult) require.EqualValues(t, 6, upResult.Source.Version) count, err := getGooseVersionCount(db, goose.DefaultTablename) require.NoError(t, err) require.EqualValues(t, 6, count) current, err := p.GetDBVersion(ctx) require.NoError(t, err) // Expecting max(version_id) to be 8 require.EqualValues(t, 6, current) } // The applied order in the database is expected to be: // 1,2,3,5,4,6 // So migrating down should be the reverse of the applied order: // 6,4,5,3,2,1 testDownAndVersion := func(wantDBVersion, wantResultVersion int64) { currentVersion, err := p.GetDBVersion(ctx) require.NoError(t, err) require.Equal(t, wantDBVersion, currentVersion) downRes, err := p.Down(ctx) require.NoError(t, err) require.NotNil(t, downRes) require.Equal(t, wantResultVersion, downRes.Source.Version) } // This behaviour may need to change, see the following issues for more details: // - https://github.com/pressly/goose/issues/523 // - https://github.com/pressly/goose/issues/402 testDownAndVersion(6, 6) testDownAndVersion(5, 4) // Ensure the max db version is 5 before down. testDownAndVersion(5, 5) testDownAndVersion(3, 3) testDownAndVersion(2, 2) testDownAndVersion(1, 1) _, err = p.Down(ctx) require.Error(t, err) require.ErrorIs(t, err, goose.ErrNoNextVersion) }) } func TestSQLiteSharedCache(t *testing.T) { t.Parallel() // goose uses *sql.Conn for most operations (incl. creating the initial table), but for Go // migrations when running outside a transaction we use *sql.DB. This is a problem for SQLite // because it does not support shared cache mode by default and it does not see the table that // was created through the initial connection. This test ensures goose works with SQLite shared // cache mode. // // Ref: https://www.sqlite.org/inmemorydb.html // // "In-memory databases are allowed to use shared cache if they are opened using a URI filename. // If the unadorned ":memory:" name is used to specify the in-memory database, then that // database always has a private cache and is only visible to the database connection that // originally opened it. However, the same in-memory database can be opened by two or more // database connections as follows: file::memory:?cache=shared" t.Run("shared_cache", func(t *testing.T) { db, err := sql.Open("sqlite", "file::memory:?cache=shared") require.NoError(t, err) fsys := fstest.MapFS{"00001_a.sql": newMapFile(`-- +goose Up`)} p, err := goose.NewProvider(goose.DialectSQLite3, db, fsys, goose.WithGoMigrations( goose.NewGoMigration(2, &goose.GoFunc{Mode: goose.TransactionDisabled}, nil), ), ) require.NoError(t, err) _, err = p.Up(context.Background()) require.NoError(t, err) }) t.Run("no_shared_cache", func(t *testing.T) { db, err := sql.Open("sqlite", "file::memory:") require.NoError(t, err) fsys := fstest.MapFS{"00001_a.sql": newMapFile(`-- +goose Up`)} p, err := goose.NewProvider(goose.DialectSQLite3, db, fsys, goose.WithGoMigrations( goose.NewGoMigration(2, &goose.GoFunc{Mode: goose.TransactionDisabled}, nil), ), ) require.NoError(t, err) _, err = p.Up(context.Background()) require.Error(t, err) require.Contains(t, err.Error(), "SQL logic error: no such table: goose_db_version") }) } func TestGoMigrationPanic(t *testing.T) { t.Parallel() ctx := context.Background() const ( wantErrString = "panic: runtime error: index out of range [7] with length 0" ) migration := goose.NewGoMigration( 1, &goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error { var ss []int _ = ss[7] return nil }}, nil, ) p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), nil, goose.WithGoMigrations(migration), // Add a Go migration that panics. ) require.NoError(t, err) _, err = p.Up(ctx) require.Error(t, err) expected := new(goose.PartialError) require.ErrorAs(t, err, &expected) require.Contains(t, expected.Err.Error(), wantErrString) } func TestCustomStoreTableExists(t *testing.T) { t.Parallel() db := newDB(t) store, err := database.NewStore(database.DialectSQLite3, goose.DefaultTablename) require.NoError(t, err) for i := 0; i < 2; i++ { p, err := goose.NewProvider("", db, newFsys(), goose.WithStore(&customStoreSQLite3{store}), ) require.NoError(t, err) _, err = p.Up(context.Background()) require.NoError(t, err) } } func TestProviderApply(t *testing.T) { t.Parallel() ctx := context.Background() p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), newFsys()) require.NoError(t, err) _, err = p.ApplyVersion(ctx, 1, true) require.NoError(t, err) // This version has a corresponding down migration, but has never been applied. _, err = p.ApplyVersion(ctx, 2, false) require.Error(t, err) require.ErrorIs(t, err, goose.ErrNotApplied) } func TestPending(t *testing.T) { t.Parallel() t.Run("allow_out_of_order", func(t *testing.T) { ctx := context.Background() fsys := newFsys() p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), fsys, goose.WithAllowOutofOrder(true), ) require.NoError(t, err) // Some migrations have been applied out of order. _, err = p.ApplyVersion(ctx, 1, true) require.NoError(t, err) _, err = p.ApplyVersion(ctx, 3, true) require.NoError(t, err) // Even though the latest migration HAS been applied, there are still pending out-of-order // migrations. current, target, err := p.GetVersions(ctx) require.NoError(t, err) require.EqualValues(t, 3, current) require.Len(t, fsys, int(target)) hasPending, err := p.HasPending(ctx) require.NoError(t, err) require.True(t, hasPending) // Apply the missing migrations. _, err = p.Up(ctx) require.NoError(t, err) // All migrations have been applied. hasPending, err = p.HasPending(ctx) require.NoError(t, err) require.False(t, hasPending) current, target, err = p.GetVersions(ctx) require.NoError(t, err) require.Equal(t, current, target) }) t.Run("disallow_out_of_order", func(t *testing.T) { ctx := context.Background() fsys := newFsys() run := func(t *testing.T, versionToApply int64) { t.Helper() p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), fsys, goose.WithAllowOutofOrder(false), ) require.NoError(t, err) // Some migrations have been applied. _, err = p.ApplyVersion(ctx, 1, true) require.NoError(t, err) _, err = p.ApplyVersion(ctx, versionToApply, true) require.NoError(t, err) // TODO(mf): revisit the pending check behavior in addition to the HasPending // method. current, target, err := p.GetVersions(ctx) require.NoError(t, err) require.Equal(t, current, versionToApply) require.Len(t, fsys, int(target)) _, err = p.HasPending(ctx) require.Error(t, err) require.Contains(t, err.Error(), "missing (out-of-order) migration") _, err = p.Up(ctx) require.Error(t, err) require.Contains(t, err.Error(), "missing (out-of-order) migration") } t.Run("latest_version", func(t *testing.T) { run(t, int64(len(fsys))) }) t.Run("latest_version_minus_one", func(t *testing.T) { run(t, int64(len(fsys)-1)) }) }) } var _ database.StoreExtender = (*customStoreSQLite3)(nil) type customStoreSQLite3 struct{ database.Store } func (s *customStoreSQLite3) TableExists(ctx context.Context, db database.DBTxConn) (bool, error) { q := `SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name=?) AS table_exists` var exists bool if err := db.QueryRowContext(ctx, q, s.Tablename()).Scan(&exists); err != nil { return false, err } return exists, 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 TestGoOnly(t *testing.T) { t.Cleanup(goose.ResetGlobalMigrations) // Not parallel because each subtest modifies global state. countUser := func(db *sql.DB) int { q := `SELECT count(*)FROM users` var count int err := db.QueryRow(q).Scan(&count) require.NoError(t, err) return count } t.Run("with_tx", func(t *testing.T) { ctx := context.Background() register := []*goose.Migration{ goose.NewGoMigration( 1, &goose.GoFunc{RunTx: newTxFn("CREATE TABLE users (id INTEGER PRIMARY KEY)")}, &goose.GoFunc{RunTx: newTxFn("DROP TABLE users")}, ), } err := goose.SetGlobalMigrations(register...) require.NoError(t, err) t.Cleanup(goose.ResetGlobalMigrations) db := newDB(t) register = []*goose.Migration{ goose.NewGoMigration( 2, &goose.GoFunc{RunTx: newTxFn("INSERT INTO users (id) VALUES (1), (2), (3)")}, &goose.GoFunc{RunTx: newTxFn("DELETE FROM users")}, ), } p, err := goose.NewProvider(goose.DialectSQLite3, db, nil, goose.WithGoMigrations(register...), ) require.NoError(t, err) sources := p.ListSources() require.Len(t, p.ListSources(), 2) assertSource(t, sources[0], goose.TypeGo, "", 1) assertSource(t, sources[1], goose.TypeGo, "", 2) // Apply migration 1 res, err := p.UpByOne(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 1), "up", false) require.Equal(t, 0, countUser(db)) require.True(t, tableExists(t, db, "users")) // Apply migration 2 res, err = p.UpByOne(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 2), "up", false) require.Equal(t, 3, countUser(db)) // Rollback migration 2 res, err = p.Down(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 2), "down", false) require.Equal(t, 0, countUser(db)) // Rollback migration 1 res, err = p.Down(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 1), "down", false) // Check table does not exist require.False(t, tableExists(t, db, "users")) }) t.Run("with_db", func(t *testing.T) { ctx := context.Background() register := []*goose.Migration{ goose.NewGoMigration( 1, &goose.GoFunc{ RunDB: newDBFn("CREATE TABLE users (id INTEGER PRIMARY KEY)"), }, &goose.GoFunc{ RunDB: newDBFn("DROP TABLE users"), }, ), } err := goose.SetGlobalMigrations(register...) require.NoError(t, err) t.Cleanup(goose.ResetGlobalMigrations) db := newDB(t) register = []*goose.Migration{ goose.NewGoMigration( 2, &goose.GoFunc{RunDB: newDBFn("INSERT INTO users (id) VALUES (1), (2), (3)")}, &goose.GoFunc{RunDB: newDBFn("DELETE FROM users")}, ), } p, err := goose.NewProvider(goose.DialectSQLite3, db, nil, goose.WithGoMigrations(register...), ) require.NoError(t, err) sources := p.ListSources() require.Len(t, p.ListSources(), 2) assertSource(t, sources[0], goose.TypeGo, "", 1) assertSource(t, sources[1], goose.TypeGo, "", 2) // Apply migration 1 res, err := p.UpByOne(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 1), "up", false) require.Equal(t, 0, countUser(db)) require.True(t, tableExists(t, db, "users")) // Apply migration 2 res, err = p.UpByOne(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 2), "up", false) require.Equal(t, 3, countUser(db)) // Rollback migration 2 res, err = p.Down(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 2), "down", false) require.Equal(t, 0, countUser(db)) // Rollback migration 1 res, err = p.Down(ctx) require.NoError(t, err) assertResult(t, res, newSource(goose.TypeGo, "", 1), "down", false) // Check table does not exist require.False(t, tableExists(t, db, "users")) }) } func newDBFn(query string) func(context.Context, *sql.DB) error { return func(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, query) return err } } func newTxFn(query string) func(context.Context, *sql.Tx) error { return func(ctx context.Context, tx *sql.Tx) error { _, err := tx.ExecContext(ctx, query) return err } } func tableExists(t *testing.T, db *sql.DB, table string) bool { q := fmt.Sprintf(`SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS table_exists FROM sqlite_master WHERE type = 'table' AND name = '%s'`, table) var b string err := db.QueryRow(q).Scan(&b) require.NoError(t, err) return b == "1" } const ( charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) func randomAlphaNumeric(length int) string { b := make([]byte, length) for i := range b { b[i] = charset[rand.Intn(len(charset))] } return string(b) } func newProviderWithDB(t *testing.T, opts ...goose.ProviderOption) (*goose.Provider, *sql.DB) { t.Helper() db := newDB(t) opts = append( opts, goose.WithVerbose(testing.Verbose()), ) p, err := goose.NewProvider(goose.DialectSQLite3, db, newFsys(), opts...) require.NoError(t, err) return p, db } func newDB(t *testing.T) *sql.DB { t.Helper() dbName := fmt.Sprintf("test_%s.db", randomAlphaNumeric(8)) db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), dbName)) require.NoError(t, err) return db } func getMaxVersionID(db *sql.DB, gooseTable string) (int64, error) { var gotVersion int64 if err := db.QueryRow( fmt.Sprintf("select max(version_id) from %s", gooseTable), ).Scan(&gotVersion); err != nil { return 0, err } return gotVersion, nil } func getTableNames(db *sql.DB) ([]string, error) { rows, err := db.Query(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`) if err != nil { return nil, err } defer rows.Close() var tables []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return nil, err } tables = append(tables, name) } if err := rows.Err(); err != nil { return nil, err } return tables, nil } func assertStatus( t *testing.T, got *goose.MigrationStatus, state goose.State, source *goose.Source, appliedIsZero bool, ) { t.Helper() require.Equal(t, got.State, state) require.Equal(t, got.Source, source) require.Equal(t, got.AppliedAt.IsZero(), appliedIsZero) } func assertResult( t *testing.T, got *goose.MigrationResult, source *goose.Source, direction string, isEmpty bool, ) { t.Helper() require.NotNil(t, got) require.Equal(t, got.Source, source) require.Equal(t, got.Direction, direction) require.Equal(t, got.Empty, isEmpty) require.NoError(t, got.Error) require.Positive(t, got.Duration) } func assertSource(t *testing.T, got *goose.Source, typ goose.MigrationType, name string, version int64) { t.Helper() require.Equal(t, got.Type, typ) require.Equal(t, got.Path, name) require.Equal(t, got.Version, version) } func newSource(t goose.MigrationType, fullpath string, version int64) *goose.Source { return &goose.Source{ Type: t, Path: fullpath, Version: version, } } func newMapFile(data string) *fstest.MapFile { return &fstest.MapFile{ Data: []byte(data), } } func newFsys() fstest.MapFS { return fstest.MapFS{ "00001_users_table.sql": newMapFile(runMigration1), "00002_posts_table.sql": newMapFile(runMigration2), "00003_comments_table.sql": newMapFile(runMigration3), "00004_insert_data.sql": newMapFile(runMigration4), "00005_posts_view.sql": newMapFile(runMigration5), "00006_empty_up.sql": newMapFile(runMigration6), "00007_empty_up_down.sql": newMapFile(runMigration7), } } var ( // known tables are the tables (including goose table) created by running all migration files. // If you add a table, make sure to add to this list and keep it in order. knownTables = []string{ "comments", "goose_db_version", "posts", "sqlite_sequence", "users", } runMigration1 = ` -- +goose Up CREATE TABLE users ( id INTEGER PRIMARY KEY, username TEXT NOT NULL, email TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- +goose Down DROP TABLE users; ` runMigration2 = ` -- +goose Up -- +goose StatementBegin CREATE TABLE posts ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, author_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (author_id) REFERENCES users(id) ); -- +goose StatementEnd SELECT 1; SELECT 2; -- +goose Down DROP TABLE posts; ` runMigration3 = ` -- +goose Up CREATE TABLE comments ( id INTEGER PRIMARY KEY, post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (post_id) REFERENCES posts(id), FOREIGN KEY (user_id) REFERENCES users(id) ); -- +goose Down DROP TABLE comments; SELECT 1; SELECT 2; SELECT 3; ` runMigration4 = ` -- +goose Up INSERT INTO users (id, username, email) VALUES (1, 'john_doe', 'john@example.com'), (2, 'jane_smith', 'jane@example.com'), (3, 'alice_wonderland', 'alice@example.com'); INSERT INTO posts (id, title, content, author_id) VALUES (1, 'Introduction to SQL', 'SQL is a powerful language for managing databases...', 1), (2, 'Data Modeling Techniques', 'Choosing the right data model is crucial...', 2), (3, 'Advanced Query Optimization', 'Optimizing queries can greatly improve...', 1); INSERT INTO comments (id, post_id, user_id, content) VALUES (1, 1, 3, 'Great introduction! Looking forward to more.'), (2, 1, 2, 'SQL can be a bit tricky at first, but practice helps.'), (3, 2, 1, 'You covered normalization really well in this post.'); -- +goose Down DELETE FROM comments; DELETE FROM posts; DELETE FROM users; ` runMigration5 = ` -- +goose NO TRANSACTION -- +goose Up CREATE VIEW posts_view AS SELECT p.id, p.title, p.content, p.created_at, u.username AS author FROM posts p JOIN users u ON p.author_id = u.id; -- +goose Down DROP VIEW posts_view; ` runMigration6 = ` -- +goose Up ` runMigration7 = ` -- +goose Up -- +goose Down ` )