From c8aa123e312f6c26f250e586f9f59310f7d9a726 Mon Sep 17 00:00:00 2001 From: Michael Fridman Date: Mon, 13 Dec 2021 00:37:44 -0500 Subject: [PATCH] Adds support to apply migrations without versioning (#291) --- README.md | 31 ++-- cmd/goose/main.go | 4 + down.go | 48 +++++- goose.go | 12 +- migration.go | 38 +++-- migration_sql.go | 42 ++--- redo.go | 25 ++- reset.go | 10 +- status.go | 15 +- tests/e2e/main_test.go | 3 + tests/e2e/no_versioning_test.go | 154 ++++++++++++++++++ .../e2e/testdata/mysql/migrations/00002_b.sql | 6 +- .../e2e/testdata/mysql/migrations/00003_c.sql | 8 +- .../testdata/postgres/migrations/00001_a.sql | 2 +- .../testdata/postgres/migrations/00002_b.sql | 6 +- .../testdata/postgres/migrations/00003_c.sql | 8 +- tests/e2e/testdata/postgres/seed/00001_a.sql | 18 ++ tests/e2e/testdata/postgres/seed/00002_b.sql | 14 ++ up.go | 42 ++++- version.go | 22 ++- 20 files changed, 416 insertions(+), 92 deletions(-) create mode 100644 tests/e2e/no_versioning_test.go create mode 100644 tests/e2e/testdata/postgres/seed/00001_a.sql create mode 100644 tests/e2e/testdata/postgres/seed/00002_b.sql diff --git a/README.md b/README.md index ee40862c2..6cc22d7a7 100644 --- a/README.md +++ b/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 - 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 @@ -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 @@ -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 diff --git a/cmd/goose/main.go b/cmd/goose/main.go index 4790049c4..5e758cdc0 100644 --- a/cmd/goose/main.go +++ b/cmd/goose/main.go @@ -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 ( @@ -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, diff --git a/down.go b/down.go index 1db984736..c0b47ba87 100644 --- a/down.go +++ b/down.go @@ -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) @@ -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 +} diff --git a/goose.go b/goose.go index 169fa50cc..bcf1df48f 100644 --- a/goose.go +++ b/goose.go @@ -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": @@ -93,7 +93,7 @@ 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": @@ -101,19 +101,19 @@ func run(command string, db *sql.DB, dir string, args []string, options ...Optio 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: diff --git a/migration.go b/migration.go index 090f45627..267006b90 100644 --- a/migration.go +++ b/migration.go @@ -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 { @@ -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)) } @@ -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") + } } } diff --git a/migration_sql.go b/migration_sql.go index 3ac3926fc..d27554e03 100644 --- a/migration_sql.go +++ b/migration_sql.go @@ -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. @@ -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") + } } } @@ -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") + } } } diff --git a/redo.go b/redo.go index 6f9049f67..c485f9f67 100644 --- a/redo.go +++ b/redo.go @@ -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 } diff --git a/reset.go b/reset.go index 26b9dcb7f..3605a64a9 100644 --- a/reset.go +++ b/reset.go @@ -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") diff --git a/status.go b/status.go index 8a24c22a5..7378f01d5 100644 --- a/status.go +++ b/status.go @@ -9,12 +9,23 @@ import ( ) // Status prints the status of all migrations. -func Status(db *sql.DB, dir string) error { - // collect all migrations +func Status(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 { + 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 if _, err := EnsureDBVersion(db); err != nil { diff --git a/tests/e2e/main_test.go b/tests/e2e/main_test.go index b94b0b8cf..7756863bd 100644 --- a/tests/e2e/main_test.go +++ b/tests/e2e/main_test.go @@ -51,6 +51,8 @@ var ( // migrationsDir is a global that points to a ./testdata/{dialect}/migrations folder. // It is set in TestMain based on the current dialect. migrationsDir = "" + // seedDir is similar to migrationsDir but contains seed data + seedDir = "" // known tables are the tables (including goose table) created by // running all migration files. If you add a table, make sure to @@ -74,6 +76,7 @@ func TestMain(m *testing.M) { os.Exit(1) } migrationsDir = filepath.Join("testdata", *dialect, "migrations") + seedDir = filepath.Join("testdata", *dialect, "seed") exitCode := m.Run() // Useful for debugging test services. diff --git a/tests/e2e/no_versioning_test.go b/tests/e2e/no_versioning_test.go new file mode 100644 index 000000000..032965bdb --- /dev/null +++ b/tests/e2e/no_versioning_test.go @@ -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 +} diff --git a/tests/e2e/testdata/mysql/migrations/00002_b.sql b/tests/e2e/testdata/mysql/migrations/00002_b.sql index 34b821c80..8f8dca03c 100644 --- a/tests/e2e/testdata/mysql/migrations/00002_b.sql +++ b/tests/e2e/testdata/mysql/migrations/00002_b.sql @@ -1,10 +1,10 @@ -- +goose Up -- +goose StatementBegin -INSERT INTO owners(owner_id, owner_name, owner_type) - VALUES (1, 'lucas', 'user'), (2, 'space', 'organization'); +INSERT INTO owners(owner_name, owner_type) + VALUES ('lucas', 'user'), ('space', 'organization'); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin -DELETE FROM owners WHERE owner_id IN (1, 2); +DELETE FROM owners; -- +goose StatementEnd diff --git a/tests/e2e/testdata/mysql/migrations/00003_c.sql b/tests/e2e/testdata/mysql/migrations/00003_c.sql index 66fb6d02a..b93a85426 100644 --- a/tests/e2e/testdata/mysql/migrations/00003_c.sql +++ b/tests/e2e/testdata/mysql/migrations/00003_c.sql @@ -1,10 +1,10 @@ -- +goose Up -- +goose StatementBegin -INSERT INTO owners(owner_id, owner_name, owner_type) - VALUES (3, 'james', 'user'), (4, 'pressly', 'organization'); +INSERT INTO owners(owner_name, owner_type) + VALUES ('james', 'user'), ('pressly', 'organization'); -INSERT INTO repos(repo_id, repo_full_name, repo_owner_id) - VALUES (1, 'james/rover', 3), (2, 'pressly/goose', 4); +INSERT INTO repos(repo_full_name, repo_owner_id) + VALUES ('james/rover', 3), ('pressly/goose', 4); -- +goose StatementEnd -- +goose Down diff --git a/tests/e2e/testdata/postgres/migrations/00001_a.sql b/tests/e2e/testdata/postgres/migrations/00001_a.sql index 67637f7bb..67bf7ba0d 100644 --- a/tests/e2e/testdata/postgres/migrations/00001_a.sql +++ b/tests/e2e/testdata/postgres/migrations/00001_a.sql @@ -9,7 +9,7 @@ CREATE TABLE owners ( ); CREATE TABLE IF NOT EXISTS repos ( - repo_id bigint UNIQUE NOT NULL, + repo_id BIGSERIAL NOT NULL, repo_full_name text NOT NULL, repo_owner_id bigint NOT NULL REFERENCES owners(owner_id) ON DELETE CASCADE, diff --git a/tests/e2e/testdata/postgres/migrations/00002_b.sql b/tests/e2e/testdata/postgres/migrations/00002_b.sql index 34b821c80..8f8dca03c 100644 --- a/tests/e2e/testdata/postgres/migrations/00002_b.sql +++ b/tests/e2e/testdata/postgres/migrations/00002_b.sql @@ -1,10 +1,10 @@ -- +goose Up -- +goose StatementBegin -INSERT INTO owners(owner_id, owner_name, owner_type) - VALUES (1, 'lucas', 'user'), (2, 'space', 'organization'); +INSERT INTO owners(owner_name, owner_type) + VALUES ('lucas', 'user'), ('space', 'organization'); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin -DELETE FROM owners WHERE owner_id IN (1, 2); +DELETE FROM owners; -- +goose StatementEnd diff --git a/tests/e2e/testdata/postgres/migrations/00003_c.sql b/tests/e2e/testdata/postgres/migrations/00003_c.sql index 66fb6d02a..b93a85426 100644 --- a/tests/e2e/testdata/postgres/migrations/00003_c.sql +++ b/tests/e2e/testdata/postgres/migrations/00003_c.sql @@ -1,10 +1,10 @@ -- +goose Up -- +goose StatementBegin -INSERT INTO owners(owner_id, owner_name, owner_type) - VALUES (3, 'james', 'user'), (4, 'pressly', 'organization'); +INSERT INTO owners(owner_name, owner_type) + VALUES ('james', 'user'), ('pressly', 'organization'); -INSERT INTO repos(repo_id, repo_full_name, repo_owner_id) - VALUES (1, 'james/rover', 3), (2, 'pressly/goose', 4); +INSERT INTO repos(repo_full_name, repo_owner_id) + VALUES ('james/rover', 3), ('pressly/goose', 4); -- +goose StatementEnd -- +goose Down diff --git a/tests/e2e/testdata/postgres/seed/00001_a.sql b/tests/e2e/testdata/postgres/seed/00001_a.sql new file mode 100644 index 000000000..d47a3dae2 --- /dev/null +++ b/tests/e2e/testdata/postgres/seed/00001_a.sql @@ -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 \ No newline at end of file diff --git a/tests/e2e/testdata/postgres/seed/00002_b.sql b/tests/e2e/testdata/postgres/seed/00002_b.sql new file mode 100644 index 000000000..a4d7ce170 --- /dev/null +++ b/tests/e2e/testdata/postgres/seed/00002_b.sql @@ -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; diff --git a/up.go b/up.go index 409b0c77b..d8d19cfe7 100644 --- a/up.go +++ b/up.go @@ -11,6 +11,7 @@ import ( type options struct { allowMissing bool applyUpByOne bool + noVersioning bool } type OptionsFunc func(o *options) @@ -19,15 +20,16 @@ func WithAllowMissing() OptionsFunc { return func(o *options) { o.allowMissing = true } } +func WithNoVersioning() OptionsFunc { + return func(o *options) { o.noVersioning = true } +} + func withApplyUpByOne() OptionsFunc { return func(o *options) { o.applyUpByOne = true } } // UpTo migrates up to a specific version. func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error { - if _, err := EnsureDBVersion(db); err != nil { - return err - } option := &options{} for _, f := range opts { f(option) @@ -36,6 +38,22 @@ func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error { if err != nil { 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) if err != nil { return err @@ -98,6 +116,24 @@ func UpTo(db *sql.DB, dir string, version int64, opts ...OptionsFunc) error { 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( db *sql.DB, missingMigrations Migrations, diff --git a/version.go b/version.go index 800540031..f6628ac51 100644 --- a/version.go +++ b/version.go @@ -2,15 +2,33 @@ package goose import ( "database/sql" + + "github.com/pkg/errors" ) // 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) if err != nil { return err } - log.Printf("goose: version %v\n", current) return nil }