mirror of
https://github.com/TECHNOFAB11/dbmate.git
synced 2025-12-12 16:10:03 +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
|
.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
|
# required to force cgo (for sqlite driver) with cross compile
|
||||||
ENV CGO_ENABLED 1
|
ENV CGO_ENABLED 1
|
||||||
|
|
@ -19,6 +19,6 @@ COPY . $GOPATH/src/github.com/amacneil/dbmate
|
||||||
WORKDIR $GOPATH/src/github.com/amacneil/dbmate
|
WORKDIR $GOPATH/src/github.com/amacneil/dbmate
|
||||||
|
|
||||||
# build
|
# build
|
||||||
RUN go install -v
|
RUN go install -v ./cmd/dbmate
|
||||||
|
|
||||||
CMD dbmate
|
CMD dbmate
|
||||||
|
|
|
||||||
17
Makefile
17
Makefile
|
|
@ -1,7 +1,8 @@
|
||||||
DC := docker-compose
|
DC := docker-compose
|
||||||
BUILD_FLAGS := -ldflags '-s'
|
BUILD_FLAGS := -ldflags '-s'
|
||||||
|
PACKAGES := . ./cmd/dbmate
|
||||||
|
|
||||||
all: clean container lint test build
|
all: clean container test lint build
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
|
|
@ -12,15 +13,15 @@ container:
|
||||||
$(DC) up -d
|
$(DC) up -d
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
$(DC) run dbmate golint
|
$(DC) run dbmate golint -set_exit_status $(PACKAGES)
|
||||||
$(DC) run dbmate go vet
|
$(DC) run dbmate go vet $(PACKAGES)
|
||||||
$(DC) run dbmate errcheck
|
$(DC) run dbmate errcheck $(PACKAGES)
|
||||||
|
|
||||||
test:
|
test:
|
||||||
$(DC) run dbmate go test -v
|
$(DC) run dbmate go test -v $(PACKAGES)
|
||||||
|
|
||||||
build: clean
|
build: clean
|
||||||
$(DC) run -e GOARCH=386 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-i386
|
$(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
|
$(DC) run -e GOARCH=amd64 dbmate go build $(BUILD_FLAGS) -o dist/dbmate-linux-amd64 ./cmd/dbmate
|
||||||
# musl target does not support sqlite
|
# 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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -10,18 +10,33 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpCommand creates the database (if necessary) and runs migrations
|
// DefaultMigrationsDir specifies default directory to find migration files
|
||||||
func UpCommand(ctx *cli.Context) error {
|
var DefaultMigrationsDir = "./db/migrations"
|
||||||
u, err := GetDatabaseURL(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -29,71 +44,59 @@ func UpCommand(ctx *cli.Context) error {
|
||||||
// create database if it does not already exist
|
// create database if it does not already exist
|
||||||
// skip this step if we cannot determine status
|
// skip this step if we cannot determine status
|
||||||
// (e.g. user does not have list database permission)
|
// (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 == nil && !exists {
|
||||||
if err := drv.CreateDatabase(u); err != nil {
|
if err := drv.CreateDatabase(db.DatabaseURL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrate
|
// migrate
|
||||||
return MigrateCommand(ctx)
|
return db.Migrate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCommand creates the current database
|
// Create creates the current database
|
||||||
func CreateCommand(ctx *cli.Context) error {
|
func (db *DB) Create() error {
|
||||||
u, err := GetDatabaseURL(ctx)
|
drv, err := db.GetDriver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
drv, err := GetDriver(u.Scheme)
|
return drv.CreateDatabase(db.DatabaseURL)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return drv.CreateDatabase(u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DropCommand drops the current database (if it exists)
|
// Drop drops the current database (if it exists)
|
||||||
func DropCommand(ctx *cli.Context) error {
|
func (db *DB) Drop() error {
|
||||||
u, err := GetDatabaseURL(ctx)
|
drv, err := db.GetDriver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
drv, err := GetDriver(u.Scheme)
|
return drv.DropDatabase(db.DatabaseURL)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return drv.DropDatabase(u)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
||||||
|
|
||||||
// NewCommand creates a new migration file
|
// New creates a new migration file
|
||||||
func NewCommand(ctx *cli.Context) error {
|
func (db *DB) New(name string) error {
|
||||||
// new migration name
|
// new migration name
|
||||||
timestamp := time.Now().UTC().Format("20060102150405")
|
timestamp := time.Now().UTC().Format("20060102150405")
|
||||||
name := ctx.Args().First()
|
|
||||||
if name == "" {
|
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)
|
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
||||||
|
|
||||||
// create migrations dir if missing
|
// create migrations dir if missing
|
||||||
migrationsDir := ctx.GlobalString("migrations-dir")
|
if err := os.MkdirAll(db.MigrationsDir, 0755); err != nil {
|
||||||
if err := os.MkdirAll(migrationsDir, 0755); err != nil {
|
return fmt.Errorf("unable to create directory `%s`", db.MigrationsDir)
|
||||||
return fmt.Errorf("Unable to create directory `%s`.", migrationsDir)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check file does not already exist
|
// check file does not already exist
|
||||||
path := filepath.Join(migrationsDir, name)
|
path := filepath.Join(db.MigrationsDir, name)
|
||||||
fmt.Printf("Creating migration: %s\n", path)
|
fmt.Printf("Creating migration: %s\n", path)
|
||||||
|
|
||||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("File already exists")
|
return fmt.Errorf("file already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
// write new migration
|
// write new migration
|
||||||
|
|
@ -111,14 +114,6 @@ func NewCommand(ctx *cli.Context) error {
|
||||||
return nil
|
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 {
|
func doTransaction(db *sql.DB, txFunc func(Transaction) error) error {
|
||||||
tx, err := db.Begin()
|
tx, err := db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -136,50 +131,44 @@ func doTransaction(db *sql.DB, txFunc func(Transaction) error) error {
|
||||||
return tx.Commit()
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func openDatabaseForMigration(ctx *cli.Context) (Driver, *sql.DB, error) {
|
func (db *DB) openDatabaseForMigration() (Driver, *sql.DB, error) {
|
||||||
u, err := GetDatabaseURL(ctx)
|
drv, err := db.GetDriver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
drv, err := GetDriver(u.Scheme)
|
sqlDB, err := drv.Open(db.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := drv.Open(u)
|
if err := drv.CreateMigrationsTable(sqlDB); err != nil {
|
||||||
if err != nil {
|
mustClose(sqlDB)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := drv.CreateMigrationsTable(db); err != nil {
|
return drv, sqlDB, nil
|
||||||
mustClose(db)
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return drv, db, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrateCommand migrates database to the latest version
|
// Migrate migrates database to the latest version
|
||||||
func MigrateCommand(ctx *cli.Context) error {
|
func (db *DB) Migrate() error {
|
||||||
migrationsDir := ctx.GlobalString("migrations-dir")
|
|
||||||
re := regexp.MustCompile(`^\d.*\.sql$`)
|
re := regexp.MustCompile(`^\d.*\.sql$`)
|
||||||
files, err := findMigrationFiles(migrationsDir, re)
|
files, err := findMigrationFiles(db.MigrationsDir, re)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) == 0 {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer mustClose(db)
|
defer mustClose(sqlDB)
|
||||||
|
|
||||||
applied, err := drv.SelectMigrations(db, -1)
|
applied, err := drv.SelectMigrations(sqlDB, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -193,13 +182,13 @@ func MigrateCommand(ctx *cli.Context) error {
|
||||||
|
|
||||||
fmt.Printf("Applying: %s\n", filename)
|
fmt.Printf("Applying: %s\n", filename)
|
||||||
|
|
||||||
migration, err := parseMigration(filepath.Join(migrationsDir, filename))
|
migration, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin transaction
|
// begin transaction
|
||||||
err = doTransaction(db, func(tx Transaction) error {
|
err = doTransaction(sqlDB, func(tx Transaction) error {
|
||||||
// run actual migration
|
// run actual migration
|
||||||
if _, err := tx.Exec(migration["up"]); err != nil {
|
if _, err := tx.Exec(migration["up"]); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -224,7 +213,7 @@ func MigrateCommand(ctx *cli.Context) error {
|
||||||
func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
|
func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
|
||||||
files, err := ioutil.ReadDir(dir)
|
files, err := ioutil.ReadDir(dir)
|
||||||
if err != nil {
|
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{}
|
matches := []string{}
|
||||||
|
|
@ -260,7 +249,7 @@ func findMigrationFile(dir string, ver string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(files) == 0 {
|
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
|
return files[0], nil
|
||||||
|
|
@ -307,15 +296,15 @@ func parseMigration(path string) (map[string]string, error) {
|
||||||
return migrations, nil
|
return migrations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RollbackCommand rolls back the most recent migration
|
// Rollback rolls back the most recent migration
|
||||||
func RollbackCommand(ctx *cli.Context) error {
|
func (db *DB) Rollback() error {
|
||||||
drv, db, err := openDatabaseForMigration(ctx)
|
drv, sqlDB, err := db.openDatabaseForMigration()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer mustClose(db)
|
defer mustClose(sqlDB)
|
||||||
|
|
||||||
applied, err := drv.SelectMigrations(db, 1)
|
applied, err := drv.SelectMigrations(sqlDB, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -326,24 +315,23 @@ func RollbackCommand(ctx *cli.Context) error {
|
||||||
latest = ver
|
latest = ver
|
||||||
}
|
}
|
||||||
if latest == "" {
|
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(db.MigrationsDir, latest)
|
||||||
filename, err := findMigrationFile(migrationsDir, latest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Rolling back: %s\n", filename)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin transaction
|
// begin transaction
|
||||||
err = doTransaction(db, func(tx Transaction) error {
|
err = doTransaction(sqlDB, func(tx Transaction) error {
|
||||||
// rollback migration
|
// rollback migration
|
||||||
if _, err := tx.Exec(migration["down"]); err != nil {
|
if _, err := tx.Exec(migration["down"]); err != nil {
|
||||||
return err
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -33,7 +33,7 @@ func GetDriver(name string) (Driver, error) {
|
||||||
case "sqlite", "sqlite3":
|
case "sqlite", "sqlite3":
|
||||||
return SQLiteDriver{}, nil
|
return SQLiteDriver{}, nil
|
||||||
default:
|
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 (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -22,6 +22,6 @@ func TestGetDriver_MySQL(t *testing.T) {
|
||||||
|
|
||||||
func TestGetDriver_Error(t *testing.T) {
|
func TestGetDriver_Error(t *testing.T) {
|
||||||
drv, err := GetDriver("foo")
|
drv, err := GetDriver("foo")
|
||||||
require.Equal(t, "Unknown driver: foo", err.Error())
|
require.Equal(t, "unknown driver: foo", err.Error())
|
||||||
require.Nil(t, drv)
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql" // mysql driver for database/sql
|
||||||
)
|
)
|
||||||
|
|
||||||
// MySQLDriver provides top level database functions
|
// MySQLDriver provides top level database functions
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3" // sqlite driver for database/sql
|
||||||
)
|
)
|
||||||
|
|
||||||
// SQLiteDriver provides top level database functions
|
// SQLiteDriver provides top level database functions
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
|
||||||
2
utils.go
2
utils.go
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package dbmate
|
||||||
|
|
||||||
// Version of dbmate
|
// Version of dbmate
|
||||||
const Version = "1.2.1"
|
const Version = "1.2.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue