diff --git a/README.md b/README.md index 6f49cfa..a184783 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ files to be saved in (which will be the same directory of the main package, e.g. `example`), an instance of `*pg.DB`, and `os.Args`; and log any potential errors that could be returned. -Once this has been set up, then you can use the `create`, `migrate`, `rollback`, +Once this has been set up, then you can use the `create`, `migrate`, `status`, `rollback`, `help` commands like so: ``` @@ -43,6 +43,13 @@ $ go run example/*.go migrate Running batch 1 with 1 migration(s)... Finished running "20180812001528_create_users_table" +$ go run example/*.go status ++---------+-----------------------------------+-------+ +| Applied | Migration | Batch | ++---------+-----------------------------------+-------+ +| √ | 20180812001528_create_users_table | 1 | ++---------+-----------------------------------+-------+ + $ go run example/*.go rollback Rolling back batch 1 with 1 migration(s)... Finished rolling back "20180812001528_create_users_table" @@ -55,12 +62,14 @@ Commands: create - create a new migration in example with the provided name migrate - run any migrations that haven't been run yet rollback - roll back the previous run batch of migrations + status - show the status of each migration help - print this help text Examples: go run example/*.go create create_users_table go run example/*.go migrate go run example/*.go rollback + go run example/*.go status go run example/*.go help ``` diff --git a/help.go b/help.go index 4a45df0..77da64f 100644 --- a/help.go +++ b/help.go @@ -9,15 +9,17 @@ Commands: create - create a new migration in %s with the provided name migrate - run any migrations that haven't been run yet rollback - roll back the previous run batch of migrations + status - show the status of each migration help - print this help text Examples: go run %s/*.go create create_users_table go run %s/*.go migrate go run %s/*.go rollback + go run %s/*.go status go run %s/*.go help ` func help(directory string) { - fmt.Printf(helpText, directory, directory, directory, directory, directory, directory) + fmt.Printf(helpText, directory, directory, directory, directory, directory, directory, directory) } diff --git a/migrations.go b/migrations.go index 59c561e..e4b15ee 100644 --- a/migrations.go +++ b/migrations.go @@ -4,6 +4,7 @@ package migrations import ( "errors" + "os" "time" "github.com/go-pg/pg/v10" @@ -72,6 +73,13 @@ func Run(db *pg.DB, directory string, args []string) error { } return rollback(db) + case "status": + err := ensureMigrationTables(db) + if err != nil { + return err + } + + return status(db, os.Stdout) default: help(directory) return nil diff --git a/status.go b/status.go new file mode 100644 index 0000000..65b85cb --- /dev/null +++ b/status.go @@ -0,0 +1,82 @@ +package migrations + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "unicode/utf8" + + "github.com/go-pg/pg/v10" +) + +type migrationWithStatus struct { + migration +} + +func status(db *pg.DB, w io.Writer) error { + // sort the registered migrations by name (which will sort by the + // timestamp in their names) + sort.Slice(migrations, func(i, j int) bool { + return migrations[i].Name < migrations[j].Name + }) + + // look at the migrations table to see the already run migrations + completed, err := getCompletedMigrations(db) + if err != nil { + return err + } + + // diff the completed migrations from the registered migrations to find + // the migrations we still need to run + uncompleted := filterMigrations(migrations, completed, false) + + return writeStatusTable(w, completed, uncompleted) +} + +func writeStatusTable(w io.Writer, completed []migration, uncompleted []migration) error { + if len(completed)+len(uncompleted) == 0 { + _, err := fmt.Fprintln(w, "No migrations found") + return err + } + + maxNameLength := 20 + for _, m := range completed { + maxNameLength = maxInt(maxNameLength, utf8.RuneCountInString(m.Name)) + } + for _, m := range uncompleted { + maxNameLength = maxInt(maxNameLength, utf8.RuneCountInString(m.Name)) + } + + bf := bytes.NewBuffer(nil) + + // write header + bf.WriteString("+---------+" + strings.Repeat("-", maxNameLength+2) + "+-------+\n") + bf.WriteString("| Applied | Migration" + strings.Repeat(" ", maxNameLength-8) + "| Batch |\n") + bf.WriteString("+---------+" + strings.Repeat("-", maxNameLength+2) + "+-------+\n") + + // write completed migrations + for _, m := range completed { + bf.WriteString("| √ | " + m.Name + strings.Repeat(" ", maxNameLength-len(m.Name)) + " | " + fmt.Sprintf("%5d", m.Batch) + " |\n") + } + + // write uncompleted migrations + for _, m := range uncompleted { + bf.WriteString("| | " + m.Name + strings.Repeat(" ", maxNameLength-len(m.Name)) + " | |\n") + } + + // write footer + bf.WriteString("+---------+" + strings.Repeat("-", maxNameLength+2) + "+-------+\n") + + _, err := bf.WriteTo(w) + return err +} + +func maxInt(a, b int) int { + if a > b { + return a + } else { + return b + } +} diff --git a/status_test.go b/status_test.go new file mode 100644 index 0000000..06ca067 --- /dev/null +++ b/status_test.go @@ -0,0 +1,92 @@ +package migrations + +import ( + "bytes" + "os" + "strings" + "testing" + + "github.com/go-pg/pg/v10" + "github.com/stretchr/testify/require" +) + +func TestStatus(t *testing.T) { + db := pg.Connect(&pg.Options{ + Addr: "localhost:5432", + User: os.Getenv("TEST_DATABASE_USER"), + Database: os.Getenv("TEST_DATABASE_NAME"), + }) + + db.AddQueryHook(logQueryHook{}) + + err := ensureMigrationTables(db) + require.Nil(t, err) + + defer clearMigrations(t, db) + defer resetMigrations(t) + + completed := []migration{ + {Name: "2021_02_26_151503_dump", Up: noopMigration, Down: noopMigration, Batch: 1}, + {Name: "2021_02_26_151504_create_a_dump_table_for_test", Up: noopMigration, Down: noopMigration, Batch: 2}, + } + uncompleted := []migration{ + {Name: "2021_02_26_151502_create_2nd_dump_table", Up: noopMigration, Down: noopMigration}, + {Name: "2021_02_26_151505_create_3rd_dump_table", Up: noopMigration, Down: noopMigration}, + } + expected := strings.TrimSpace(` ++---------+------------------------------------------------+-------+ +| Applied | Migration | Batch | ++---------+------------------------------------------------+-------+ +| √ | 2021_02_26_151503_dump | 1 | +| √ | 2021_02_26_151504_create_a_dump_table_for_test | 2 | +| | 2021_02_26_151502_create_2nd_dump_table | | +| | 2021_02_26_151505_create_3rd_dump_table | | ++---------+------------------------------------------------+-------+ +`) + + t.Run("status_command", func(tt *testing.T) { + clearMigrations(tt, db) + resetMigrations(tt) + + migrations = completed[:1] + err := migrate(db) + require.Nil(tt, err, "migrate: %v", err) + migrations = completed[:2] + err = migrate(db) + require.Nil(tt, err, "migrate: %v", err) + + migrations = append(migrations, uncompleted...) + bf := bytes.NewBuffer(nil) + err = status(db, bf) + require.Nil(tt, err, "status: %v", err) + + got := strings.TrimSpace(bf.String()) + if got != expected { + tt.Errorf("status table not match:\nEXPECTED:\n%s\nACTUAL:\n%s", expected, got) + } + }) + + t.Run("write_status_table", func(tt *testing.T) { + bf := bytes.NewBuffer(nil) + err := writeStatusTable(bf, completed, uncompleted) + require.Nil(tt, err, "write_status_table: %v", err) + + got := strings.TrimSpace(bf.String()) + if got != expected { + tt.Errorf("status table not match:\nEXPECTED:\n%s\nACTUAL:\n%s", expected, got) + } + }) + + t.Run("no_migrations_found", func(tt *testing.T) { + expected := strings.TrimSpace(`No migrations found`) + + bf := bytes.NewBuffer(nil) + err := writeStatusTable(bf, nil, nil) + require.Nil(tt, err, "write_status_table: %v", err) + + got := strings.TrimSpace(bf.String()) + if got != expected { + tt.Errorf("status table not match:\nEXPECTED:\n%s\nACTUAL:\n%s", expected, got) + } + }) +}