mirror of
https://github.com/TECHNOFAB11/dbmate.git
synced 2025-12-11 23:50:04 +01:00
Separate cmd package for easier importing (#11)
This commit is contained in:
parent
247d7296f8
commit
e393f387b3
21 changed files with 408 additions and 368 deletions
1
.agignore
Normal file
1
.agignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
/vendor
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,2 @@
|
|||
/dist
|
||||
.DS_Store
|
||||
dbmate
|
||||
dist
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.8.0
|
||||
FROM golang:1.8
|
||||
|
||||
# required to force cgo (for sqlite driver) with cross compile
|
||||
ENV CGO_ENABLED 1
|
||||
|
|
@ -19,6 +19,6 @@ COPY . $GOPATH/src/github.com/amacneil/dbmate
|
|||
WORKDIR $GOPATH/src/github.com/amacneil/dbmate
|
||||
|
||||
# build
|
||||
RUN go install -v
|
||||
RUN go install -v ./cmd/dbmate
|
||||
|
||||
CMD dbmate
|
||||
|
|
|
|||
17
Makefile
17
Makefile
|
|
@ -1,7 +1,8 @@
|
|||
DC := docker-compose
|
||||
BUILD_FLAGS := -ldflags '-s'
|
||||
PACKAGES := . ./cmd/dbmate
|
||||
|
||||
all: clean container lint test build
|
||||
all: clean container test lint build
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
|
|
@ -12,15 +13,15 @@ container:
|
|||
$(DC) up -d
|
||||
|
||||
lint:
|
||||
$(DC) run dbmate golint
|
||||
$(DC) run dbmate go vet
|
||||
$(DC) run dbmate errcheck
|
||||
$(DC) run dbmate golint -set_exit_status $(PACKAGES)
|
||||
$(DC) run dbmate go vet $(PACKAGES)
|
||||
$(DC) run dbmate errcheck $(PACKAGES)
|
||||
|
||||
test:
|
||||
$(DC) run dbmate go test -v
|
||||
$(DC) run dbmate go test -v $(PACKAGES)
|
||||
|
||||
build: clean
|
||||
$(DC) run -e GOARCH=386 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-i386
|
||||
$(DC) run -e GOARCH=amd64 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-amd64
|
||||
$(DC) run -e GOARCH=386 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-i386 ./cmd/dbmate
|
||||
$(DC) run -e GOARCH=amd64 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-amd64 ./cmd/dbmate
|
||||
# musl target does not support sqlite
|
||||
$(DC) run -e GOARCH=amd64 -e CGO_ENABLED=0 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-musl-amd64
|
||||
$(DC) run -e GOARCH=amd64 -e CGO_ENABLED=0 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-musl-amd64 ./cmd/dbmate
|
||||
|
|
|
|||
127
cmd/dbmate/main.go
Normal file
127
cmd/dbmate/main.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/amacneil/dbmate"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
loadDotEnv()
|
||||
|
||||
app := NewApp()
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// NewApp creates a new command line app
|
||||
func NewApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "dbmate"
|
||||
app.Usage = "A lightweight, framework-independent database migration tool."
|
||||
app.Version = dbmate.Version
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "migrations-dir, d",
|
||||
Value: dbmate.DefaultMigrationsDir,
|
||||
Usage: "specify the directory containing migration files",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "env, e",
|
||||
Value: "DATABASE_URL",
|
||||
Usage: "specify an environment variable containing the database URL",
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Generate a new migration file",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
return db.New(name)
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "up",
|
||||
Usage: "Create database (if necessary) and migrate to the latest version",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
return db.Up()
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "Create database",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
return db.Create()
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "drop",
|
||||
Usage: "Drop database (if it exists)",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
return db.Drop()
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate to the latest version",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
return db.Migrate()
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "rollback",
|
||||
Aliases: []string{"down"},
|
||||
Usage: "Rollback the most recent migration",
|
||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||
return db.Rollback()
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
// load environment variables from .env file
|
||||
func loadDotEnv() {
|
||||
if _, err := os.Stat(".env"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
}
|
||||
|
||||
// action wraps a cli.ActionFunc with dbmate initialization logic
|
||||
func action(f func(*dbmate.DB, *cli.Context) error) cli.ActionFunc {
|
||||
return func(c *cli.Context) error {
|
||||
u, err := getDatabaseURL(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db := dbmate.NewDB(u)
|
||||
db.MigrationsDir = c.GlobalString("migrations-dir")
|
||||
|
||||
return f(db, c)
|
||||
}
|
||||
}
|
||||
|
||||
// getDatabaseURL returns the current environment database url
|
||||
func getDatabaseURL(c *cli.Context) (u *url.URL, err error) {
|
||||
env := c.GlobalString("env")
|
||||
value := os.Getenv(env)
|
||||
|
||||
return url.Parse(value)
|
||||
}
|
||||
39
cmd/dbmate/main_test.go
Normal file
39
cmd/dbmate/main_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func testContext(t *testing.T, u *url.URL) *cli.Context {
|
||||
var err error
|
||||
|
||||
err = os.Setenv("DATABASE_URL", u.String())
|
||||
require.Nil(t, err)
|
||||
|
||||
app := NewApp()
|
||||
flagset := flag.NewFlagSet(app.Name, flag.ContinueOnError)
|
||||
for _, f := range app.Flags {
|
||||
f.Apply(flagset)
|
||||
}
|
||||
|
||||
return cli.NewContext(app, flagset, nil)
|
||||
}
|
||||
|
||||
func TestGetDatabaseUrl(t *testing.T) {
|
||||
envURL, err := url.Parse("foo://example.org/db")
|
||||
require.Nil(t, err)
|
||||
ctx := testContext(t, envURL)
|
||||
|
||||
u, err := getDatabaseURL(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "foo", u.Scheme)
|
||||
require.Equal(t, "example.org", u.Host)
|
||||
require.Equal(t, "/db", u.Path)
|
||||
}
|
||||
169
commands_test.go
169
commands_test.go
|
|
@ -1,169 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var testdataDir string
|
||||
|
||||
func testContext(t *testing.T, u *url.URL) *cli.Context {
|
||||
var err error
|
||||
|
||||
// only chdir once, because testdata is relative to current directory
|
||||
if testdataDir == "" {
|
||||
testdataDir, err = filepath.Abs("./testdata")
|
||||
require.Nil(t, err)
|
||||
|
||||
err = os.Chdir(testdataDir)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
err = os.Setenv("DATABASE_URL", u.String())
|
||||
require.Nil(t, err)
|
||||
|
||||
app := NewApp()
|
||||
flagset := flag.NewFlagSet(app.Name, flag.ContinueOnError)
|
||||
for _, f := range app.Flags {
|
||||
f.Apply(flagset)
|
||||
}
|
||||
|
||||
return cli.NewContext(app, flagset, nil)
|
||||
}
|
||||
|
||||
func testURLs(t *testing.T) []*url.URL {
|
||||
return []*url.URL{
|
||||
postgresTestURL(t),
|
||||
mySQLTestURL(t),
|
||||
sqliteTestURL(t),
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDatabaseUrl(t *testing.T) {
|
||||
envURL, err := url.Parse("foo://example.org/db")
|
||||
require.Nil(t, err)
|
||||
ctx := testContext(t, envURL)
|
||||
|
||||
u, err := GetDatabaseURL(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, "foo", u.Scheme)
|
||||
require.Equal(t, "example.org", u.Host)
|
||||
require.Equal(t, "/db", u.Path)
|
||||
}
|
||||
|
||||
func testMigrateCommandURL(t *testing.T, u *url.URL) {
|
||||
ctx := testContext(t, u)
|
||||
|
||||
// drop and recreate database
|
||||
err := DropCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
err = CreateCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// migrate
|
||||
err = MigrateCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify results
|
||||
db, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(db)
|
||||
|
||||
count := 0
|
||||
err = db.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
err = db.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestMigrateCommand(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testMigrateCommandURL(t, u)
|
||||
}
|
||||
}
|
||||
|
||||
func testUpCommandURL(t *testing.T, u *url.URL) {
|
||||
ctx := testContext(t, u)
|
||||
|
||||
// drop database
|
||||
err := DropCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// create and migrate
|
||||
err = UpCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify results
|
||||
db, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(db)
|
||||
|
||||
count := 0
|
||||
err = db.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
err = db.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestUpCommand(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testUpCommandURL(t, u)
|
||||
}
|
||||
}
|
||||
|
||||
func testRollbackCommandURL(t *testing.T, u *url.URL) {
|
||||
ctx := testContext(t, u)
|
||||
|
||||
// drop, recreate, and migrate database
|
||||
err := DropCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
err = CreateCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
err = MigrateCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify migration
|
||||
db, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(db)
|
||||
|
||||
count := 0
|
||||
err = db.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
// rollback
|
||||
err = RollbackCommand(ctx)
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify rollback
|
||||
err = db.QueryRow("select count(*) from schema_migrations").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
err = db.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.NotNil(t, err)
|
||||
require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error())
|
||||
}
|
||||
|
||||
func TestRollbackCommand(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testRollbackCommandURL(t, u)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
@ -10,18 +10,33 @@ import (
|
|||
"regexp"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// UpCommand creates the database (if necessary) and runs migrations
|
||||
func UpCommand(ctx *cli.Context) error {
|
||||
u, err := GetDatabaseURL(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// DefaultMigrationsDir specifies default directory to find migration files
|
||||
var DefaultMigrationsDir = "./db/migrations"
|
||||
|
||||
drv, err := GetDriver(u.Scheme)
|
||||
// DB allows dbmate actions to be performed on a specified database
|
||||
type DB struct {
|
||||
DatabaseURL *url.URL
|
||||
MigrationsDir string
|
||||
}
|
||||
|
||||
// NewDB initializes a new dbmate database
|
||||
func NewDB(databaseURL *url.URL) *DB {
|
||||
return &DB{
|
||||
DatabaseURL: databaseURL,
|
||||
MigrationsDir: DefaultMigrationsDir,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDriver loads the required database driver
|
||||
func (db *DB) GetDriver() (Driver, error) {
|
||||
return GetDriver(db.DatabaseURL.Scheme)
|
||||
}
|
||||
|
||||
// Up creates the database (if necessary) and runs migrations
|
||||
func (db *DB) Up() error {
|
||||
drv, err := db.GetDriver()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -29,71 +44,59 @@ func UpCommand(ctx *cli.Context) error {
|
|||
// create database if it does not already exist
|
||||
// skip this step if we cannot determine status
|
||||
// (e.g. user does not have list database permission)
|
||||
exists, err := drv.DatabaseExists(u)
|
||||
exists, err := drv.DatabaseExists(db.DatabaseURL)
|
||||
if err == nil && !exists {
|
||||
if err := drv.CreateDatabase(u); err != nil {
|
||||
if err := drv.CreateDatabase(db.DatabaseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// migrate
|
||||
return MigrateCommand(ctx)
|
||||
return db.Migrate()
|
||||
}
|
||||
|
||||
// CreateCommand creates the current database
|
||||
func CreateCommand(ctx *cli.Context) error {
|
||||
u, err := GetDatabaseURL(ctx)
|
||||
// Create creates the current database
|
||||
func (db *DB) Create() error {
|
||||
drv, err := db.GetDriver()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
drv, err := GetDriver(u.Scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return drv.CreateDatabase(u)
|
||||
return drv.CreateDatabase(db.DatabaseURL)
|
||||
}
|
||||
|
||||
// DropCommand drops the current database (if it exists)
|
||||
func DropCommand(ctx *cli.Context) error {
|
||||
u, err := GetDatabaseURL(ctx)
|
||||
// Drop drops the current database (if it exists)
|
||||
func (db *DB) Drop() error {
|
||||
drv, err := db.GetDriver()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
drv, err := GetDriver(u.Scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return drv.DropDatabase(u)
|
||||
return drv.DropDatabase(db.DatabaseURL)
|
||||
}
|
||||
|
||||
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
||||
|
||||
// NewCommand creates a new migration file
|
||||
func NewCommand(ctx *cli.Context) error {
|
||||
// New creates a new migration file
|
||||
func (db *DB) New(name string) error {
|
||||
// new migration name
|
||||
timestamp := time.Now().UTC().Format("20060102150405")
|
||||
name := ctx.Args().First()
|
||||
if name == "" {
|
||||
return fmt.Errorf("Please specify a name for the new migration.")
|
||||
return fmt.Errorf("please specify a name for the new migration")
|
||||
}
|
||||
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
||||
|
||||
// create migrations dir if missing
|
||||
migrationsDir := ctx.GlobalString("migrations-dir")
|
||||
if err := os.MkdirAll(migrationsDir, 0755); err != nil {
|
||||
return fmt.Errorf("Unable to create directory `%s`.", migrationsDir)
|
||||
if err := os.MkdirAll(db.MigrationsDir, 0755); err != nil {
|
||||
return fmt.Errorf("unable to create directory `%s`", db.MigrationsDir)
|
||||
}
|
||||
|
||||
// check file does not already exist
|
||||
path := filepath.Join(migrationsDir, name)
|
||||
path := filepath.Join(db.MigrationsDir, name)
|
||||
fmt.Printf("Creating migration: %s\n", path)
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
return fmt.Errorf("File already exists")
|
||||
return fmt.Errorf("file already exists")
|
||||
}
|
||||
|
||||
// write new migration
|
||||
|
|
@ -111,14 +114,6 @@ func NewCommand(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// GetDatabaseURL returns the current environment database url
|
||||
func GetDatabaseURL(ctx *cli.Context) (u *url.URL, err error) {
|
||||
env := ctx.GlobalString("env")
|
||||
value := os.Getenv(env)
|
||||
|
||||
return url.Parse(value)
|
||||
}
|
||||
|
||||
func doTransaction(db *sql.DB, txFunc func(Transaction) error) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
|
|
@ -136,50 +131,44 @@ func doTransaction(db *sql.DB, txFunc func(Transaction) error) error {
|
|||
return tx.Commit()
|
||||
}
|
||||
|
||||
func openDatabaseForMigration(ctx *cli.Context) (Driver, *sql.DB, error) {
|
||||
u, err := GetDatabaseURL(ctx)
|
||||
func (db *DB) openDatabaseForMigration() (Driver, *sql.DB, error) {
|
||||
drv, err := db.GetDriver()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
drv, err := GetDriver(u.Scheme)
|
||||
sqlDB, err := drv.Open(db.DatabaseURL)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
db, err := drv.Open(u)
|
||||
if err != nil {
|
||||
if err := drv.CreateMigrationsTable(sqlDB); err != nil {
|
||||
mustClose(sqlDB)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := drv.CreateMigrationsTable(db); err != nil {
|
||||
mustClose(db)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return drv, db, nil
|
||||
return drv, sqlDB, nil
|
||||
}
|
||||
|
||||
// MigrateCommand migrates database to the latest version
|
||||
func MigrateCommand(ctx *cli.Context) error {
|
||||
migrationsDir := ctx.GlobalString("migrations-dir")
|
||||
// Migrate migrates database to the latest version
|
||||
func (db *DB) Migrate() error {
|
||||
re := regexp.MustCompile(`^\d.*\.sql$`)
|
||||
files, err := findMigrationFiles(migrationsDir, re)
|
||||
files, err := findMigrationFiles(db.MigrationsDir, re)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("No migration files found.")
|
||||
return fmt.Errorf("no migration files found")
|
||||
}
|
||||
|
||||
drv, db, err := openDatabaseForMigration(ctx)
|
||||
drv, sqlDB, err := db.openDatabaseForMigration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mustClose(db)
|
||||
defer mustClose(sqlDB)
|
||||
|
||||
applied, err := drv.SelectMigrations(db, -1)
|
||||
applied, err := drv.SelectMigrations(sqlDB, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -193,13 +182,13 @@ func MigrateCommand(ctx *cli.Context) error {
|
|||
|
||||
fmt.Printf("Applying: %s\n", filename)
|
||||
|
||||
migration, err := parseMigration(filepath.Join(migrationsDir, filename))
|
||||
migration, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// begin transaction
|
||||
err = doTransaction(db, func(tx Transaction) error {
|
||||
err = doTransaction(sqlDB, func(tx Transaction) error {
|
||||
// run actual migration
|
||||
if _, err := tx.Exec(migration["up"]); err != nil {
|
||||
return err
|
||||
|
|
@ -224,7 +213,7 @@ func MigrateCommand(ctx *cli.Context) error {
|
|||
func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not find migrations directory `%s`.", dir)
|
||||
return nil, fmt.Errorf("could not find migrations directory `%s`", dir)
|
||||
}
|
||||
|
||||
matches := []string{}
|
||||
|
|
@ -260,7 +249,7 @@ func findMigrationFile(dir string, ver string) (string, error) {
|
|||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return "", fmt.Errorf("Can't find migration file: %s*.sql", ver)
|
||||
return "", fmt.Errorf("can't find migration file: %s*.sql", ver)
|
||||
}
|
||||
|
||||
return files[0], nil
|
||||
|
|
@ -307,15 +296,15 @@ func parseMigration(path string) (map[string]string, error) {
|
|||
return migrations, nil
|
||||
}
|
||||
|
||||
// RollbackCommand rolls back the most recent migration
|
||||
func RollbackCommand(ctx *cli.Context) error {
|
||||
drv, db, err := openDatabaseForMigration(ctx)
|
||||
// Rollback rolls back the most recent migration
|
||||
func (db *DB) Rollback() error {
|
||||
drv, sqlDB, err := db.openDatabaseForMigration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mustClose(db)
|
||||
defer mustClose(sqlDB)
|
||||
|
||||
applied, err := drv.SelectMigrations(db, 1)
|
||||
applied, err := drv.SelectMigrations(sqlDB, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -326,24 +315,23 @@ func RollbackCommand(ctx *cli.Context) error {
|
|||
latest = ver
|
||||
}
|
||||
if latest == "" {
|
||||
return fmt.Errorf("Can't rollback: no migrations have been applied.")
|
||||
return fmt.Errorf("can't rollback: no migrations have been applied")
|
||||
}
|
||||
|
||||
migrationsDir := ctx.GlobalString("migrations-dir")
|
||||
filename, err := findMigrationFile(migrationsDir, latest)
|
||||
filename, err := findMigrationFile(db.MigrationsDir, latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Rolling back: %s\n", filename)
|
||||
|
||||
migration, err := parseMigration(filepath.Join(migrationsDir, filename))
|
||||
migration, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// begin transaction
|
||||
err = doTransaction(db, func(tx Transaction) error {
|
||||
err = doTransaction(sqlDB, func(tx Transaction) error {
|
||||
// rollback migration
|
||||
if _, err := tx.Exec(migration["down"]); err != nil {
|
||||
return err
|
||||
145
db_test.go
Normal file
145
db_test.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package dbmate
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var testdataDir string
|
||||
|
||||
func newTestDB(t *testing.T, u *url.URL) *DB {
|
||||
var err error
|
||||
|
||||
// only chdir once, because testdata is relative to current directory
|
||||
if testdataDir == "" {
|
||||
testdataDir, err = filepath.Abs("./testdata")
|
||||
require.Nil(t, err)
|
||||
|
||||
err = os.Chdir(testdataDir)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
return NewDB(u)
|
||||
}
|
||||
|
||||
func testURLs(t *testing.T) []*url.URL {
|
||||
return []*url.URL{
|
||||
postgresTestURL(t),
|
||||
mySQLTestURL(t),
|
||||
sqliteTestURL(t),
|
||||
}
|
||||
}
|
||||
|
||||
func testMigrateURL(t *testing.T, u *url.URL) {
|
||||
db := newTestDB(t, u)
|
||||
|
||||
// drop and recreate database
|
||||
err := db.Drop()
|
||||
require.Nil(t, err)
|
||||
err = db.Create()
|
||||
require.Nil(t, err)
|
||||
|
||||
// migrate
|
||||
err = db.Migrate()
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify results
|
||||
sqlDB, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(sqlDB)
|
||||
|
||||
count := 0
|
||||
err = sqlDB.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
err = sqlDB.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestMigrate(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testMigrateURL(t, u)
|
||||
}
|
||||
}
|
||||
|
||||
func testUpURL(t *testing.T, u *url.URL) {
|
||||
db := newTestDB(t, u)
|
||||
|
||||
// drop database
|
||||
err := db.Drop()
|
||||
require.Nil(t, err)
|
||||
|
||||
// create and migrate
|
||||
err = db.Up()
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify results
|
||||
sqlDB, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(sqlDB)
|
||||
|
||||
count := 0
|
||||
err = sqlDB.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
err = sqlDB.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
}
|
||||
|
||||
func TestUp(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testUpURL(t, u)
|
||||
}
|
||||
}
|
||||
|
||||
func testRollbackURL(t *testing.T, u *url.URL) {
|
||||
db := newTestDB(t, u)
|
||||
|
||||
// drop, recreate, and migrate database
|
||||
err := db.Drop()
|
||||
require.Nil(t, err)
|
||||
err = db.Create()
|
||||
require.Nil(t, err)
|
||||
err = db.Migrate()
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify migration
|
||||
sqlDB, err := GetDriverOpen(u)
|
||||
require.Nil(t, err)
|
||||
defer mustClose(sqlDB)
|
||||
|
||||
count := 0
|
||||
err = sqlDB.QueryRow(`select count(*) from schema_migrations
|
||||
where version = '20151129054053'`).Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 1, count)
|
||||
|
||||
// rollback
|
||||
err = db.Rollback()
|
||||
require.Nil(t, err)
|
||||
|
||||
// verify rollback
|
||||
err = sqlDB.QueryRow("select count(*) from schema_migrations").Scan(&count)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, count)
|
||||
|
||||
err = sqlDB.QueryRow("select count(*) from users").Scan(&count)
|
||||
require.NotNil(t, err)
|
||||
require.Regexp(t, "(does not exist|doesn't exist|no such table)", err.Error())
|
||||
}
|
||||
|
||||
func TestRollback(t *testing.T) {
|
||||
for _, u := range testURLs(t) {
|
||||
testRollbackURL(t, u)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
@ -33,7 +33,7 @@ func GetDriver(name string) (Driver, error) {
|
|||
case "sqlite", "sqlite3":
|
||||
return SQLiteDriver{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown driver: %s", name)
|
||||
return nil, fmt.Errorf("unknown driver: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -22,6 +22,6 @@ func TestGetDriver_MySQL(t *testing.T) {
|
|||
|
||||
func TestGetDriver_Error(t *testing.T) {
|
||||
drv, err := GetDriver("foo")
|
||||
require.Equal(t, "Unknown driver: foo", err.Error())
|
||||
require.Equal(t, "unknown driver: foo", err.Error())
|
||||
require.Nil(t, drv)
|
||||
}
|
||||
|
|
|
|||
91
main.go
91
main.go
|
|
@ -1,91 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
loadDotEnv()
|
||||
|
||||
app := NewApp()
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// NewApp creates a new command line app
|
||||
func NewApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "dbmate"
|
||||
app.Usage = "A lightweight, framework-independent database migration tool."
|
||||
app.Version = Version
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "migrations-dir, d",
|
||||
Value: "./db/migrations",
|
||||
Usage: "specify the directory containing migration files",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "env, e",
|
||||
Value: "DATABASE_URL",
|
||||
Usage: "specify an environment variable containing the database URL",
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Generate a new migration file",
|
||||
Action: NewCommand,
|
||||
},
|
||||
{
|
||||
Name: "up",
|
||||
Usage: "Create database (if necessary) and migrate to the latest version",
|
||||
Action: UpCommand,
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "Create database",
|
||||
Action: CreateCommand,
|
||||
},
|
||||
{
|
||||
Name: "drop",
|
||||
Usage: "Drop database (if it exists)",
|
||||
Action: DropCommand,
|
||||
},
|
||||
{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate to the latest version",
|
||||
Action: MigrateCommand,
|
||||
},
|
||||
{
|
||||
Name: "rollback",
|
||||
Aliases: []string{"down"},
|
||||
Usage: "Rollback the most recent migration",
|
||||
Action: RollbackCommand,
|
||||
},
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
type command func(*cli.Context) error
|
||||
|
||||
func loadDotEnv() {
|
||||
if _, err := os.Stat(".env"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
}
|
||||
4
mysql.go
4
mysql.go
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
@ -6,7 +6,7 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/go-sql-driver/mysql" // mysql driver for database/sql
|
||||
)
|
||||
|
||||
// MySQLDriver provides top level database functions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"os"
|
||||
"regexp"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
_ "github.com/mattn/go-sqlite3" // sqlite driver for database/sql
|
||||
)
|
||||
|
||||
// SQLiteDriver provides top level database functions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
|
|
|||
2
utils.go
2
utils.go
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package dbmate
|
||||
|
||||
// Version of dbmate
|
||||
const Version = "1.2.1"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue