Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(status): Add status command #29

Merged
merged 1 commit into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand All @@ -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"
Expand All @@ -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
```

Expand Down
4 changes: 3 additions & 1 deletion help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 8 additions & 0 deletions migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package migrations

import (
"errors"
"os"
"time"

"github.com/go-pg/pg/v10"
Expand Down Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions status.go
Original file line number Diff line number Diff line change
@@ -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
}
}
92 changes: 92 additions & 0 deletions status_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}