Skip to content

Commit

Permalink
Adds support to apply migrations without versioning (#291)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfridman authored Dec 13, 2021
1 parent fd1ba04 commit c8aa123
Show file tree
Hide file tree
Showing 20 changed files with 416 additions and 92 deletions.
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Goose supports [embedding SQL migrations](#embedded-sql-migrations), which means
- goose pkg doesn't have any vendor dependencies anymore
- We use timestamped migrations by default but recommend a hybrid approach of using timestamps in the development process and sequential versions in production.
- Supports missing (out-of-order) migrations with the `-allow-missing` flag, or if using as a library supply the functional option `goose.WithAllowMissing()` to Up, UpTo or UpByOne.
- Supports applying ad-hoc migrations without tracking them in the schema table. Useful for seeding a database after migrations have been applied. Use `-no-versioning` flag or the functional option `goose.WithNoVersioning()`.

# Install

Expand All @@ -41,6 +42,9 @@ For a lite version of the binary without DB connection dependent commands, use t

$ go build -tags='no_postgres no_mysql no_sqlite3' -i -o goose ./cmd/goose

For macOS users `goose` is available as a [Homebrew Formulae](https://formulae.brew.sh/formula/goose#default):

$ brew install goose

# Usage

Expand Down Expand Up @@ -70,23 +74,26 @@ Examples:
goose mssql "sqlserver://user:password@dbname:1433?database=master" status
Options:
-allow-missing
applies missing (out-of-order) migrations
applies missing (out-of-order) migrations
-certfile string
file path to root CA's certificates in pem format (only support on mysql)
-sslcert string
file path to SSL certificates in pem format (only support on mysql)
-sslkey string
file path to SSL key in pem format (only support on mysql)
file path to root CA's certificates in pem format (only support on mysql)
-dir string
directory with migration files (default ".")
-h print help
-s use sequential numbering for new migrations
directory with migration files (default ".")
-h print help
-no-versioning
apply migration commands with no versioning, in file order, from directory pointed to
-s use sequential numbering for new migrations
-ssl-cert string
file path to SSL certificates in pem format (only support on mysql)
-ssl-key string
file path to SSL key in pem format (only support on mysql)
-table string
migrations table name (default "goose_db_version")
-v enable verbose mode
migrations table name (default "goose_db_version")
-v enable verbose mode
-version
print version
print version
Commands:
up Migrate the DB to the most recent version available
Expand Down
4 changes: 4 additions & 0 deletions cmd/goose/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var (
allowMissing = flags.Bool("allow-missing", false, "applies missing (out-of-order) migrations")
sslcert = flags.String("ssl-cert", "", "file path to SSL certificates in pem format (only support on mysql)")
sslkey = flags.String("ssl-key", "", "file path to SSL key in pem format (only support on mysql)")
noVersioning = flags.Bool("no-versioning", false, "apply migration commands with no versioning, in file order, from directory pointed to")
)

var (
Expand Down Expand Up @@ -99,6 +100,9 @@ func main() {
if *allowMissing {
options = append(options, goose.WithAllowMissing())
}
if *noVersioning {
options = append(options, goose.WithNoVersioning())
}
if err := goose.RunWithOptions(
command,
db,
Expand Down
48 changes: 41 additions & 7 deletions down.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,47 @@ import (
)

// Down rolls back a single migration from the current version.
func Down(db *sql.DB, dir string) error {
currentVersion, err := GetDBVersion(db)
func Down(db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}

migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if option.noVersioning {
if len(migrations) == 0 {
return nil
}
currentVersion := migrations[len(migrations)-1].Version
// Migrate only the latest migration down.
return downToNoVersioning(db, migrations, currentVersion-1)
}
currentVersion, err := GetDBVersion(db)
if err != nil {
return err
}

current, err := migrations.Current(currentVersion)
if err != nil {
return fmt.Errorf("no migration %v", currentVersion)
}

return current.Down(db)
}

// DownTo rolls back migrations to a specific version.
func DownTo(db *sql.DB, dir string, version int64) error {
func DownTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}
if option.noVersioning {
return downToNoVersioning(db, migrations, version)
}

for {
currentVersion, err := GetDBVersion(db)
Expand All @@ -54,3 +70,21 @@ func DownTo(db *sql.DB, dir string, version int64) error {
}
}
}

// downToNoVersioning applies down migrations down to, but not including, the
// target version.
func downToNoVersioning(db *sql.DB, migrations Migrations, version int64) error {
var finalVersion int64
for i := len(migrations) - 1; i >= 0; i-- {
if version >= migrations[i].Version {
finalVersion = migrations[i].Version
break
}
migrations[i].noVersioning = true
if err := migrations[i].Down(db); err != nil {
return err
}
}
log.Printf("goose: down to current file version: %d\n", finalVersion)
return nil
}
12 changes: 6 additions & 6 deletions goose.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio
return err
}
case "down":
if err := Down(db, dir); err != nil {
if err := Down(db, dir, options...); err != nil {
return err
}
case "down-to":
Expand All @@ -93,27 +93,27 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio
if err != nil {
return fmt.Errorf("version must be a number (got '%s')", args[0])
}
if err := DownTo(db, dir, version); err != nil {
if err := DownTo(db, dir, version, options...); err != nil {
return err
}
case "fix":
if err := Fix(dir); err != nil {
return err
}
case "redo":
if err := Redo(db, dir); err != nil {
if err := Redo(db, dir, options...); err != nil {
return err
}
case "reset":
if err := Reset(db, dir); err != nil {
if err := Reset(db, dir, options...); err != nil {
return err
}
case "status":
if err := Status(db, dir); err != nil {
if err := Status(db, dir, options...); err != nil {
return err
}
case "version":
if err := Version(db, dir); err != nil {
if err := Version(db, dir, options...); err != nil {
return err
}
default:
Expand Down
38 changes: 20 additions & 18 deletions migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ type MigrationRecord struct {

// Migration struct.
type Migration struct {
Version int64
Next int64 // next version, or -1 if none
Previous int64 // previous version, -1 if none
Source string // path to .sql script or go file
Registered bool
UpFn func(*sql.Tx) error // Up go migration function
DownFn func(*sql.Tx) error // Down go migration function
Version int64
Next int64 // next version, or -1 if none
Previous int64 // previous version, -1 if none
Source string // path to .sql script or go file
Registered bool
UpFn func(*sql.Tx) error // Up go migration function
DownFn func(*sql.Tx) error // Down go migration function
noVersioning bool
}

func (m *Migration) String() string {
Expand Down Expand Up @@ -63,7 +64,7 @@ func (m *Migration) run(db *sql.DB, direction bool) error {
return errors.Wrapf(err, "ERROR %v: failed to parse SQL migration file", filepath.Base(m.Source))
}

if err := runSQLMigration(db, statements, useTx, m.Version, direction); err != nil {
if err := runSQLMigration(db, statements, useTx, m.Version, direction, m.noVersioning); err != nil {
return errors.Wrapf(err, "ERROR %v: failed to run SQL migration", filepath.Base(m.Source))
}

Expand Down Expand Up @@ -94,16 +95,17 @@ func (m *Migration) run(db *sql.DB, direction bool) error {
return errors.Wrapf(err, "ERROR %v: failed to run Go migration function %T", filepath.Base(m.Source), fn)
}
}

if direction {
if _, err := tx.Exec(GetDialect().insertVersionSQL(), m.Version, direction); err != nil {
tx.Rollback()
return errors.Wrap(err, "ERROR failed to execute transaction")
}
} else {
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), m.Version); err != nil {
tx.Rollback()
return errors.Wrap(err, "ERROR failed to execute transaction")
if !m.noVersioning {
if direction {
if _, err := tx.Exec(GetDialect().insertVersionSQL(), m.Version, direction); err != nil {
tx.Rollback()
return errors.Wrap(err, "ERROR failed to execute transaction")
}
} else {
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), m.Version); err != nil {
tx.Rollback()
return errors.Wrap(err, "ERROR failed to execute transaction")
}
}
}

Expand Down
42 changes: 23 additions & 19 deletions migration_sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
//
// All statements following an Up or Down directive are grouped together
// until another direction directive is found.
func runSQLMigration(db *sql.DB, statements []string, useTx bool, v int64, direction bool) error {
func runSQLMigration(db *sql.DB, statements []string, useTx bool, v int64, direction bool, noVersioning bool) error {
if useTx {
// TRANSACTION.

Expand All @@ -35,17 +35,19 @@ func runSQLMigration(db *sql.DB, statements []string, useTx bool, v int64, direc
}
}

if direction {
if _, err := tx.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
verboseInfo("Rollback transaction")
tx.Rollback()
return errors.Wrap(err, "failed to insert new goose version")
}
} else {
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
verboseInfo("Rollback transaction")
tx.Rollback()
return errors.Wrap(err, "failed to delete goose version")
if !noVersioning {
if direction {
if _, err := tx.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
verboseInfo("Rollback transaction")
tx.Rollback()
return errors.Wrap(err, "failed to insert new goose version")
}
} else {
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
verboseInfo("Rollback transaction")
tx.Rollback()
return errors.Wrap(err, "failed to delete goose version")
}
}
}

Expand All @@ -64,13 +66,15 @@ func runSQLMigration(db *sql.DB, statements []string, useTx bool, v int64, direc
return errors.Wrapf(err, "failed to execute SQL query %q", clearStatement(query))
}
}
if direction {
if _, err := db.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
return errors.Wrap(err, "failed to insert new goose version")
}
} else {
if _, err := db.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
return errors.Wrap(err, "failed to delete goose version")
if !noVersioning {
if direction {
if _, err := db.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
return errors.Wrap(err, "failed to insert new goose version")
}
} else {
if _, err := db.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
return errors.Wrap(err, "failed to delete goose version")
}
}
}

Expand Down
25 changes: 18 additions & 7 deletions redo.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,40 @@ import (
)

// Redo rolls back the most recently applied migration, then runs it again.
func Redo(db *sql.DB, dir string) error {
currentVersion, err := GetDBVersion(db)
if err != nil {
return err
func Redo(db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}

migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return err
}
var (
currentVersion int64
)
if option.noVersioning {
if len(migrations) == 0 {
return nil
}
currentVersion = migrations[len(migrations)-1].Version
} else {
if currentVersion, err = GetDBVersion(db); err != nil {
return err
}
}

current, err := migrations.Current(currentVersion)
if err != nil {
return err
}
current.noVersioning = option.noVersioning

if err := current.Down(db); err != nil {
return err
}

if err := current.Up(db); err != nil {
return err
}

return nil
}
10 changes: 9 additions & 1 deletion reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import (
)

// Reset rolls back all migrations
func Reset(db *sql.DB, dir string) error {
func Reset(db *sql.DB, dir string, opts ...OptionsFunc) error {
option := &options{}
for _, f := range opts {
f(option)
}
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
if err != nil {
return errors.Wrap(err, "failed to collect migrations")
}
if option.noVersioning {
return DownTo(db, dir, minVersion, opts...)
}

statuses, err := dbMigrationsStatus(db)
if err != nil {
return errors.Wrap(err, "failed to get status of migrations")
Expand Down
Loading

0 comments on commit c8aa123

Please sign in to comment.