diff --git a/provider.go b/provider.go index e7870caeb..80183418d 100644 --- a/provider.go +++ b/provider.go @@ -409,8 +409,16 @@ func (p *Provider) apply( if err != nil && !errors.Is(err, database.ErrVersionNotFound) { return nil, err } - // If the migration has already been applied, return an error. But, if the migration is being - // rolled back, we allow the individual migration to be applied again. + // There are a few states here: + // 1. direction is up + // a. migration is applied, this is an error (ErrAlreadyApplied) + // b. migration is not applied, apply it + // 2. direction is down + // a. migration is applied, rollback + // b. migration is not applied, this is an error (ErrNotApplied) + if result == nil && !direction { + return nil, fmt.Errorf("version %d: %w", version, ErrNotApplied) + } if result != nil && direction { return nil, fmt.Errorf("version %d: %w", version, ErrAlreadyApplied) } diff --git a/provider_errors.go b/provider_errors.go index 72c8d672c..79a2cda2b 100644 --- a/provider_errors.go +++ b/provider_errors.go @@ -6,15 +6,21 @@ import ( ) var ( - // ErrVersionNotFound when a migration version is not found. + // ErrVersionNotFound is returned when a specific migration version is not located. This can + // occur if a .sql file or a Go migration function for the specified version is missing. ErrVersionNotFound = errors.New("version not found") - // ErrAlreadyApplied when a migration has already been applied. - ErrAlreadyApplied = errors.New("already applied") - // ErrNoMigrations is returned by [NewProvider] when no migrations are found. ErrNoMigrations = errors.New("no migrations found") + // ErrAlreadyApplied indicates that the migration cannot be applied because it has already been + // executed. This error is returned by [Provider.Apply]. + ErrAlreadyApplied = errors.New("migration already applied") + + // ErrNotApplied indicates that the rollback cannot be performed because the migration has not + // yet been applied. This error is returned by [Provider.Apply]. + ErrNotApplied = errors.New("migration not applied") + // errInvalidVersion is returned when a migration version is invalid. errInvalidVersion = errors.New("version must be greater than 0") ) diff --git a/provider_run_test.go b/provider_run_test.go index e6926ea16..efa8d7091 100644 --- a/provider_run_test.go +++ b/provider_run_test.go @@ -262,7 +262,7 @@ func TestProviderRun(t *testing.T) { _, err = p.ApplyVersion(ctx, 1, true) check.HasError(t, err) check.Bool(t, errors.Is(err, goose.ErrAlreadyApplied), true) - check.Contains(t, err.Error(), "version 1: already applied") + check.Contains(t, err.Error(), "version 1: migration already applied") }) t.Run("status", func(t *testing.T) { ctx := context.Background() @@ -765,6 +765,20 @@ func TestCustomStoreTableExists(t *testing.T) { check.NoError(t, err) } +func TestProviderApply(t *testing.T) { + t.Parallel() + + ctx := context.Background() + p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), newFsys()) + check.NoError(t, err) + _, err = p.ApplyVersion(ctx, 1, true) + check.NoError(t, err) + // This version has a corresponding down migration, but has never been applied. + _, err = p.ApplyVersion(ctx, 2, false) + check.HasError(t, err) + check.Bool(t, errors.Is(err, goose.ErrNotApplied), true) +} + type customStoreSQLite3 struct { database.Store } diff --git a/provider_types.go b/provider_types.go index 1b4983b2b..d8bef1673 100644 --- a/provider_types.go +++ b/provider_types.go @@ -1,6 +1,10 @@ package goose -import "time" +import ( + "fmt" + "path/filepath" + "time" +) // MigrationType is the type of migration. type MigrationType string @@ -32,6 +36,19 @@ type MigrationResult struct { Error error } +func (m *MigrationResult) String() string { + state := "OK" + if m.Empty { + state = "EMPTY" + } + return fmt.Sprintf("%-6s %-4s %s (%s)", + state, + m.Direction, + filepath.Base(m.Source.Path), + truncateDuration(m.Duration), + ) +} + // State represents the state of a migration. type State string