Merge pull request #1 from pressly/refactor

Refactor
pull/2/head
Vojtech Vitek 2016-03-03 15:01:03 -05:00
commit ece53000a6
30 changed files with 287 additions and 856 deletions

16
.travis.yml Normal file
View File

@ -0,0 +1,16 @@
sudo: false
language: go
go:
- 1.6
- tip
install:
- go get github.com/go-sql-driver/mysql
- go get github.com/lib/pq
- go get github.com/mattn/go-sqlite3
- go get github.com/ziutek/mymysql/godrv
script:
- go test
- go run ./cmd/goose/main.go sqlite3 foo.db up

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
Original work Copyright (c) 2012 Liam Staskawicz
Modified work Copyright (c) 2016 Vojtech Vitek
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,7 +0,0 @@
Copyright (c) <2012> <Liam Staskawicz>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

122
README.md
View File

@ -1,24 +1,38 @@
# goose
goose is a database migration tool.
Goose is a database migration tool. Manage your database's evolution by creating incremental SQL files or Go functions.
You can manage your database's evolution by creating incremental SQL or Go scripts.
[![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis]
[![Build Status](https://drone.io/bitbucket.org/liamstask/goose/status.png)](https://drone.io/bitbucket.org/liamstask/goose/latest)
This is a fork of https://bitbucket.org/liamstask/goose with following differences:
- No config files
- Meant to be imported by your application, so you can run complex Go migration functions with your own DB driver
- Standalone goose binary can only run SQL files -- we dropped building .go files in favor of the above
# Goals
- [x] Move lib/goose to top level directory
- [x] Remove all config files
- [x] Commands should be part of the API
- [x] Update & finish README
- [ ] Registry for Go migration functions
# Install
$ go get bitbucket.org/liamstask/goose/cmd/goose
$ go get -u github.com/pressly/cmd/goose
This will install the `goose` binary to your `$GOPATH/bin` directory.
You can also build goose into your own applications by importing `bitbucket.org/liamstask/goose/lib/goose`. Documentation is available at [godoc.org](http://godoc.org/bitbucket.org/liamstask/goose/lib/goose).
NOTE: the API is still new, and may undergo some changes.
*Note: A standalone goose binary can only run pure SQL migrations. To run complex Go migrations, you have to import this pkg and register functions.*
# Usage
goose provides several commands to help manage your database schema.
`goose [OPTIONS] DRIVER DBSTRING COMMAND`
Examples:
$ goose postgres "user=postgres dbname=postgres sslmode=disable" up
$ goose mysql "user:password@/dbname" down
$ goose sqlite3 ./foo.db status
## create
@ -175,91 +189,11 @@ The numeric portion of the function name (`20130106222315`) must be the leading
A transaction is provided, rather than the DB instance directly, since goose also needs to record the schema version within the same transaction. Each migration should run as a single transaction to ensure DB integrity, so it's good practice anyway.
## License
# Configuration
Licensed under [MIT License](./LICENSE)
goose expects you to maintain a folder (typically called "db"), which contains the following:
* a `dbconf.yml` file that describes the database configurations you'd like to use
* a folder called "migrations" which contains `.sql` and/or `.go` scripts that implement your migrations
You may use the `-path` option to specify an alternate location for the folder containing your config and migrations.
A sample `dbconf.yml` looks like
```yml
development:
driver: postgres
open: user=liam dbname=tester sslmode=disable
```
Here, `development` specifies the name of the environment, and the `driver` and `open` elements are passed directly to database/sql to access the specified database.
You may include as many environments as you like, and you can use the `-env` command line option to specify which one to use. goose defaults to using an environment called `development`.
goose will expand environment variables in the `open` element. For an example, see the Heroku section below.
## Other Drivers
goose knows about some common SQL drivers, but it can still be used to run Go-based migrations with any driver supported by `database/sql`. An import path and known dialect are required.
Currently, available dialects are: "postgres", "mysql", or "sqlite3"
To run Go-based migrations with another driver, specify its import path and dialect, as shown below.
```yml
customdriver:
driver: custom
open: custom open string
import: github.com/custom/driver
dialect: mysql
```
NOTE: Because migrations written in SQL are executed directly by the goose binary, only drivers compiled into goose may be used for these migrations.
## Using goose with Heroku
These instructions assume that you're using [Keith Rarick's Heroku Go buildpack](https://github.com/kr/heroku-buildpack-go). First, add a file to your project called (e.g.) `install_goose.go` to trigger building of the goose executable during deployment, with these contents:
```go
// use build constraints to work around http://code.google.com/p/go/issues/detail?id=4210
// +build heroku
// note: need at least one blank line after build constraint
package main
import _ "bitbucket.org/liamstask/goose/cmd/goose"
```
[Set up your Heroku database(s) as usual.](https://devcenter.heroku.com/articles/heroku-postgresql)
Then make use of environment variable expansion in your `dbconf.yml`:
```yml
production:
driver: postgres
open: $DATABASE_URL
```
To run goose in production, use `heroku run`:
heroku run goose -env production up
# Contributors
Thank you!
* Josh Bleecher Snyder (josharian)
* Abigail Walthall (ghthor)
* Daniel Heath (danielrheath)
* Chris Baynes (chris_baynes)
* Michael Gerow (gerow)
* Vytautas Šaltenis (rtfb)
* James Cooper (coopernurse)
* Gyepi Sam (gyepisam)
* Matt Sherman (clipperhouse)
* runner_mei
* John Luebs (jkl1337)
* Luke Hutton (lukehutton)
* Kevin Gorjan (kevingorjan)
* Brendan Fosberry (Fozz)
* Nate Guerin (gusennan)
[GoDoc]: https://godoc.org/github.com/pressly/goose
[GoDoc Widget]: https://godoc.org/github.com/pressly/goose?status.svg
[Travis]: https://travis-ci.org/pressly/goose
[Travis Widget]: https://travis-ci.org/pressly/goose.svg?branch=master

View File

@ -1,27 +0,0 @@
package main
import (
"flag"
)
// shamelessly snagged from the go tool
// each command gets its own set of args,
// defines its own entry point, and provides its own help
type Command struct {
Run func(cmd *Command, args ...string)
Flag flag.FlagSet
Name string
Usage string
Summary string
Help string
}
func (c *Command) Exec(args []string) {
c.Flag.Usage = func() {
// helpFunc(c, c.Name)
}
c.Flag.Parse(args)
c.Run(c, c.Flag.Args()...)
}

View File

@ -1,51 +0,0 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"fmt"
"log"
"os"
"path/filepath"
"time"
)
var createCmd = &Command{
Name: "create",
Usage: "",
Summary: "Create the scaffolding for a new migration",
Help: `create extended help here...`,
Run: createRun,
}
func createRun(cmd *Command, args ...string) {
if len(args) < 1 {
log.Fatal("goose create: migration name required")
}
migrationType := "go" // default to Go migrations
if len(args) >= 2 {
migrationType = args[1]
}
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
if err = os.MkdirAll(conf.MigrationsDir, 0777); err != nil {
log.Fatal(err)
}
n, err := goose.CreateMigration(args[0], migrationType, conf.MigrationsDir, time.Now())
if err != nil {
log.Fatal(err)
}
a, e := filepath.Abs(n)
if e != nil {
log.Fatal(e)
}
fmt.Println("goose: created", a)
}

View File

@ -1,29 +0,0 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"fmt"
"log"
)
var dbVersionCmd = &Command{
Name: "dbversion",
Usage: "",
Summary: "Print the current version of the database",
Help: `dbversion extended help here...`,
Run: dbVersionRun,
}
func dbVersionRun(cmd *Command, args ...string) {
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
current, err := goose.GetDBVersion(conf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("goose: dbversion %v\n", current)
}

View File

@ -1,36 +0,0 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"log"
)
var downCmd = &Command{
Name: "down",
Usage: "",
Summary: "Roll back the version by 1",
Help: `down extended help here...`,
Run: downRun,
}
func downRun(cmd *Command, args ...string) {
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
current, err := goose.GetDBVersion(conf)
if err != nil {
log.Fatal(err)
}
previous, err := goose.GetPreviousDBVersion(conf.MigrationsDir, current)
if err != nil {
log.Fatal(err)
}
if err = goose.RunMigrations(conf, conf.MigrationsDir, previous); err != nil {
log.Fatal(err)
}
}

View File

@ -1,39 +0,0 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"log"
)
var redoCmd = &Command{
Name: "redo",
Usage: "",
Summary: "Re-run the latest migration",
Help: `redo extended help here...`,
Run: redoRun,
}
func redoRun(cmd *Command, args ...string) {
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
current, err := goose.GetDBVersion(conf)
if err != nil {
log.Fatal(err)
}
previous, err := goose.GetPreviousDBVersion(conf.MigrationsDir, current)
if err != nil {
log.Fatal(err)
}
if err := goose.RunMigrations(conf, conf.MigrationsDir, previous); err != nil {
log.Fatal(err)
}
if err := goose.RunMigrations(conf, conf.MigrationsDir, current); err != nil {
log.Fatal(err)
}
}

View File

@ -1,31 +0,0 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"log"
)
var upCmd = &Command{
Name: "up",
Usage: "",
Summary: "Migrate the DB to the most recent version available",
Help: `up extended help here...`,
Run: upRun,
}
func upRun(cmd *Command, args ...string) {
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
target, err := goose.GetMostRecentDBVersion(conf.MigrationsDir)
if err != nil {
log.Fatal(err)
}
if err := goose.RunMigrations(conf, conf.MigrationsDir, target); err != nil {
log.Fatal(err)
}
}

View File

@ -1,78 +1,90 @@
package main
import (
"bitbucket.org/liamstask/goose/lib/goose"
"database/sql"
"flag"
"fmt"
"log"
"os"
"strings"
"text/template"
"github.com/pressly/goose"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
_ "github.com/ziutek/mymysql/godrv"
)
// global options. available to any subcommands.
var flagPath = flag.String("path", "db", "folder containing db info")
var flagEnv = flag.String("env", "development", "which DB environment to use")
var flagPgSchema = flag.String("pgschema", "", "which postgres-schema to migrate (default = none)")
// helper to create a DBConf from the given flags
func dbConfFromFlags() (dbconf *goose.DBConf, err error) {
return goose.NewDBConf(*flagPath, *flagEnv, *flagPgSchema)
}
var commands = []*Command{
upCmd,
downCmd,
redoCmd,
statusCmd,
createCmd,
dbVersionCmd,
}
var (
flags = flag.NewFlagSet("goose", flag.ExitOnError)
dir = flags.String("dir", ".", "directory with migration files")
)
func main() {
flags.Usage = usage
flags.Parse(os.Args[1:])
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) == 0 || args[0] == "-h" {
flag.Usage()
args := flags.Args()
if len(args) != 3 {
flags.Usage()
return
}
var cmd *Command
name := args[0]
for _, c := range commands {
if strings.HasPrefix(c.Name, name) {
cmd = c
break
if args[0] == "-h" || args[0] == "--help" {
flags.Usage()
return
}
driver, dbstring, command := args[0], args[1], args[2]
switch driver {
case "postgres", "mysql", "sqlite3":
if err := goose.SetDialect(driver); err != nil {
log.Fatal(err)
}
default:
log.Fatalf("%q driver not supported\n", driver)
}
if cmd == nil {
fmt.Printf("error: unknown command %q\n", name)
flag.Usage()
os.Exit(1)
switch dbstring {
case "":
log.Fatalf("-dbstring=%q not supported\n", dbstring)
default:
}
cmd.Exec(args[1:])
db, err := sql.Open(driver, dbstring)
if err != nil {
log.Fatalf("-dbstring=%q: %v\n", dbstring, err)
}
if err := goose.Run(command, db, *dir); err != nil {
log.Fatalf("goose run: %v", err)
}
}
func usage() {
fmt.Print(usagePrefix)
flag.PrintDefaults()
usageTmpl.Execute(os.Stdout, commands)
flags.PrintDefaults()
fmt.Print(usageCommands)
}
var usagePrefix = `
goose is a database migration management system for Go projects.
var (
usagePrefix = `Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
Usage:
goose [options] <subcommand> [subcommand options]
Examples:
goose postgres "user=postgres dbname=postgres sslmode=disable" up
goose mysql "user:password@/dbname" down
goose sqlite3 ./foo.db status
Options:
`
var usageTmpl = template.Must(template.New("usage").Parse(
`
Commands:{{range .}}
{{.Name | printf "%-10s"}} {{.Summary}}{{end}}
`))
usageCommands = `
Commands:
up Migrate the DB to the most recent version available
down Roll back the version by 1
redo Re-run the latest migration
status Dump the migration status for the current DB
dbversion Print the current version of the database
`
)

View File

@ -1,22 +0,0 @@
test:
driver: postgres
open: user=liam dbname=tester sslmode=disable
development:
driver: postgres
open: user=liam dbname=tester sslmode=disable
production:
driver: postgres
open: user=liam dbname=tester sslmode=verify-full
customimport:
driver: customdriver
open: customdriver open
import: github.com/custom/driver
dialect: mysql
environment_variable_config:
driver: $DB_DRIVER
open: $DATABASE_URL

View File

@ -1,15 +0,0 @@
package main
import (
"database/sql"
"fmt"
)
func Up_20130106222315(txn *sql.Tx) {
fmt.Println("Hello from migration 20130106222315 Up!")
}
func Down_20130106222315(txn *sql.Tx) {
fmt.Println("Hello from migration 20130106222315 Down!")
}

View File

@ -2,6 +2,8 @@ package goose
import (
"database/sql"
"fmt"
"github.com/mattn/go-sqlite3"
)
@ -13,15 +15,22 @@ type SqlDialect interface {
dbVersionQuery(db *sql.DB) (*sql.Rows, error)
}
// drivers that we don't know about can ask for a dialect by name
func dialectByName(d string) SqlDialect {
var dialect SqlDialect = &PostgresDialect{}
func GetDialect() SqlDialect {
return dialect
}
func SetDialect(d string) error {
switch d {
case "postgres":
return &PostgresDialect{}
dialect = &PostgresDialect{}
case "mysql":
return &MySqlDialect{}
dialect = &MySqlDialect{}
case "sqlite3":
return &Sqlite3Dialect{}
dialect = &Sqlite3Dialect{}
default:
return fmt.Errorf("%q: unknown dialect", d)
}
return nil

23
down.go Normal file
View File

@ -0,0 +1,23 @@
package goose
import (
"database/sql"
)
func Down(db *sql.DB, dir string) error {
current, err := GetDBVersion(db)
if err != nil {
return err
}
previous, err := GetPreviousDBVersion(dir, current)
if err != nil {
return err
}
if err = RunMigrations(db, dir, previous); err != nil {
return err
}
return nil
}

View File

@ -1,4 +1,3 @@
-- +goose Up
CREATE TABLE post (
id int NOT NULL,

View File

@ -1,4 +1,3 @@
-- +goose Up
CREATE TABLE fancier_post (
id int NOT NULL,

34
goose.go Normal file
View File

@ -0,0 +1,34 @@
package goose
import (
"database/sql"
"fmt"
)
func Run(command string, db *sql.DB, dir string) error {
switch command {
case "up":
if err := Up(db, dir); err != nil {
return err
}
case "down":
if err := Down(db, dir); err != nil {
return err
}
case "redo":
if err := Redo(db, dir); err != nil {
return err
}
case "status":
if err := Status(db, dir); err != nil {
return err
}
case "version":
if err := Version(db, dir); err != nil {
return err
}
default:
return fmt.Errorf("%q: no such command", command)
}
return nil
}

View File

@ -1,139 +0,0 @@
package goose
import (
"database/sql"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/kylelemons/go-gypsy/yaml"
"github.com/lib/pq"
)
// DBDriver encapsulates the info needed to work with
// a specific database driver
type DBDriver struct {
Name string
OpenStr string
Import string
Dialect SqlDialect
}
type DBConf struct {
MigrationsDir string
Env string
Driver DBDriver
PgSchema string
}
// extract configuration details from the given file
func NewDBConf(p, env string, pgschema string) (*DBConf, error) {
cfgFile := filepath.Join(p, "dbconf.yml")
f, err := yaml.ReadFile(cfgFile)
if err != nil {
return nil, err
}
drv, err := f.Get(fmt.Sprintf("%s.driver", env))
if err != nil {
return nil, err
}
drv = os.ExpandEnv(drv)
open, err := f.Get(fmt.Sprintf("%s.open", env))
if err != nil {
return nil, err
}
open = os.ExpandEnv(open)
// Automatically parse postgres urls
if drv == "postgres" {
// Assumption: If we can parse the URL, we should
if parsedURL, err := pq.ParseURL(open); err == nil && parsedURL != "" {
open = parsedURL
}
}
d := newDBDriver(drv, open)
// allow the configuration to override the Import for this driver
if imprt, err := f.Get(fmt.Sprintf("%s.import", env)); err == nil {
d.Import = imprt
}
// allow the configuration to override the Dialect for this driver
if dialect, err := f.Get(fmt.Sprintf("%s.dialect", env)); err == nil {
d.Dialect = dialectByName(dialect)
}
if !d.IsValid() {
return nil, errors.New(fmt.Sprintf("Invalid DBConf: %v", d))
}
return &DBConf{
MigrationsDir: filepath.Join(p, "migrations"),
Env: env,
Driver: d,
PgSchema: pgschema,
}, nil
}
// Create a new DBDriver and populate driver specific
// fields for drivers that we know about.
// Further customization may be done in NewDBConf
func newDBDriver(name, open string) DBDriver {
d := DBDriver{
Name: name,
OpenStr: open,
}
switch name {
case "postgres":
d.Import = "github.com/lib/pq"
d.Dialect = &PostgresDialect{}
case "mymysql":
d.Import = "github.com/ziutek/mymysql/godrv"
d.Dialect = &MySqlDialect{}
case "mysql":
d.Import = "github.com/go-sql-driver/mysql"
d.Dialect = &MySqlDialect{}
case "sqlite3":
d.Import = "github.com/mattn/go-sqlite3"
d.Dialect = &Sqlite3Dialect{}
}
return d
}
// ensure we have enough info about this driver
func (drv *DBDriver) IsValid() bool {
return len(drv.Import) > 0 && drv.Dialect != nil
}
// OpenDBFromDBConf wraps database/sql.DB.Open() and configures
// the newly opened DB based on the given DBConf.
//
// Callers must Close() the returned DB.
func OpenDBFromDBConf(conf *DBConf) (*sql.DB, error) {
db, err := sql.Open(conf.Driver.Name, conf.Driver.OpenStr)
if err != nil {
return nil, err
}
// if a postgres schema has been specified, apply it
if conf.Driver.Name == "postgres" && conf.PgSchema != "" {
if _, err := db.Exec("SET search_path TO " + conf.PgSchema); err != nil {
return nil, err
}
}
return db, nil
}

View File

@ -1,70 +0,0 @@
package goose
import (
"os"
"reflect"
"testing"
)
func TestBasics(t *testing.T) {
dbconf, err := NewDBConf("../../db-sample", "test", "")
if err != nil {
t.Fatal(err)
}
got := []string{dbconf.MigrationsDir, dbconf.Env, dbconf.Driver.Name, dbconf.Driver.OpenStr}
want := []string{"../../db-sample/migrations", "test", "postgres", "user=liam dbname=tester sslmode=disable"}
for i, s := range got {
if s != want[i] {
t.Errorf("Unexpected DBConf value. got %v, want %v", s, want[i])
}
}
}
func TestImportOverride(t *testing.T) {
dbconf, err := NewDBConf("../../db-sample", "customimport", "")
if err != nil {
t.Fatal(err)
}
got := dbconf.Driver.Import
want := "github.com/custom/driver"
if got != want {
t.Errorf("bad custom import. got %v want %v", got, want)
}
}
func TestDriverSetFromEnvironmentVariable(t *testing.T) {
databaseUrlEnvVariableKey := "DB_DRIVER"
databaseUrlEnvVariableVal := "sqlite3"
databaseOpenStringKey := "DATABASE_URL"
databaseOpenStringVal := "db.db"
os.Setenv(databaseUrlEnvVariableKey, databaseUrlEnvVariableVal)
os.Setenv(databaseOpenStringKey, databaseOpenStringVal)
dbconf, err := NewDBConf("../../db-sample", "environment_variable_config", "")
if err != nil {
t.Fatal(err)
}
got := reflect.TypeOf(dbconf.Driver.Dialect)
want := reflect.TypeOf(&Sqlite3Dialect{})
if got != want {
t.Errorf("Not able to read the driver type from environment variable."+
"got %v want %v", got, want)
}
gotOpenString := dbconf.Driver.OpenStr
wantOpenString := databaseOpenStringVal
if gotOpenString != wantOpenString {
t.Errorf("Not able to read the open string from the environment."+
"got %v want %v", gotOpenString, wantOpenString)
}
}

View File

@ -1,137 +0,0 @@
package goose
import (
"bytes"
"encoding/gob"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"text/template"
)
type templateData struct {
Version int64
Import string
Conf string // gob encoded DBConf
Direction bool
Func string
InsertStmt string
}
func init() {
gob.Register(PostgresDialect{})
gob.Register(MySqlDialect{})
gob.Register(Sqlite3Dialect{})
}
//
// Run a .go migration.
//
// In order to do this, we copy a modified version of the
// original .go migration, and execute it via `go run` along
// with a main() of our own creation.
//
func runGoMigration(conf *DBConf, path string, version int64, direction bool) error {
// everything gets written to a temp dir, and zapped afterwards
d, e := ioutil.TempDir("", "goose")
if e != nil {
log.Fatal(e)
}
defer os.RemoveAll(d)
directionStr := "Down"
if direction {
directionStr = "Up"
}
var bb bytes.Buffer
if err := gob.NewEncoder(&bb).Encode(conf); err != nil {
return err
}
// XXX: there must be a better way of making this byte array
// available to the generated code...
// but for now, print an array literal of the gob bytes
var sb bytes.Buffer
sb.WriteString("[]byte{ ")
for _, b := range bb.Bytes() {
sb.WriteString(fmt.Sprintf("0x%02x, ", b))
}
sb.WriteString("}")
td := &templateData{
Version: version,
Import: conf.Driver.Import,
Conf: sb.String(),
Direction: direction,
Func: fmt.Sprintf("%v_%v", directionStr, version),
InsertStmt: conf.Driver.Dialect.insertVersionSql(),
}
main, e := writeTemplateToFile(filepath.Join(d, "goose_main.go"), goMigrationDriverTemplate, td)
if e != nil {
log.Fatal(e)
}
outpath := filepath.Join(d, filepath.Base(path))
if _, e = copyFile(outpath, path); e != nil {
log.Fatal(e)
}
cmd := exec.Command("go", "run", main, outpath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if e = cmd.Run(); e != nil {
log.Fatal("`go run` failed: ", e)
}
return nil
}
//
// template for the main entry point to a go-based migration.
// this gets linked against the substituted versions of the user-supplied
// scripts in order to execute a migration via `go run`
//
var goMigrationDriverTemplate = template.Must(template.New("goose.go-driver").Parse(`
package main
import (
"log"
"bytes"
"encoding/gob"
_ "{{.Import}}"
"bitbucket.org/liamstask/goose/lib/goose"
)
func main() {
var conf goose.DBConf
buf := bytes.NewBuffer({{ .Conf }})
if err := gob.NewDecoder(buf).Decode(&conf); err != nil {
log.Fatal("gob.Decode - ", err)
}
db, err := goose.OpenDBFromDBConf(&conf)
if err != nil {
log.Fatal("failed to open DB:", err)
}
defer db.Close()
txn, err := db.Begin()
if err != nil {
log.Fatal("db.Begin:", err)
}
{{ .Func }}(txn)
err = goose.FinalizeMigration(&conf, txn, {{ .Direction }}, {{ .Version }})
if err != nil {
log.Fatal("Commit() failed:", err)
}
}
`))

View File

@ -12,11 +12,6 @@ import (
"strings"
"text/template"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
_ "github.com/ziutek/mymysql/godrv"
)
var (
@ -48,25 +43,13 @@ func newMigration(v int64, src string) *Migration {
return &Migration{v, -1, -1, src}
}
func RunMigrations(conf *DBConf, migrationsDir string, target int64) (err error) {
db, err := OpenDBFromDBConf(conf)
if err != nil {
return err
}
defer db.Close()
return RunMigrationsOnDb(conf, migrationsDir, target, db)
}
// Runs migration on a specific database instance.
func RunMigrationsOnDb(conf *DBConf, migrationsDir string, target int64, db *sql.DB) (err error) {
current, err := EnsureDBVersion(conf, db)
func RunMigrations(db *sql.DB, dir string, target int64) (err error) {
current, err := EnsureDBVersion(db)
if err != nil {
return err
}
migrations, err := CollectMigrations(migrationsDir, current, target)
migrations, err := CollectMigrations(dir, current, target)
if err != nil {
return err
}
@ -80,18 +63,17 @@ func RunMigrationsOnDb(conf *DBConf, migrationsDir string, target int64, db *sql
direction := current < target
ms.Sort(direction)
fmt.Printf("goose: migrating db environment '%v', current version: %d, target: %d\n",
conf.Env, current, target)
fmt.Printf("goose: migrating db, current version: %d, target: %d\n", current, target)
for _, m := range ms {
switch filepath.Ext(m.Source) {
case ".go":
err = runGoMigration(conf, m.Source, m.Version, direction)
case ".sql":
err = runSQLMigration(conf, db, m.Source, m.Version, direction)
default:
continue
}
err = runSQLMigration(db, m.Source, m.Version, direction)
if err != nil {
return errors.New(fmt.Sprintf("FAIL %v, quitting migration", err))
}
@ -192,12 +174,12 @@ func NumericComponent(name string) (int64, error) {
// retrieve the current version for this DB.
// Create and initialize the DB version table if it doesn't exist.
func EnsureDBVersion(conf *DBConf, db *sql.DB) (int64, error) {
func EnsureDBVersion(db *sql.DB) (int64, error) {
rows, err := conf.Driver.Dialect.dbVersionQuery(db)
rows, err := GetDialect().dbVersionQuery(db)
if err != nil {
if err == ErrTableDoesNotExist {
return 0, createVersionTable(conf, db)
return 0, createVersionTable(db)
}
return 0, err
}
@ -242,13 +224,13 @@ func EnsureDBVersion(conf *DBConf, db *sql.DB) (int64, error) {
// Create the goose_db_version table
// and insert the initial 0 value into it
func createVersionTable(conf *DBConf, db *sql.DB) error {
func createVersionTable(db *sql.DB) error {
txn, err := db.Begin()
if err != nil {
return err
}
d := conf.Driver.Dialect
d := GetDialect()
if _, err := txn.Exec(d.createVersionTableSql()); err != nil {
txn.Rollback()
@ -267,15 +249,8 @@ func createVersionTable(conf *DBConf, db *sql.DB) error {
// wrapper for EnsureDBVersion for callers that don't already have
// their own DB instance
func GetDBVersion(conf *DBConf) (version int64, err error) {
db, err := OpenDBFromDBConf(conf)
if err != nil {
return -1, err
}
defer db.Close()
version, err = EnsureDBVersion(conf, db)
func GetDBVersion(db *sql.DB) (int64, error) {
version, err := EnsureDBVersion(db)
if err != nil {
return -1, err
}
@ -357,13 +332,7 @@ func CreateMigration(name, migrationType, dir string, t time.Time) (path string,
filename := fmt.Sprintf("%v_%v.%v", timestamp, name, migrationType)
fpath := filepath.Join(dir, filename)
var tmpl *template.Template
if migrationType == "sql" {
tmpl = sqlMigrationTemplate
} else {
tmpl = goMigrationTemplate
}
tmpl := sqlMigrationTemplate
path, err = writeTemplateToFile(fpath, tmpl, timestamp)
@ -372,10 +341,10 @@ func CreateMigration(name, migrationType, dir string, t time.Time) (path string,
// Update the version table for the given migration,
// and finalize the transaction.
func FinalizeMigration(conf *DBConf, txn *sql.Tx, direction bool, v int64) error {
func FinalizeMigration(txn *sql.Tx, direction bool, v int64) error {
// XXX: drop goose_db_version table on some minimum version number?
stmt := conf.Driver.Dialect.insertVersionSql()
stmt := GetDialect().insertVersionSql()
if _, err := txn.Exec(stmt, v, direction); err != nil {
txn.Rollback()
return err
@ -384,24 +353,6 @@ func FinalizeMigration(conf *DBConf, txn *sql.Tx, direction bool, v int64) error
return txn.Commit()
}
var goMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`
package main
import (
"database/sql"
)
// Up is executed when this migration is applied
func Up_{{ . }}(txn *sql.Tx) {
}
// Down is executed when this migration is rolled back
func Down_{{ . }}(txn *sql.Tx) {
}
`))
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied

View File

@ -135,7 +135,7 @@ func splitSQLStatements(r io.Reader, direction bool) (stmts []string) {
//
// All statements following an Up or Down directive are grouped together
// until another direction directive is found.
func runSQLMigration(conf *DBConf, db *sql.DB, scriptFile string, v int64, direction bool) error {
func runSQLMigration(db *sql.DB, scriptFile string, v int64, direction bool) error {
txn, err := db.Begin()
if err != nil {
@ -160,7 +160,7 @@ func runSQLMigration(conf *DBConf, db *sql.DB, scriptFile string, v int64, direc
}
}
if err = FinalizeMigration(conf, txn, direction, v); err != nil {
if err = FinalizeMigration(txn, direction, v); err != nil {
log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(scriptFile), err)
}

27
redo.go Normal file
View File

@ -0,0 +1,27 @@
package goose
import (
"database/sql"
)
func Redo(db *sql.DB, dir string) error {
current, err := GetDBVersion(db)
if err != nil {
return err
}
previous, err := GetPreviousDBVersion(dir, current)
if err != nil {
return err
}
if err := RunMigrations(db, dir, previous); err != nil {
return err
}
if err := RunMigrations(db, dir, current); err != nil {
return err
}
return nil
}

View File

@ -1,7 +1,6 @@
package main
package goose
import (
"bitbucket.org/liamstask/goose/lib/goose"
"database/sql"
"fmt"
"log"
@ -9,55 +8,32 @@ import (
"time"
)
var statusCmd = &Command{
Name: "status",
Usage: "",
Summary: "dump the migration status for the current DB",
Help: `status extended help here...`,
Run: statusRun,
}
type StatusData struct {
Source string
Status string
}
func statusRun(cmd *Command, args ...string) {
conf, err := dbConfFromFlags()
if err != nil {
log.Fatal(err)
}
func Status(db *sql.DB, dir string) error {
// collect all migrations
min := int64(0)
max := int64((1 << 63) - 1)
migrations, e := goose.CollectMigrations(conf.MigrationsDir, min, max)
if e != nil {
log.Fatal(e)
migrations, err := CollectMigrations(dir, min, max)
if err != nil {
return err
}
db, e := goose.OpenDBFromDBConf(conf)
if e != nil {
log.Fatal("couldn't open DB:", e)
}
defer db.Close()
// must ensure that the version table exists if we're running on a pristine DB
if _, e := goose.EnsureDBVersion(conf, db); e != nil {
log.Fatal(e)
if _, err := EnsureDBVersion(db); err != nil {
return err
}
fmt.Printf("goose: status for environment '%v'\n", conf.Env)
fmt.Println("goose: status")
fmt.Println(" Applied At Migration")
fmt.Println(" =======================================")
for _, m := range migrations {
printMigrationStatus(db, m.Version, filepath.Base(m.Source))
}
return nil
}
func printMigrationStatus(db *sql.DB, version int64, script string) {
var row goose.MigrationRecord
var row MigrationRecord
q := fmt.Sprintf("SELECT tstamp, is_applied FROM goose_db_version WHERE version_id=%d ORDER BY tstamp DESC LIMIT 1", version)
e := db.QueryRow(q).Scan(&row.TStamp, &row.IsApplied)

17
up.go Normal file
View File

@ -0,0 +1,17 @@
package goose
import (
"database/sql"
)
func Up(db *sql.DB, dir string) error {
target, err := GetMostRecentDBVersion(dir)
if err != nil {
return err
}
if err := RunMigrations(db, dir, target); err != nil {
return err
}
return nil
}

16
version.go Normal file
View File

@ -0,0 +1,16 @@
package goose
import (
"database/sql"
"fmt"
)
func Version(db *sql.DB, dir string) error {
current, err := GetDBVersion(db)
if err != nil {
return err
}
fmt.Printf("goose: dbversion %v\n", current)
return nil
}