Adds support to apply migrations without versioning (#291)

pull/299/head v3.5.0
Michael Fridman 2021-12-13 00:37:44 -05:00 committed by GitHub
parent fd1ba04fc8
commit c8aa123e31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 418 additions and 94 deletions

View File

@ -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

View File

@ -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
View File

@ -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
}

View File

@ -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:

View File

@ -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")
}
} }
} }

View File

@ -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
View File

@ -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
} }

View File

@ -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")

View File

@ -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 {

View File

@ -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.

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View File

@ -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,

View File

@ -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
} }