go migrations: much simpler and more robust way to execute migrations. go migrations are all 'package main', and the version is simply appended to the function name in order to distinguish potentially several migrations in the same 'package main'

pull/2/head
Liam Staskawicz 2013-01-06 22:47:45 -08:00
parent 85a0c8b565
commit f89880da03
5 changed files with 48 additions and 118 deletions

View File

@ -64,20 +64,19 @@ func init() {
}
var goMigrationScaffoldTmpl = template.Must(template.New("driver").Parse(`
package migration_{{ . }}
package main
import (
"database/sql"
"fmt"
)
// Up is executed when this migration is applied
func Up(txn *sql.Tx) {
func Up_{{ . }}(txn *sql.Tx) {
}
// Down is executed when this migration is rolled back
func Down(txn *sql.Tx) {
func Down_{{ . }}(txn *sql.Tx) {
}
`))

View File

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

View File

@ -0,0 +1,15 @@
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

@ -1,37 +1,21 @@
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
const (
UpDecl = "func Up(txn *sql.Tx) {"
UpDeclFmt = "func migration_%d_Up(txn *sql.Tx) {\n"
DownDecl = "func Down(txn *sql.Tx) {"
DownDeclFmt = "func migration_%d_Down(txn *sql.Tx) {\n"
)
type TemplateData struct {
Version int64
DBDriver string
DBOpen string
Direction string
}
func directionStr(direction bool) string {
if direction {
return "Up"
}
return "Down"
Direction bool
Func string
}
//
@ -50,11 +34,17 @@ func runGoMigration(conf *DBConf, path string, version int64, direction bool) er
}
defer os.RemoveAll(d)
directionStr := "Down"
if direction {
directionStr = "Up"
}
td := &TemplateData{
Version: version,
DBDriver: conf.Driver,
DBOpen: conf.OpenStr,
Direction: directionStr(direction),
Direction: direction,
Func: fmt.Sprintf("%v_%v", directionStr, version),
}
main, e := writeTemplateToFile(filepath.Join(d, "goose_main.go"), goMigrationTmpl, td)
if e != nil {
@ -62,7 +52,7 @@ func runGoMigration(conf *DBConf, path string, version int64, direction bool) er
}
outpath := filepath.Join(d, filepath.Base(path))
if e = writeSubstituted(path, outpath, version); e != nil {
if _, e = copyFile(outpath, path); e != nil {
log.Fatal(e)
}
@ -76,82 +66,6 @@ func runGoMigration(conf *DBConf, path string, version int64, direction bool) er
return nil
}
//
// a little cheesy, but do a simple text substitution on the contents of the
// migration script. this has 2 motivations:
// * rewrite the package to 'main' so that we can run as part of `go run`
// * namespace the Up() and Down() funcs so we can compile several
// .go migrations into the same binary for execution
//
func writeSubstituted(inpath, outpath string, version int64) error {
fin, e := os.Open(inpath)
if e != nil {
return e
}
defer fin.Close()
fout, e := os.OpenFile(outpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if e != nil {
return e
}
defer fout.Close()
rw := bufio.NewReadWriter(bufio.NewReader(fin), bufio.NewWriter(fout))
for {
// XXX: could optimize the case in which we've already found
// everything we're looking for, and just copy the rest in bulk
line, _, e := rw.ReadLine()
// XXX: handle isPrefix from ReadLine()
if e != nil {
if e != io.EOF {
log.Fatal("failed to read from migration script:", e)
}
rw.Flush()
break
}
lineStr := string(line)
if strings.HasPrefix(lineStr, "package migration_") {
if _, e := rw.WriteString("package main\n"); e != nil {
return e
}
continue
}
if lineStr == UpDecl {
up := fmt.Sprintf(UpDeclFmt, version)
if _, e := rw.WriteString(up); e != nil {
return e
}
continue
}
if lineStr == DownDecl {
down := fmt.Sprintf(DownDeclFmt, version)
if _, e := rw.WriteString(down); e != nil {
return e
}
continue
}
// default case
if _, e := rw.Write(line); e != nil {
return e
}
if _, e := rw.WriteRune('\n'); e != nil {
return 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
@ -179,11 +93,11 @@ func main() {
log.Fatal("db.Begin:", err)
}
migration_{{ .Version }}_{{ .Direction }}(txn)
{{ .Func }}(txn)
// XXX: drop goose_db_version table on some minimum version number?
isApplied := "{{ .Direction }}" == "Up"
versionStmt := fmt.Sprintf("INSERT INTO goose_db_version (version_id, is_applied) VALUES (%d, %t);", {{ .Version }}, isApplied)
versionFmt := "INSERT INTO goose_db_version (version_id, is_applied) VALUES (%v, %t);"
versionStmt := fmt.Sprintf(versionFmt, int64({{ .Version }}), {{ .Direction }})
if _, err = txn.Exec(versionStmt); err != nil {
txn.Rollback()
log.Fatal("failed to write version: ", err)

17
util.go
View File

@ -1,6 +1,7 @@
package main
import (
"io"
"os"
"text/template"
)
@ -21,3 +22,19 @@ func writeTemplateToFile(path string, t *template.Template, data interface{}) (s
return f.Name(), nil
}
func copyFile(dst, src string) (int64, error) {
sf, err := os.Open(src)
if err != nil {
return 0, err
}
defer sf.Close()
df, err := os.Create(dst)
if err != nil {
return 0, err
}
defer df.Close()
return io.Copy(df, sf)
}