diff --git a/README.md b/README.md index 2e49637..44372b0 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ dbmate drop # drop the database dbmate migrate # run any pending migrations dbmate rollback # roll back the most recent migration dbmate down # alias for rollback +dbmate status # show the status of all migrations dbmate dump # write the database schema.sql file dbmate wait # wait for the database server to become available ``` diff --git a/main.go b/main.go index 807ce87..34151a2 100644 --- a/main.go +++ b/main.go @@ -102,6 +102,13 @@ func NewApp() *cli.App { return db.Rollback() }), }, + { + Name: "status", + Usage: "List applied and pending migrations", + Action: action(func(db *dbmate.DB, c *cli.Context) error { + return db.Status() + }), + }, { Name: "dump", Usage: "Write the database schema to disk", diff --git a/pkg/dbmate/db.go b/pkg/dbmate/db.go index fd5ba53..83f8220 100644 --- a/pkg/dbmate/db.go +++ b/pkg/dbmate/db.go @@ -35,6 +35,14 @@ type DB struct { WaitTimeout time.Duration } +// migrationFileRegexp pattern for valid migration files +var migrationFileRegexp = regexp.MustCompile(`^\d.*\.sql$`) + +type statusResult struct { + filename string + applied bool +} + // New initializes a new dbmate database func New(databaseURL *url.URL) *DB { return &DB{ @@ -253,8 +261,7 @@ func (db *DB) openDatabaseForMigration() (Driver, *sql.DB, error) { // Migrate migrates database to the latest version func (db *DB) Migrate() error { - re := regexp.MustCompile(`^\d.*\.sql$`) - files, err := findMigrationFiles(db.MigrationsDir, re) + files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp) if err != nil { return err } @@ -445,3 +452,66 @@ func (db *DB) Rollback() error { return nil } + +func checkMigrationsStatus(db *DB) ([]statusResult, error) { + files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp) + if err != nil { + return nil, err + } + + if len(files) == 0 { + return nil, fmt.Errorf("no migration files found") + } + + drv, sqlDB, err := db.openDatabaseForMigration() + if err != nil { + return nil, err + } + defer mustClose(sqlDB) + + applied, err := drv.SelectMigrations(sqlDB, -1) + if err != nil { + return nil, err + } + + var results []statusResult + + for _, filename := range files { + ver := migrationVersion(filename) + res := statusResult{filename: filename} + if ok := applied[ver]; ok { + res.applied = true + } else { + res.applied = false + } + + results = append(results, res) + } + + return results, nil +} + +// Status shows the status of all migrations +func (db *DB) Status() error { + results, err := checkMigrationsStatus(db) + if err != nil { + return err + } + + var totalApplied int + + for _, res := range results { + if res.applied { + fmt.Println("[X]", res.filename) + totalApplied++ + } else { + fmt.Println("[ ]", res.filename) + } + } + + fmt.Println() + fmt.Printf("Applied: %d\n", totalApplied) + fmt.Printf("Pending: %d\n", len(results)-totalApplied) + + return nil +} diff --git a/pkg/dbmate/db_test.go b/pkg/dbmate/db_test.go index 61691fb..4b5ebc0 100644 --- a/pkg/dbmate/db_test.go +++ b/pkg/dbmate/db_test.go @@ -298,6 +298,9 @@ func testRollbackURL(t *testing.T, u *url.URL) { require.NoError(t, err) require.Equal(t, 1, count) + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) + require.Nil(t, err) + // rollback err = db.Rollback() require.NoError(t, err) @@ -305,9 +308,9 @@ func testRollbackURL(t *testing.T, u *url.URL) { // verify rollback err = sqlDB.QueryRow("select count(*) from schema_migrations").Scan(&count) require.NoError(t, err) - require.Equal(t, 0, count) + require.Equal(t, 1, count) - err = sqlDB.QueryRow("select count(*) from users").Scan(&count) + err = sqlDB.QueryRow("select count(*) from posts").Scan(&count) require.NotNil(t, err) require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error()) } @@ -317,3 +320,53 @@ func TestRollback(t *testing.T) { testRollbackURL(t, u) } } + +func testStatusUrl(t *testing.T, u *url.URL) { + db := newTestDB(t, u) + + // drop, recreate, and migrate database + err := db.Drop() + require.NoError(t, err) + err = db.Create() + require.NoError(t, err) + + // verify migration + sqlDB, err := GetDriverOpen(u) + require.NoError(t, err) + defer mustClose(sqlDB) + + // two pending + results, err := checkMigrationsStatus(db) + require.NoError(t, err) + require.Len(t, results, 2) + require.False(t, results[0].applied) + require.False(t, results[1].applied) + + // run migrations + err = db.Migrate() + require.NoError(t, err) + + // two applied + results, err = checkMigrationsStatus(db) + require.NoError(t, err) + require.Len(t, results, 2) + require.True(t, results[0].applied) + require.True(t, results[1].applied) + + // rollback last migration + err = db.Rollback() + require.NoError(t, err) + + // one applied, one pending + results, err = checkMigrationsStatus(db) + require.NoError(t, err) + require.Len(t, results, 2) + require.True(t, results[0].applied) + require.False(t, results[1].applied) +} + +func TestStatus(t *testing.T) { + for _, u := range testURLs(t) { + testStatusUrl(t, u) + } +} diff --git a/testdata/db/migrations/20200227231541_test_posts.sql b/testdata/db/migrations/20200227231541_test_posts.sql new file mode 100644 index 0000000..b990c00 --- /dev/null +++ b/testdata/db/migrations/20200227231541_test_posts.sql @@ -0,0 +1,8 @@ +-- migrate:up +create table posts ( + id integer, + name varchar(255) +); + +-- migrate:down +drop table posts;