mirror of https://github.com/pressly/goose.git
parent
fd1ba04fc8
commit
c8aa123e31
31
README.md
31
README.md
|
@ -30,6 +30,7 @@ Goose supports [embedding SQL migrations](#embedded-sql-migrations), which means
|
||||||
- goose pkg doesn't have any vendor dependencies anymore
|
- 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.
|
- 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 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
|
# Install
|
||||||
|
|
||||||
|
@ -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
|
$ 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
|
# Usage
|
||||||
|
|
||||||
|
@ -70,23 +74,26 @@ Examples:
|
||||||
goose mssql "sqlserver://user:password@dbname:1433?database=master" status
|
goose mssql "sqlserver://user:password@dbname:1433?database=master" status
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-allow-missing
|
-allow-missing
|
||||||
applies missing (out-of-order) migrations
|
applies missing (out-of-order) migrations
|
||||||
-certfile string
|
-certfile string
|
||||||
file path to root CA's certificates in pem format (only support on mysql)
|
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)
|
|
||||||
-dir string
|
-dir string
|
||||||
directory with migration files (default ".")
|
directory with migration files (default ".")
|
||||||
-h print help
|
-h print help
|
||||||
-s use sequential numbering for new migrations
|
-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
|
-table string
|
||||||
migrations table name (default "goose_db_version")
|
migrations table name (default "goose_db_version")
|
||||||
-v enable verbose mode
|
-v enable verbose mode
|
||||||
-version
|
-version
|
||||||
print version
|
print version
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
up Migrate the DB to the most recent version available
|
up Migrate the DB to the most recent version available
|
||||||
|
|
|
@ -22,6 +22,7 @@ var (
|
||||||
allowMissing = flags.Bool("allow-missing", false, "applies missing (out-of-order) migrations")
|
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)")
|
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)")
|
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 (
|
var (
|
||||||
|
@ -99,6 +100,9 @@ func main() {
|
||||||
if *allowMissing {
|
if *allowMissing {
|
||||||
options = append(options, goose.WithAllowMissing())
|
options = append(options, goose.WithAllowMissing())
|
||||||
}
|
}
|
||||||
|
if *noVersioning {
|
||||||
|
options = append(options, goose.WithNoVersioning())
|
||||||
|
}
|
||||||
if err := goose.RunWithOptions(
|
if err := goose.RunWithOptions(
|
||||||
command,
|
command,
|
||||||
db,
|
db,
|
||||||
|
|
52
down.go
52
down.go
|
@ -6,31 +6,47 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Down rolls back a single migration from the current version.
|
// Down rolls back a single migration from the current version.
|
||||||
func Down(db *sql.DB, dir string) error {
|
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
|
||||||
|
}
|
||||||
|
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)
|
currentVersion, err := GetDBVersion(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
current, err := migrations.Current(currentVersion)
|
current, err := migrations.Current(currentVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("no migration %v", currentVersion)
|
return fmt.Errorf("no migration %v", currentVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
return current.Down(db)
|
return current.Down(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownTo rolls back migrations to a specific version.
|
// 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)
|
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if option.noVersioning {
|
||||||
|
return downToNoVersioning(db, migrations, version)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
currentVersion, err := GetDBVersion(db)
|
currentVersion, err := GetDBVersion(db)
|
||||||
|
@ -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
goose.go
12
goose.go
|
@ -81,7 +81,7 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "down":
|
case "down":
|
||||||
if err := Down(db, dir); err != nil {
|
if err := Down(db, dir, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "down-to":
|
case "down-to":
|
||||||
|
@ -93,7 +93,7 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("version must be a number (got '%s')", args[0])
|
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
|
return err
|
||||||
}
|
}
|
||||||
case "fix":
|
case "fix":
|
||||||
|
@ -101,19 +101,19 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "redo":
|
case "redo":
|
||||||
if err := Redo(db, dir); err != nil {
|
if err := Redo(db, dir, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "reset":
|
case "reset":
|
||||||
if err := Reset(db, dir); err != nil {
|
if err := Reset(db, dir, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "status":
|
case "status":
|
||||||
if err := Status(db, dir); err != nil {
|
if err := Status(db, dir, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "version":
|
case "version":
|
||||||
if err := Version(db, dir); err != nil {
|
if err := Version(db, dir, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
38
migration.go
38
migration.go
|
@ -20,13 +20,14 @@ type MigrationRecord struct {
|
||||||
|
|
||||||
// Migration struct.
|
// Migration struct.
|
||||||
type Migration struct {
|
type Migration struct {
|
||||||
Version int64
|
Version int64
|
||||||
Next int64 // next version, or -1 if none
|
Next int64 // next version, or -1 if none
|
||||||
Previous int64 // previous version, -1 if none
|
Previous int64 // previous version, -1 if none
|
||||||
Source string // path to .sql script or go file
|
Source string // path to .sql script or go file
|
||||||
Registered bool
|
Registered bool
|
||||||
UpFn func(*sql.Tx) error // Up go migration function
|
UpFn func(*sql.Tx) error // Up go migration function
|
||||||
DownFn func(*sql.Tx) error // Down go migration function
|
DownFn func(*sql.Tx) error // Down go migration function
|
||||||
|
noVersioning bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Migration) String() string {
|
func (m *Migration) String() string {
|
||||||
|
@ -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))
|
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))
|
return errors.Wrapf(err, "ERROR %v: failed to run SQL migration", filepath.Base(m.Source))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
return errors.Wrapf(err, "ERROR %v: failed to run Go migration function %T", filepath.Base(m.Source), fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !m.noVersioning {
|
||||||
if direction {
|
if direction {
|
||||||
if _, err := tx.Exec(GetDialect().insertVersionSQL(), m.Version, direction); err != nil {
|
if _, err := tx.Exec(GetDialect().insertVersionSQL(), m.Version, direction); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return errors.Wrap(err, "ERROR failed to execute transaction")
|
return errors.Wrap(err, "ERROR failed to execute transaction")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), m.Version); err != nil {
|
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), m.Version); err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return errors.Wrap(err, "ERROR failed to execute transaction")
|
return errors.Wrap(err, "ERROR failed to execute transaction")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
//
|
//
|
||||||
// All statements following an Up or Down directive are grouped together
|
// All statements following an Up or Down directive are grouped together
|
||||||
// until another direction directive is found.
|
// 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 {
|
if useTx {
|
||||||
// TRANSACTION.
|
// TRANSACTION.
|
||||||
|
|
||||||
|
@ -35,17 +35,19 @@ func runSQLMigration(db *sql.DB, statements []string, useTx bool, v int64, direc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if direction {
|
if !noVersioning {
|
||||||
if _, err := tx.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
|
if direction {
|
||||||
verboseInfo("Rollback transaction")
|
if _, err := tx.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
|
||||||
tx.Rollback()
|
verboseInfo("Rollback transaction")
|
||||||
return errors.Wrap(err, "failed to insert new goose version")
|
tx.Rollback()
|
||||||
}
|
return errors.Wrap(err, "failed to insert new goose version")
|
||||||
} else {
|
}
|
||||||
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
|
} else {
|
||||||
verboseInfo("Rollback transaction")
|
if _, err := tx.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
|
||||||
tx.Rollback()
|
verboseInfo("Rollback transaction")
|
||||||
return errors.Wrap(err, "failed to delete goose version")
|
tx.Rollback()
|
||||||
|
return errors.Wrap(err, "failed to delete goose version")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
return errors.Wrapf(err, "failed to execute SQL query %q", clearStatement(query))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if direction {
|
if !noVersioning {
|
||||||
if _, err := db.Exec(GetDialect().insertVersionSQL(), v, direction); err != nil {
|
if direction {
|
||||||
return errors.Wrap(err, "failed to insert new goose version")
|
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 {
|
} else {
|
||||||
return errors.Wrap(err, "failed to delete goose version")
|
if _, err := db.Exec(GetDialect().deleteVersionSQL(), v); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to delete goose version")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
redo.go
25
redo.go
|
@ -5,29 +5,40 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Redo rolls back the most recently applied migration, then runs it again.
|
// Redo rolls back the most recently applied migration, then runs it again.
|
||||||
func Redo(db *sql.DB, dir string) error {
|
func Redo(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
||||||
currentVersion, err := GetDBVersion(db)
|
option := &options{}
|
||||||
if err != nil {
|
for _, f := range opts {
|
||||||
return err
|
f(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
current, err := migrations.Current(currentVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
current.noVersioning = option.noVersioning
|
||||||
|
|
||||||
if err := current.Down(db); err != nil {
|
if err := current.Down(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := current.Up(db); err != nil {
|
if err := current.Up(db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
10
reset.go
10
reset.go
|
@ -8,11 +8,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset rolls back all migrations
|
// 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)
|
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to collect migrations")
|
return errors.Wrap(err, "failed to collect migrations")
|
||||||
}
|
}
|
||||||
|
if option.noVersioning {
|
||||||
|
return DownTo(db, dir, minVersion, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
statuses, err := dbMigrationsStatus(db)
|
statuses, err := dbMigrationsStatus(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to get status of migrations")
|
return errors.Wrap(err, "failed to get status of migrations")
|
||||||
|
|
15
status.go
15
status.go
|
@ -9,12 +9,23 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status prints the status of all migrations.
|
// Status prints the status of all migrations.
|
||||||
func Status(db *sql.DB, dir string) error {
|
func Status(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
||||||
// collect all migrations
|
option := &options{}
|
||||||
|
for _, f := range opts {
|
||||||
|
f(option)
|
||||||
|
}
|
||||||
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to collect migrations")
|
return errors.Wrap(err, "failed to collect migrations")
|
||||||
}
|
}
|
||||||
|
if option.noVersioning {
|
||||||
|
log.Println(" Applied At Migration")
|
||||||
|
log.Println(" =======================================")
|
||||||
|
for _, current := range migrations {
|
||||||
|
log.Printf(" %-24s -- %v\n", "no versioning", filepath.Base(current.Source))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// must ensure that the version table exists if we're running on a pristine DB
|
// must ensure that the version table exists if we're running on a pristine DB
|
||||||
if _, err := EnsureDBVersion(db); err != nil {
|
if _, err := EnsureDBVersion(db); err != nil {
|
||||||
|
|
|
@ -51,6 +51,8 @@ var (
|
||||||
// migrationsDir is a global that points to a ./testdata/{dialect}/migrations folder.
|
// migrationsDir is a global that points to a ./testdata/{dialect}/migrations folder.
|
||||||
// It is set in TestMain based on the current dialect.
|
// It is set in TestMain based on the current dialect.
|
||||||
migrationsDir = ""
|
migrationsDir = ""
|
||||||
|
// seedDir is similar to migrationsDir but contains seed data
|
||||||
|
seedDir = ""
|
||||||
|
|
||||||
// known tables are the tables (including goose table) created by
|
// known tables are the tables (including goose table) created by
|
||||||
// running all migration files. If you add a table, make sure to
|
// running all migration files. If you add a table, make sure to
|
||||||
|
@ -74,6 +76,7 @@ func TestMain(m *testing.M) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
migrationsDir = filepath.Join("testdata", *dialect, "migrations")
|
migrationsDir = filepath.Join("testdata", *dialect, "migrations")
|
||||||
|
seedDir = filepath.Join("testdata", *dialect, "seed")
|
||||||
|
|
||||||
exitCode := m.Run()
|
exitCode := m.Run()
|
||||||
// Useful for debugging test services.
|
// Useful for debugging test services.
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNoVersioning(t *testing.T) {
|
||||||
|
if *dialect != dialectPostgres {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
const (
|
||||||
|
// Total owners created by the seed files.
|
||||||
|
wantSeedOwnerCount = 250
|
||||||
|
// These are owners created by migration files.
|
||||||
|
wantOwnerCount = 4
|
||||||
|
)
|
||||||
|
is := is.New(t)
|
||||||
|
db, err := newDockerDB(t)
|
||||||
|
is.NoErr(err)
|
||||||
|
goose.SetDialect(*dialect)
|
||||||
|
|
||||||
|
err = goose.Up(db, migrationsDir)
|
||||||
|
is.NoErr(err)
|
||||||
|
baseVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
t.Run("seed-up-down-to-zero", func(t *testing.T) {
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
// Run (all) up migrations from the seed dir
|
||||||
|
{
|
||||||
|
err = goose.Up(db, seedDir, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, wantSeedOwnerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run (all) down migrations from the seed dir
|
||||||
|
{
|
||||||
|
err = goose.DownTo(db, seedDir, 0, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The migrations added 4 non-seed owners, they must remain
|
||||||
|
// in the database afterwards
|
||||||
|
ownerCount, err := countOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(ownerCount, wantOwnerCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test-seed-up-reset", func(t *testing.T) {
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
// Run (all) up migrations from the seed dir
|
||||||
|
{
|
||||||
|
err = goose.Up(db, seedDir, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, wantSeedOwnerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run reset (effectively the same as down-to 0)
|
||||||
|
{
|
||||||
|
err = goose.Reset(db, seedDir, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The migrations added 4 non-seed owners, they must remain
|
||||||
|
// in the database afterwards
|
||||||
|
ownerCount, err := countOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(ownerCount, wantOwnerCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test-seed-up-redo", func(t *testing.T) {
|
||||||
|
is := is.NewRelaxed(t)
|
||||||
|
// Run (all) up migrations from the seed dir
|
||||||
|
{
|
||||||
|
err = goose.Up(db, seedDir, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, wantSeedOwnerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run reset (effectively the same as down-to 0)
|
||||||
|
{
|
||||||
|
err = goose.Redo(db, seedDir, goose.WithNoVersioning())
|
||||||
|
is.NoErr(err)
|
||||||
|
// Confirm no changes to the versioned schema in the DB
|
||||||
|
currentVersion, err := goose.GetDBVersion(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(baseVersion, currentVersion)
|
||||||
|
seedOwnerCount, err := countSeedOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(seedOwnerCount, wantSeedOwnerCount) // owners should be unchanged
|
||||||
|
}
|
||||||
|
|
||||||
|
// The migrations added 4 non-seed owners, they must remain
|
||||||
|
// in the database afterwards along with the 250 seed owners for a
|
||||||
|
// total of 254.
|
||||||
|
ownerCount, err := countOwners(db)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(ownerCount, wantOwnerCount+wantSeedOwnerCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func countSeedOwners(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func countOwners(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
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
INSERT INTO owners(owner_id, owner_name, owner_type)
|
INSERT INTO owners(owner_name, owner_type)
|
||||||
VALUES (1, 'lucas', 'user'), (2, 'space', 'organization');
|
VALUES ('lucas', 'user'), ('space', 'organization');
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
DELETE FROM owners WHERE owner_id IN (1, 2);
|
DELETE FROM owners;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
INSERT INTO owners(owner_id, owner_name, owner_type)
|
INSERT INTO owners(owner_name, owner_type)
|
||||||
VALUES (3, 'james', 'user'), (4, 'pressly', 'organization');
|
VALUES ('james', 'user'), ('pressly', 'organization');
|
||||||
|
|
||||||
INSERT INTO repos(repo_id, repo_full_name, repo_owner_id)
|
INSERT INTO repos(repo_full_name, repo_owner_id)
|
||||||
VALUES (1, 'james/rover', 3), (2, 'pressly/goose', 4);
|
VALUES ('james/rover', 3), ('pressly/goose', 4);
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
|
@ -9,7 +9,7 @@ CREATE TABLE owners (
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS repos (
|
CREATE TABLE IF NOT EXISTS repos (
|
||||||
repo_id bigint UNIQUE NOT NULL,
|
repo_id BIGSERIAL NOT NULL,
|
||||||
repo_full_name text NOT NULL,
|
repo_full_name text NOT NULL,
|
||||||
repo_owner_id bigint NOT NULL REFERENCES owners(owner_id) ON DELETE CASCADE,
|
repo_owner_id bigint NOT NULL REFERENCES owners(owner_id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
INSERT INTO owners(owner_id, owner_name, owner_type)
|
INSERT INTO owners(owner_name, owner_type)
|
||||||
VALUES (1, 'lucas', 'user'), (2, 'space', 'organization');
|
VALUES ('lucas', 'user'), ('space', 'organization');
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
DELETE FROM owners WHERE owner_id IN (1, 2);
|
DELETE FROM owners;
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
-- +goose Up
|
-- +goose Up
|
||||||
-- +goose StatementBegin
|
-- +goose StatementBegin
|
||||||
INSERT INTO owners(owner_id, owner_name, owner_type)
|
INSERT INTO owners(owner_name, owner_type)
|
||||||
VALUES (3, 'james', 'user'), (4, 'pressly', 'organization');
|
VALUES ('james', 'user'), ('pressly', 'organization');
|
||||||
|
|
||||||
INSERT INTO repos(repo_id, repo_full_name, repo_owner_id)
|
INSERT INTO repos(repo_full_name, repo_owner_id)
|
||||||
VALUES (1, 'james/rover', 3), (2, 'pressly/goose', 4);
|
VALUES ('james/rover', 3), ('pressly/goose', 4);
|
||||||
-- +goose StatementEnd
|
-- +goose StatementEnd
|
||||||
|
|
||||||
-- +goose Down
|
-- +goose Down
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
|
||||||
|
-- Insert 100 owners.
|
||||||
|
INSERT INTO owners (owner_name, owner_type)
|
||||||
|
SELECT
|
||||||
|
'seed-user-' || i,
|
||||||
|
(SELECT('{user,organization}'::owner_type []) [MOD(i, 2)+1])
|
||||||
|
FROM
|
||||||
|
generate_series(1, 100) s (i);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
-- NOTE: there are 4 existing users from the migrations, that's why owner_id starts at 5
|
||||||
|
DELETE FROM owners where owner_name LIKE 'seed-user-%' AND owner_id BETWEEN 5 AND 104;
|
||||||
|
SELECT setval('owners_owner_id_seq', COALESCE((SELECT MAX(owner_id)+1 FROM owners), 1), false);
|
||||||
|
-- +goose StatementEnd
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
-- Insert 150 more owners.
|
||||||
|
INSERT INTO owners (owner_name, owner_type)
|
||||||
|
SELECT
|
||||||
|
'seed-user-' || i,
|
||||||
|
(SELECT('{user,organization}'::owner_type []) [MOD(i, 2)+1])
|
||||||
|
FROM
|
||||||
|
generate_series(101, 250) s (i);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- NOTE: there are 4 migration owners and 100 seed owners, that's why owner_id starts at 105
|
||||||
|
DELETE FROM owners where owner_name LIKE 'seed-user-%' AND owner_id BETWEEN 105 AND 254;
|
||||||
|
SELECT setval('owners_owner_id_seq', max(owner_id)) FROM owners;
|
42
up.go
42
up.go
|
@ -11,6 +11,7 @@ import (
|
||||||
type options struct {
|
type options struct {
|
||||||
allowMissing bool
|
allowMissing bool
|
||||||
applyUpByOne bool
|
applyUpByOne bool
|
||||||
|
noVersioning bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionsFunc func(o *options)
|
type OptionsFunc func(o *options)
|
||||||
|
@ -19,15 +20,16 @@ func WithAllowMissing() OptionsFunc {
|
||||||
return func(o *options) { o.allowMissing = true }
|
return func(o *options) { o.allowMissing = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithNoVersioning() OptionsFunc {
|
||||||
|
return func(o *options) { o.noVersioning = true }
|
||||||
|
}
|
||||||
|
|
||||||
func withApplyUpByOne() OptionsFunc {
|
func withApplyUpByOne() OptionsFunc {
|
||||||
return func(o *options) { o.applyUpByOne = true }
|
return func(o *options) { o.applyUpByOne = true }
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpTo migrates up to a specific version.
|
// UpTo migrates up to a specific version.
|
||||||
func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
||||||
if _, err := EnsureDBVersion(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
option := &options{}
|
option := &options{}
|
||||||
for _, f := range opts {
|
for _, f := range opts {
|
||||||
f(option)
|
f(option)
|
||||||
|
@ -36,6 +38,22 @@ func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if option.noVersioning {
|
||||||
|
if len(foundMigrations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if option.applyUpByOne {
|
||||||
|
// For up-by-one this means keep re-applying the first
|
||||||
|
// migration over and over.
|
||||||
|
version = foundMigrations[0].Version
|
||||||
|
}
|
||||||
|
return upToNoVersioning(db, foundMigrations, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := EnsureDBVersion(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
dbMigrations, err := listAllDBVersions(db)
|
dbMigrations, err := listAllDBVersions(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -98,6 +116,24 @@ func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// upToNoVersioning applies up migrations up to, and including, the
|
||||||
|
// target version.
|
||||||
|
func upToNoVersioning(db *sql.DB, migrations Migrations, version int64) error {
|
||||||
|
var finalVersion int64
|
||||||
|
for _, current := range migrations {
|
||||||
|
if current.Version > version {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
current.noVersioning = true
|
||||||
|
if err := current.Up(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
finalVersion = current.Version
|
||||||
|
}
|
||||||
|
log.Printf("goose: up to current file version: %d\n", finalVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func upWithMissing(
|
func upWithMissing(
|
||||||
db *sql.DB,
|
db *sql.DB,
|
||||||
missingMigrations Migrations,
|
missingMigrations Migrations,
|
||||||
|
|
22
version.go
22
version.go
|
@ -2,15 +2,33 @@ package goose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Version prints the current version of the database.
|
// Version prints the current version of the database.
|
||||||
func Version(db *sql.DB, dir string) error {
|
func Version(db *sql.DB, dir string, opts ...OptionsFunc) error {
|
||||||
|
option := &options{}
|
||||||
|
for _, f := range opts {
|
||||||
|
f(option)
|
||||||
|
}
|
||||||
|
if option.noVersioning {
|
||||||
|
var current int64
|
||||||
|
migrations, err := CollectMigrations(dir, minVersion, maxVersion)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to collect migrations")
|
||||||
|
}
|
||||||
|
if len(migrations) > 0 {
|
||||||
|
current = migrations[len(migrations)-1].Version
|
||||||
|
}
|
||||||
|
log.Printf("goose: file version %v\n", current)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
current, err := GetDBVersion(db)
|
current, err := GetDBVersion(db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("goose: version %v\n", current)
|
log.Printf("goose: version %v\n", current)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue