Merge pull request #154 from pressly/34

Improve migration templates
pull/156/head
Vojtech Vitek 2019-03-05 22:26:26 -05:00 committed by GitHub
commit 346605b722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 150 additions and 38 deletions

View File

@ -1,7 +1,7 @@
sudo: false
language: go
go:
- 1.8
- 1.12
install:
- go get github.com/golang/dep/cmd/dep

View File

@ -7,30 +7,48 @@ import (
"path/filepath"
"text/template"
"time"
"github.com/pkg/errors"
)
type tmplVars struct {
Version string
CamelName string
}
// Create writes a new blank migration file.
func CreateWithTemplate(db *sql.DB, dir string, migrationTemplate *template.Template, name, migrationType string) error {
func CreateWithTemplate(db *sql.DB, dir string, tmpl *template.Template, name, migrationType string) error {
version := time.Now().Format(timestampFormat)
filename := fmt.Sprintf("%v_%v.%v", version, name, migrationType)
filename := fmt.Sprintf("%v_%v.%v", version, snakeCase(name), migrationType)
fpath := filepath.Join(dir, filename)
tmpl := sqlMigrationTemplate
if migrationType == "go" {
tmpl = goSQLMigrationTemplate
if tmpl == nil {
if migrationType == "go" {
tmpl = goSQLMigrationTemplate
} else {
tmpl = sqlMigrationTemplate
}
}
if migrationTemplate != nil {
tmpl = migrationTemplate
path := filepath.Join(dir, filename)
if _, err := os.Stat(path); !os.IsNotExist(err) {
return errors.Wrap(err, "failed to create migration file")
}
path, err := writeTemplateToFile(fpath, tmpl, version)
f, err := os.Create(path)
if err != nil {
return err
return errors.Wrap(err, "failed to create migration file")
}
defer f.Close()
vars := tmplVars{
Version: version,
CamelName: camelCase(name),
}
if err := tmpl.Execute(f, vars); err != nil {
return errors.Wrap(err, "failed to execute tmpl")
}
log.Printf("Created new file: %s\n", path)
log.Printf("Created new file: %s\n", f.Name())
return nil
}
@ -39,33 +57,18 @@ func Create(db *sql.DB, dir, name, migrationType string) error {
return CreateWithTemplate(db, dir, nil, name, migrationType)
}
func writeTemplateToFile(path string, t *template.Template, version string) (string, error) {
if _, err := os.Stat(path); !os.IsNotExist(err) {
return "", fmt.Errorf("failed to create file: %v already exists", path)
}
f, err := os.Create(path)
if err != nil {
return "", err
}
defer f.Close()
err = t.Execute(f, version)
if err != nil {
return "", err
}
return f.Name(), nil
}
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`-- +goose Up
-- SQL in this section is executed when the migration is applied.
-- +goose StatementBegin
SELECT 'up SQL query';
-- +goose StatementEnd
-- +goose Down
-- SQL in this section is executed when the migration is rolled back.
-- +goose StatementBegin
SELECT 'down SQL query';
-- +goose StatementEnd
`))
var goSQLMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`package migration
var goSQLMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`package migrations
import (
"database/sql"
@ -73,15 +76,15 @@ import (
)
func init() {
goose.AddMigration(Up{{.}}, Down{{.}})
goose.AddMigration(up{{.CamelName}}, down{{.CamelName}})
}
func Up{{.}}(tx *sql.Tx) error {
func up{{.CamelName}}(tx *sql.Tx) error {
// This code is executed when the migration is applied.
return nil
}
func Down{{.}}(tx *sql.Tx) error {
func down{{.CamelName}}(tx *sql.Tx) error {
// This code is executed when the migration is rolled back.
return nil
}

84
helpers.go Normal file
View File

@ -0,0 +1,84 @@
package goose
import (
"bytes"
"strings"
"unicode"
"unicode/utf8"
)
type camelSnakeStateMachine int
const ( // _$$_This is some text, OK?!
idle camelSnakeStateMachine = iota // 0 ↑ ↑ ↑
firstAlphaNum // 1 ↑ ↑ ↑ ↑ ↑
alphaNum // 2 ↑↑↑ ↑ ↑↑↑ ↑↑↑ ↑
delimiter // 3 ↑ ↑ ↑ ↑ ↑
)
func (s camelSnakeStateMachine) next(r rune) camelSnakeStateMachine {
switch s {
case idle:
if isAlphaNum(r) {
return firstAlphaNum
}
case firstAlphaNum:
if isAlphaNum(r) {
return alphaNum
}
return delimiter
case alphaNum:
if !isAlphaNum(r) {
return delimiter
}
case delimiter:
if isAlphaNum(r) {
return firstAlphaNum
}
return idle
}
return s
}
func camelCase(str string) string {
var b strings.Builder
stateMachine := idle
for i := 0; i < len(str); {
r, size := utf8.DecodeRuneInString(str[i:])
i += size
stateMachine = stateMachine.next(r)
switch stateMachine {
case firstAlphaNum:
b.WriteRune(unicode.ToUpper(r))
case alphaNum:
b.WriteRune(unicode.ToLower(r))
}
}
return b.String()
}
func snakeCase(str string) string {
var b bytes.Buffer
stateMachine := idle
for i := 0; i < len(str); {
r, size := utf8.DecodeRuneInString(str[i:])
i += size
stateMachine = stateMachine.next(r)
switch stateMachine {
case firstAlphaNum, alphaNum:
b.WriteRune(unicode.ToLower(r))
case delimiter:
b.WriteByte('_')
}
}
if stateMachine == idle {
return string(bytes.TrimSuffix(b.Bytes(), []byte{'_'}))
}
return b.String()
}
func isAlphaNum(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r)
}

25
helpers_test.go Normal file
View File

@ -0,0 +1,25 @@
package goose
import (
"testing"
)
func TestCamelSnake(t *testing.T) {
tt := []struct {
in string
camel string
snake string
}{
{in: "Add updated_at to users table", camel: "AddUpdatedAtToUsersTable", snake: "add_updated_at_to_users_table"},
{in: "$()&^%(_--crazy__--input$)", camel: "CrazyInput", snake: "crazy_input"},
}
for _, test := range tt {
if got := camelCase(test.in); got != test.camel {
t.Errorf("unexpected CamelCase for input(%q), got %q, want %q", test.in, got, test.camel)
}
if got := snakeCase(test.in); got != test.snake {
t.Errorf("unexpected snake_case for input(%q), got %q, want %q", test.in, got, test.snake)
}
}
}