add library itself
This commit is contained in:
parent
854f436599
commit
b6684a18ac
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
229
README.md
229
README.md
@ -1 +1,228 @@
|
||||
gorm-goose
|
||||
# gorm-goose
|
||||
|
||||
This is a fork of [https://bitbucket.org/liamstask/goose](https://bitbucket.org/liamstask/goose) for [gorm](https://github.com/jinzhu/gorm).
|
||||
|
||||
gorm-goose is a database migration tool for [gorm](https://github.com/jinzhu/gorm).
|
||||
Currently, available drivers are: "postgres", "mysql", or "sqlite3".
|
||||
|
||||
You can manage your database's evolution by creating incremental SQL or Go scripts.
|
||||
|
||||
# Install
|
||||
|
||||
$ go get github.com/Altoros/gorm-goose/cmd/gorm-goose
|
||||
|
||||
This will install the `gorm-goose` binary to your `$GOPATH/bin` directory.
|
||||
|
||||
You can also build gorm-goose into your own applications by importing `github.com/Altoros/gorm-goose/lib/goose`.
|
||||
|
||||
# Usage
|
||||
|
||||
gorm-goose provides several commands to help manage your database schema.
|
||||
|
||||
## create
|
||||
|
||||
Create a new Go migration.
|
||||
|
||||
$ gorm-goose create AddSomeColumns
|
||||
$ goose: created db/migrations/20130106093224_AddSomeColumns.go
|
||||
|
||||
Edit the newly created script to define the behavior of your migration.
|
||||
|
||||
You can also create an SQL migration:
|
||||
|
||||
$ gorm-goose create AddSomeColumns sql
|
||||
$ goose: created db/migrations/20130106093224_AddSomeColumns.sql
|
||||
|
||||
## up
|
||||
|
||||
Apply all available migrations.
|
||||
|
||||
$ gorm-goose up
|
||||
$ goose: migrating db environment 'development', current version: 0, target: 3
|
||||
$ OK 001_basics.sql
|
||||
$ OK 002_next.sql
|
||||
$ OK 003_and_again.go
|
||||
|
||||
### option: pgschema
|
||||
|
||||
Use the `pgschema` flag with the `up` command specify a postgres schema.
|
||||
|
||||
$ gorm-goose -pgschema=my_schema_name up
|
||||
$ goose: migrating db environment 'development', current version: 0, target: 3
|
||||
$ OK 001_basics.sql
|
||||
$ OK 002_next.sql
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## down
|
||||
|
||||
Roll back a single migration from the current version.
|
||||
|
||||
$ gorm-goose down
|
||||
$ goose: migrating db environment 'development', current version: 3, target: 2
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## redo
|
||||
|
||||
Roll back the most recently applied migration, then run it again.
|
||||
|
||||
$ gorm-goose redo
|
||||
$ goose: migrating db environment 'development', current version: 3, target: 2
|
||||
$ OK 003_and_again.go
|
||||
$ goose: migrating db environment 'development', current version: 2, target: 3
|
||||
$ OK 003_and_again.go
|
||||
|
||||
## status
|
||||
|
||||
Print the status of all migrations:
|
||||
|
||||
$ gorm-goose status
|
||||
$ goose: status for environment 'development'
|
||||
$ Applied At Migration
|
||||
$ =======================================
|
||||
$ Sun Jan 6 11:25:03 2013 -- 001_basics.sql
|
||||
$ Sun Jan 6 11:25:03 2013 -- 002_next.sql
|
||||
$ Pending -- 003_and_again.go
|
||||
|
||||
## dbversion
|
||||
|
||||
Print the current version of the database:
|
||||
|
||||
$ gorm-goose dbversion
|
||||
$ goose: dbversion 002
|
||||
|
||||
|
||||
`gorm-goose -h` provides more detailed info on each command.
|
||||
|
||||
|
||||
# Migrations
|
||||
|
||||
gorm-goose supports migrations written in SQL or in Go - see the `gorm-goose create` command above for details on how to generate them.
|
||||
|
||||
## SQL Migrations
|
||||
|
||||
A sample SQL migration looks like:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
CREATE TABLE post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE post;
|
||||
```
|
||||
|
||||
Notice the annotations in the comments. Any statements following `-- +goose Up` will be executed as part of a forward migration, and any statements following `-- +goose Down` will be executed as part of a rollback.
|
||||
|
||||
By default, SQL statements are delimited by semicolons - in fact, query statements must end with a semicolon to be properly recognized by goose.
|
||||
|
||||
More complex statements (PL/pgSQL) that have semicolons within them must be annotated with `-- +goose StatementBegin` and `-- +goose StatementEnd` to be properly recognized. For example:
|
||||
|
||||
```sql
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
|
||||
returns void AS $$
|
||||
DECLARE
|
||||
create_query text;
|
||||
BEGIN
|
||||
FOR create_query IN SELECT
|
||||
'CREATE TABLE IF NOT EXISTS histories_'
|
||||
|| TO_CHAR( d, 'YYYY_MM' )
|
||||
|| ' ( CHECK( created_at >= timestamp '''
|
||||
|| TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' AND created_at < timestamp '''
|
||||
|| TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' ) ) inherits ( histories );'
|
||||
FROM generate_series( $1, $2, '1 month' ) AS d
|
||||
LOOP
|
||||
EXECUTE create_query;
|
||||
END LOOP; -- LOOP END
|
||||
END; -- FUNCTION END
|
||||
$$
|
||||
language plpgsql;
|
||||
-- +goose StatementEnd
|
||||
```
|
||||
|
||||
## Go Migrations
|
||||
|
||||
A sample Go migration looks like:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func Up_20130106222315(txn *gorm.DB) {
|
||||
fmt.Println("Hello from migration 20130106222315 Up!")
|
||||
}
|
||||
|
||||
func Down_20130106222315(txn *gorm.DB) {
|
||||
fmt.Println("Hello from migration 20130106222315 Down!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
`Up_20130106222315()` will be executed as part of a forward migration, and `Down_20130106222315()` will be executed as part of a rollback.
|
||||
|
||||
The numeric portion of the function name (`20130106222315`) must be the leading portion of migration's filename, such as `20130106222315_descriptive_name.go`. `gorm-goose create` does this by default.
|
||||
|
||||
A transaction is provided, rather than the DB instance directly, since gorm-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.
|
||||
|
||||
|
||||
# Configuration
|
||||
|
||||
gorm-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. gorm-goose defaults to using an environment called `development`.
|
||||
|
||||
gorm-goose will expand environment variables in the `open` element. For an example, see the Heroku section below.
|
||||
|
||||
## 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 _ "github.com/Altoros/gorm-goose/cmd/gorm-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 gorm-goose in production, use `heroku run`:
|
||||
|
||||
heroku run gorm-goose -env production up
|
||||
|
27
cmd/gorm-goose/cmd.go
Normal file
27
cmd/gorm-goose/cmd.go
Normal file
@ -0,0 +1,27 @@
|
||||
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()...)
|
||||
}
|
52
cmd/gorm-goose/cmd_create.go
Normal file
52
cmd/gorm-goose/cmd_create.go
Normal file
@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
30
cmd/gorm-goose/cmd_dbversion.go
Normal file
30
cmd/gorm-goose/cmd_dbversion.go
Normal file
@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
37
cmd/gorm-goose/cmd_down.go
Normal file
37
cmd/gorm-goose/cmd_down.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
40
cmd/gorm-goose/cmd_redo.go
Normal file
40
cmd/gorm-goose/cmd_redo.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
77
cmd/gorm-goose/cmd_status.go
Normal file
77
cmd/gorm-goose/cmd_status.go
Normal file
@ -0,0 +1,77 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Printf("goose: status for environment '%v'\n", conf.Env)
|
||||
fmt.Println(" Applied At Migration")
|
||||
fmt.Println(" =======================================")
|
||||
for _, m := range migrations {
|
||||
printMigrationStatus(db, m.Version, filepath.Base(m.Source))
|
||||
}
|
||||
}
|
||||
|
||||
func printMigrationStatus(db *gorm.DB, version int64, script string) {
|
||||
row := goose.MigrationRecord{}
|
||||
result := db.Where("version_id = ?", version).Order("t_stamp desc").First(&row)
|
||||
|
||||
if result.Error != nil && !result.RecordNotFound() {
|
||||
log.Fatal(result.Error)
|
||||
}
|
||||
|
||||
var appliedAt string
|
||||
|
||||
if row.IsApplied {
|
||||
appliedAt = row.TStamp.Format(time.ANSIC)
|
||||
} else {
|
||||
appliedAt = "Pending"
|
||||
}
|
||||
|
||||
fmt.Printf(" %-24s -- %v\n", appliedAt, script)
|
||||
}
|
32
cmd/gorm-goose/cmd_up.go
Normal file
32
cmd/gorm-goose/cmd_up.go
Normal file
@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
79
cmd/gorm-goose/main.go
Normal file
79
cmd/gorm-goose/main.go
Normal file
@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Altoros/gorm-goose/lib/goose"
|
||||
)
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if len(args) == 0 || args[0] == "-h" {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *Command
|
||||
name := args[0]
|
||||
for _, c := range commands {
|
||||
if strings.HasPrefix(c.Name, name) {
|
||||
cmd = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cmd == nil {
|
||||
fmt.Printf("error: unknown command %q\n", name)
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd.Exec(args[1:])
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Print(usagePrefix)
|
||||
flag.PrintDefaults()
|
||||
usageTmpl.Execute(os.Stdout, commands)
|
||||
}
|
||||
|
||||
var usagePrefix = `
|
||||
gorm-goose is a database migration management system for Go projects.
|
||||
|
||||
Usage:
|
||||
gorm-goose [options] <subcommand> [subcommand options]
|
||||
|
||||
Options:
|
||||
`
|
||||
var usageTmpl = template.Must(template.New("usage").Parse(
|
||||
`
|
||||
Commands:{{range .}}
|
||||
{{.Name | printf "%-10s"}} {{.Summary}}{{end}}
|
||||
`))
|
16
db-sample/dbconf.yml
Normal file
16
db-sample/dbconf.yml
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
test:
|
||||
driver: mysql
|
||||
open: user:password@/dbname
|
||||
|
||||
development:
|
||||
driver: postgres
|
||||
open: host=myhost user=gorm dbname=gorm sslmode=disable password=mypassword
|
||||
|
||||
production:
|
||||
driver: sqlite3
|
||||
open: /tmp/gorm.db
|
||||
|
||||
environment_variable_config:
|
||||
driver: $DB_DRIVER
|
||||
open: $DATABASE_URL
|
11
db-sample/migrations/001_basics.sql
Normal file
11
db-sample/migrations/001_basics.sql
Normal file
@ -0,0 +1,11 @@
|
||||
|
||||
-- +goose Up
|
||||
CREATE TABLE post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE post;
|
12
db-sample/migrations/002_next.sql
Normal file
12
db-sample/migrations/002_next.sql
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
-- +goose Up
|
||||
CREATE TABLE fancier_post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
created_on timestamp without time zone,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE fancier_post;
|
15
db-sample/migrations/20130106222315_and_again.go
Normal file
15
db-sample/migrations/20130106222315_and_again.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func Up_20130106222315(txn *gorm.DB) {
|
||||
fmt.Println("Hello from migration 20130106222315 Up!")
|
||||
}
|
||||
|
||||
func Down_20130106222315(txn *gorm.DB) {
|
||||
fmt.Println("Hello from migration 20130106222315 Down!")
|
||||
}
|
116
lib/goose/dbconf.go
Normal file
116
lib/goose/dbconf.go
Normal file
@ -0,0 +1,116 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/kylelemons/go-gypsy/yaml"
|
||||
)
|
||||
|
||||
// DBDriver encapsulates the info needed to work with
|
||||
// a specific database driver
|
||||
type DBDriver struct {
|
||||
Name string
|
||||
OpenStr string
|
||||
Import string
|
||||
}
|
||||
|
||||
type DBConf struct {
|
||||
MigrationsDir string
|
||||
Env string
|
||||
Driver DBDriver
|
||||
PgSchema string
|
||||
}
|
||||
|
||||
// NewDBConf 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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !d.IsValid() {
|
||||
return nil, fmt.Errorf("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/jinzhu/gorm/dialects/postgres"
|
||||
|
||||
case "mysql":
|
||||
d.Import = "github.com/jinzhu/gorm/dialects/mysql"
|
||||
d.OpenStr = d.OpenStr + "?charset=utf8&parseTime=True&loc=Local"
|
||||
|
||||
case "sqlite3":
|
||||
d.Import = "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// IsValid ensure we have enough info about this driver
|
||||
func (drv *DBDriver) IsValid() bool {
|
||||
return len(drv.Import) > 0
|
||||
}
|
||||
|
||||
// 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) (*gorm.DB, error) {
|
||||
db, err := gorm.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).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
69
lib/goose/dbconf_test.go
Normal file
69
lib/goose/dbconf_test.go
Normal file
@ -0,0 +1,69 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"os"
|
||||
"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 := dbconf.Driver.Name
|
||||
want := "sqlite3"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
402
lib/goose/migrate.go
Normal file
402
lib/goose/migrate.go
Normal file
@ -0,0 +1,402 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
||||
_ "github.com/jinzhu/gorm/dialects/postgres"
|
||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTableDoesNotExist = errors.New("table does not exist")
|
||||
ErrNoPreviousVersion = errors.New("no previous version found")
|
||||
)
|
||||
|
||||
type MigrationRecord struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
VersionId int64
|
||||
TStamp time.Time `gorm:"default: now()"`
|
||||
IsApplied bool // was this a result of up() or down()
|
||||
}
|
||||
|
||||
type Migration struct {
|
||||
Version int64
|
||||
Next int64 // next version, or -1 if none
|
||||
Previous int64 // previous version, -1 if none
|
||||
Source string // path to .go or .sql script
|
||||
}
|
||||
|
||||
type migrationSorter []*Migration
|
||||
|
||||
// helpers so we can use pkg sort
|
||||
func (ms migrationSorter) Len() int { return len(ms) }
|
||||
func (ms migrationSorter) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
|
||||
func (ms migrationSorter) Less(i, j int) bool { return ms[i].Version < ms[j].Version }
|
||||
|
||||
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 *gorm.DB) (err error) {
|
||||
current, err := EnsureDBVersion(conf, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migrations, err := CollectMigrations(migrationsDir, current, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(migrations) == 0 {
|
||||
fmt.Printf("goose: no migrations to run. current version: %d\n", current)
|
||||
return nil
|
||||
}
|
||||
|
||||
ms := migrationSorter(migrations)
|
||||
direction := current < target
|
||||
ms.Sort(direction)
|
||||
|
||||
fmt.Printf("goose: migrating db environment '%v', current version: %d, target: %d\n",
|
||||
conf.Env, 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)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("FAIL %v, quitting migration", err))
|
||||
}
|
||||
|
||||
fmt.Println("OK ", filepath.Base(m.Source))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// collect all the valid looking migration scripts in the
|
||||
// migrations folder, and key them by version
|
||||
func CollectMigrations(dirpath string, current, target int64) (m []*Migration, err error) {
|
||||
|
||||
// extract the numeric component of each migration,
|
||||
// filter out any uninteresting files,
|
||||
// and ensure we only have one file per migration version.
|
||||
filepath.Walk(dirpath, func(name string, info os.FileInfo, err error) error {
|
||||
|
||||
if v, e := NumericComponent(name); e == nil {
|
||||
|
||||
for _, g := range m {
|
||||
if v == g.Version {
|
||||
log.Fatalf("more than one file specifies the migration for version %d (%s and %s)",
|
||||
v, g.Source, filepath.Join(dirpath, name))
|
||||
}
|
||||
}
|
||||
|
||||
if versionFilter(v, current, target) {
|
||||
m = append(m, newMigration(v, name))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func versionFilter(v, current, target int64) bool {
|
||||
|
||||
if target > current {
|
||||
return v > current && v <= target
|
||||
}
|
||||
|
||||
if target < current {
|
||||
return v <= current && v > target
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ms migrationSorter) Sort(direction bool) {
|
||||
|
||||
// sort ascending or descending by version
|
||||
if direction {
|
||||
sort.Sort(ms)
|
||||
} else {
|
||||
sort.Sort(sort.Reverse(ms))
|
||||
}
|
||||
|
||||
// now that we're sorted in the appropriate direction,
|
||||
// populate next and previous for each migration
|
||||
for i, m := range ms {
|
||||
prev := int64(-1)
|
||||
if i > 0 {
|
||||
prev = ms[i-1].Version
|
||||
ms[i-1].Next = m.Version
|
||||
}
|
||||
ms[i].Previous = prev
|
||||
}
|
||||
}
|
||||
|
||||
// look for migration scripts with names in the form:
|
||||
// XXX_descriptivename.ext
|
||||
// where XXX specifies the version number
|
||||
// and ext specifies the type of migration
|
||||
func NumericComponent(name string) (int64, error) {
|
||||
|
||||
base := filepath.Base(name)
|
||||
|
||||
if ext := filepath.Ext(base); ext != ".go" && ext != ".sql" {
|
||||
return 0, errors.New("not a recognized migration file type")
|
||||
}
|
||||
|
||||
idx := strings.Index(base, "_")
|
||||
if idx < 0 {
|
||||
return 0, errors.New("no separator found")
|
||||
}
|
||||
|
||||
n, e := strconv.ParseInt(base[:idx], 10, 64)
|
||||
if e == nil && n <= 0 {
|
||||
return 0, errors.New("migration IDs must be greater than zero")
|
||||
}
|
||||
|
||||
return n, e
|
||||
}
|
||||
|
||||
// EnsureDBVersion retrieve the current version for this DB.
|
||||
// Create and initialize the DB version table if it doesn't exist.
|
||||
func EnsureDBVersion(conf *DBConf, db *gorm.DB) (int64, error) {
|
||||
rows := []MigrationRecord{}
|
||||
err := db.Order("id desc").Find(&rows).Error
|
||||
|
||||
if err != nil {
|
||||
return 0, createVersionTable(conf, db)
|
||||
}
|
||||
|
||||
// The most recent record for each migration specifies
|
||||
// whether it has been applied or rolled back.
|
||||
// The first version we find that has been applied is the current version.
|
||||
|
||||
toSkip := make([]int64, 0)
|
||||
|
||||
for _, row := range rows {
|
||||
// have we already marked this version to be skipped?
|
||||
skip := false
|
||||
for _, v := range toSkip {
|
||||
if v == row.VersionId {
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
|
||||
// if version has been applied we're done
|
||||
if row.IsApplied {
|
||||
return row.VersionId, nil
|
||||
}
|
||||
|
||||
// latest version of migration has not been applied.
|
||||
toSkip = append(toSkip, row.VersionId)
|
||||
}
|
||||
|
||||
panic("failure in EnsureDBVersion()")
|
||||
}
|
||||
|
||||
// Create the goose_db_version table
|
||||
// and insert the initial 0 value into it
|
||||
func createVersionTable(conf *DBConf, db *gorm.DB) error {
|
||||
txn := db.Begin()
|
||||
if txn.Error != nil {
|
||||
return txn.Error
|
||||
}
|
||||
|
||||
if err := txn.CreateTable(&MigrationRecord{}).Error; err != nil {
|
||||
txn.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
record := MigrationRecord{VersionId: 0, IsApplied: true}
|
||||
if err := txn.Create(&record).Error; err != nil {
|
||||
txn.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return txn.Commit().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)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func GetPreviousDBVersion(dirpath string, version int64) (previous int64, err error) {
|
||||
|
||||
previous = -1
|
||||
sawGivenVersion := false
|
||||
|
||||
filepath.Walk(dirpath, func(name string, info os.FileInfo, walkerr error) error {
|
||||
|
||||
if !info.IsDir() {
|
||||
if v, e := NumericComponent(name); e == nil {
|
||||
if v > previous && v < version {
|
||||
previous = v
|
||||
}
|
||||
if v == version {
|
||||
sawGivenVersion = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if previous == -1 {
|
||||
if sawGivenVersion {
|
||||
// the given version is (likely) valid but we didn't find
|
||||
// anything before it.
|
||||
// 'previous' must reflect that no migrations have been applied.
|
||||
previous = 0
|
||||
} else {
|
||||
err = ErrNoPreviousVersion
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// helper to identify the most recent possible version
|
||||
// within a folder of migration scripts
|
||||
func GetMostRecentDBVersion(dirpath string) (version int64, err error) {
|
||||
|
||||
version = -1
|
||||
|
||||
filepath.Walk(dirpath, func(name string, info os.FileInfo, walkerr error) error {
|
||||
if walkerr != nil {
|
||||
return walkerr
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
if v, e := NumericComponent(name); e == nil {
|
||||
if v > version {
|
||||
version = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if version == -1 {
|
||||
err = errors.New("no valid version found")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func CreateMigration(name, migrationType, dir string, t time.Time) (path string, err error) {
|
||||
|
||||
if migrationType != "go" && migrationType != "sql" {
|
||||
return "", errors.New("migration type must be 'go' or 'sql'")
|
||||
}
|
||||
|
||||
timestamp := t.Format("20060102150405")
|
||||
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
|
||||
}
|
||||
|
||||
path, err = writeTemplateToFile(fpath, tmpl, timestamp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FinalizeMigration update the version table for the given migration,
|
||||
// and finalize the transaction.
|
||||
func FinalizeMigration(conf *DBConf, txn *gorm.DB, direction bool, v int64) error {
|
||||
|
||||
// XXX: drop goose_db_version table on some minimum version number?
|
||||
record := MigrationRecord{VersionId: v, IsApplied: direction}
|
||||
if err := txn.Create(&record).Error; err != nil {
|
||||
txn.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return txn.Commit().Error
|
||||
}
|
||||
|
||||
var goMigrationTemplate = template.Must(template.New("goose.go-migration").Parse(`
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// Up is executed when this migration is applied
|
||||
func Up_{{ . }}(txn *gorm.DB) {
|
||||
|
||||
}
|
||||
|
||||
// Down is executed when this migration is rolled back
|
||||
func Down_{{ . }}(txn *gorm.DB) {
|
||||
|
||||
}
|
||||
`))
|
||||
|
||||
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`
|
||||
-- +goose Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
|
||||
|
||||
-- +goose Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
|
||||
`))
|
71
lib/goose/migrate_test.go
Normal file
71
lib/goose/migrate_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMigrationMapSortUp(t *testing.T) {
|
||||
|
||||
ms := migrationSorter{}
|
||||
|
||||
// insert in any order
|
||||
ms = append(ms, newMigration(20120000, "test"))
|
||||
ms = append(ms, newMigration(20128000, "test"))
|
||||
ms = append(ms, newMigration(20129000, "test"))
|
||||
ms = append(ms, newMigration(20127000, "test"))
|
||||
|
||||
ms.Sort(true) // sort Upwards
|
||||
|
||||
sorted := []int64{20120000, 20127000, 20128000, 20129000}
|
||||
|
||||
validateMigrationSort(t, ms, sorted)
|
||||
}
|
||||
|
||||
func TestMigrationMapSortDown(t *testing.T) {
|
||||
|
||||
ms := migrationSorter{}
|
||||
|
||||
// insert in any order
|
||||
ms = append(ms, newMigration(20120000, "test"))
|
||||
ms = append(ms, newMigration(20128000, "test"))
|
||||
ms = append(ms, newMigration(20129000, "test"))
|
||||
ms = append(ms, newMigration(20127000, "test"))
|
||||
|
||||
ms.Sort(false) // sort Downwards
|
||||
|
||||
sorted := []int64{20129000, 20128000, 20127000, 20120000}
|
||||
|
||||
validateMigrationSort(t, ms, sorted)
|
||||
}
|
||||
|
||||
func validateMigrationSort(t *testing.T, ms migrationSorter, sorted []int64) {
|
||||
|
||||
for i, m := range ms {
|
||||
if sorted[i] != m.Version {
|
||||
t.Error("incorrect sorted version")
|
||||
}
|
||||
|
||||
var next, prev int64
|
||||
|
||||
if i == 0 {
|
||||
prev = -1
|
||||
next = ms[i+1].Version
|
||||
} else if i == len(ms)-1 {
|
||||
prev = ms[i-1].Version
|
||||
next = -1
|
||||
} else {
|
||||
prev = ms[i-1].Version
|
||||
next = ms[i+1].Version
|
||||
}
|
||||
|
||||
if m.Next != next {
|
||||
t.Errorf("mismatched Next. v: %v, got %v, wanted %v\n", m, m.Next, next)
|
||||
}
|
||||
|
||||
if m.Previous != prev {
|
||||
t.Errorf("mismatched Previous v: %v, got %v, wanted %v\n", m, m.Previous, prev)
|
||||
}
|
||||
}
|
||||
|
||||
t.Log(ms)
|
||||
}
|
129
lib/goose/migration_go.go
Normal file
129
lib/goose/migration_go.go
Normal file
@ -0,0 +1,129 @@
|
||||
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
|
||||
}
|
||||
|
||||
//
|
||||
// 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),
|
||||
}
|
||||
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}}"
|
||||
"github.com/Altoros/gorm-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 := db.Begin()
|
||||
if txn.Error != nil {
|
||||
log.Fatal("db.Begin:", txn.Error)
|
||||
}
|
||||
|
||||
{{ .Func }}(txn)
|
||||
|
||||
err = goose.FinalizeMigration(&conf, txn, {{ .Direction }}, {{ .Version }})
|
||||
if err != nil {
|
||||
log.Fatal("Commit() failed:", err)
|
||||
}
|
||||
}
|
||||
`))
|
168
lib/goose/migration_sql.go
Normal file
168
lib/goose/migration_sql.go
Normal file
@ -0,0 +1,168 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
const sqlCmdPrefix = "-- +goose "
|
||||
|
||||
// Checks the line to see if the line has a statement-ending semicolon
|
||||
// or if the line contains a double-dash comment.
|
||||
func endsWithSemicolon(line string) bool {
|
||||
|
||||
prev := ""
|
||||
scanner := bufio.NewScanner(strings.NewReader(line))
|
||||
scanner.Split(bufio.ScanWords)
|
||||
|
||||
for scanner.Scan() {
|
||||
word := scanner.Text()
|
||||
if strings.HasPrefix(word, "--") {
|
||||
break
|
||||
}
|
||||
prev = word
|
||||
}
|
||||
|
||||
return strings.HasSuffix(prev, ";")
|
||||
}
|
||||
|
||||
// Split the given sql script into individual statements.
|
||||
//
|
||||
// The base case is to simply split on semicolons, as these
|
||||
// naturally terminate a statement.
|
||||
//
|
||||
// However, more complex cases like pl/pgsql can have semicolons
|
||||
// within a statement. For these cases, we provide the explicit annotations
|
||||
// 'StatementBegin' and 'StatementEnd' to allow the script to
|
||||
// tell us to ignore semicolons.
|
||||
func splitSQLStatements(r io.Reader, direction bool) (stmts []string) {
|
||||
|
||||
var buf bytes.Buffer
|
||||
scanner := bufio.NewScanner(r)
|
||||
|
||||
// track the count of each section
|
||||
// so we can diagnose scripts with no annotations
|
||||
upSections := 0
|
||||
downSections := 0
|
||||
|
||||
statementEnded := false
|
||||
ignoreSemicolons := false
|
||||
directionIsActive := false
|
||||
|
||||
for scanner.Scan() {
|
||||
|
||||
line := scanner.Text()
|
||||
|
||||
// handle any goose-specific commands
|
||||
if strings.HasPrefix(line, sqlCmdPrefix) {
|
||||
cmd := strings.TrimSpace(line[len(sqlCmdPrefix):])
|
||||
switch cmd {
|
||||
case "Up":
|
||||
directionIsActive = (direction == true)
|
||||
upSections++
|
||||
break
|
||||
|
||||
case "Down":
|
||||
directionIsActive = (direction == false)
|
||||
downSections++
|
||||
break
|
||||
|
||||
case "StatementBegin":
|
||||
if directionIsActive {
|
||||
ignoreSemicolons = true
|
||||
}
|
||||
break
|
||||
|
||||
case "StatementEnd":
|
||||
if directionIsActive {
|
||||
statementEnded = (ignoreSemicolons == true)
|
||||
ignoreSemicolons = false
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !directionIsActive {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := buf.WriteString(line + "\n"); err != nil {
|
||||
log.Fatalf("io err: %v", err)
|
||||
}
|
||||
|
||||
// Wrap up the two supported cases: 1) basic with semicolon; 2) psql statement
|
||||
// Lines that end with semicolon that are in a statement block
|
||||
// do not conclude statement.
|
||||
if (!ignoreSemicolons && endsWithSemicolon(line)) || statementEnded {
|
||||
statementEnded = false
|
||||
stmts = append(stmts, buf.String())
|
||||
buf.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("scanning migration: %v", err)
|
||||
}
|
||||
|
||||
// diagnose likely migration script errors
|
||||
if ignoreSemicolons {
|
||||
log.Println("WARNING: saw '-- +goose StatementBegin' with no matching '-- +goose StatementEnd'")
|
||||
}
|
||||
|
||||
if bufferRemaining := strings.TrimSpace(buf.String()); len(bufferRemaining) > 0 {
|
||||
log.Printf("WARNING: Unexpected unfinished SQL query: %s. Missing a semicolon?\n", bufferRemaining)
|
||||
}
|
||||
|
||||
if upSections == 0 && downSections == 0 {
|
||||
log.Fatal("ERROR: no Up/Down annotations found, so no statements were executed.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Run a migration specified in raw SQL.
|
||||
//
|
||||
// Sections of the script can be annotated with a special comment,
|
||||
// starting with "-- +goose" to specify whether the section should
|
||||
// be applied during an Up or Down migration
|
||||
//
|
||||
// All statements following an Up or Down directive are grouped together
|
||||
// until another direction directive is found.
|
||||
func runSQLMigration(conf *DBConf, db *gorm.DB, scriptFile string, v int64, direction bool) error {
|
||||
|
||||
txn := db.Begin()
|
||||
if txn.Error != nil {
|
||||
log.Fatal("db.Begin:", txn.Error)
|
||||
}
|
||||
|
||||
f, err := os.Open(scriptFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// find each statement, checking annotations for up/down direction
|
||||
// and execute each of them in the current transaction.
|
||||
// Commits the transaction if successfully applied each statement and
|
||||
// records the version into the version table or returns an error and
|
||||
// rolls back the transaction.
|
||||
for _, query := range splitSQLStatements(f, direction) {
|
||||
if err = txn.Exec(query).Error; err != nil {
|
||||
txn.Rollback()
|
||||
log.Fatalf("FAIL %s (%v), quitting migration.", filepath.Base(scriptFile), err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = FinalizeMigration(conf, txn, direction, v); err != nil {
|
||||
log.Fatalf("error finalizing migration %s, quitting. (%v)", filepath.Base(scriptFile), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
147
lib/goose/migration_sql_test.go
Normal file
147
lib/goose/migration_sql_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSemicolons(t *testing.T) {
|
||||
|
||||
type testData struct {
|
||||
line string
|
||||
result bool
|
||||
}
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
line: "END;",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
line: "END; -- comment",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
line: "END ; -- comment",
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
line: "END -- comment",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
line: "END -- comment ;",
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
line: "END \" ; \" -- comment",
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
r := endsWithSemicolon(test.line)
|
||||
if r != test.result {
|
||||
t.Errorf("incorrect semicolon. got %v, want %v", r, test.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitStatements(t *testing.T) {
|
||||
|
||||
type testData struct {
|
||||
sql string
|
||||
direction bool
|
||||
count int
|
||||
}
|
||||
|
||||
tests := []testData{
|
||||
{
|
||||
sql: functxt,
|
||||
direction: true,
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
sql: functxt,
|
||||
direction: false,
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
sql: multitxt,
|
||||
direction: true,
|
||||
count: 2,
|
||||
},
|
||||
{
|
||||
sql: multitxt,
|
||||
direction: false,
|
||||
count: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
stmts := splitSQLStatements(strings.NewReader(test.sql), test.direction)
|
||||
if len(stmts) != test.count {
|
||||
t.Errorf("incorrect number of stmts. got %v, want %v", len(stmts), test.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var functxt = `-- +goose Up
|
||||
CREATE TABLE IF NOT EXISTS histories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
current_value varchar(2000) NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
-- +goose StatementBegin
|
||||
CREATE OR REPLACE FUNCTION histories_partition_creation( DATE, DATE )
|
||||
returns void AS $$
|
||||
DECLARE
|
||||
create_query text;
|
||||
BEGIN
|
||||
FOR create_query IN SELECT
|
||||
'CREATE TABLE IF NOT EXISTS histories_'
|
||||
|| TO_CHAR( d, 'YYYY_MM' )
|
||||
|| ' ( CHECK( created_at >= timestamp '''
|
||||
|| TO_CHAR( d, 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' AND created_at < timestamp '''
|
||||
|| TO_CHAR( d + INTERVAL '1 month', 'YYYY-MM-DD 00:00:00' )
|
||||
|| ''' ) ) inherits ( histories );'
|
||||
FROM generate_series( $1, $2, '1 month' ) AS d
|
||||
LOOP
|
||||
EXECUTE create_query;
|
||||
END LOOP; -- LOOP END
|
||||
END; -- FUNCTION END
|
||||
$$
|
||||
language plpgsql;
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
drop function histories_partition_creation(DATE, DATE);
|
||||
drop TABLE histories;
|
||||
`
|
||||
|
||||
// test multiple up/down transitions in a single script
|
||||
var multitxt = `-- +goose Up
|
||||
CREATE TABLE post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE post;
|
||||
|
||||
-- +goose Up
|
||||
CREATE TABLE fancier_post (
|
||||
id int NOT NULL,
|
||||
title text,
|
||||
body text,
|
||||
created_on timestamp without time zone,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
-- +goose Down
|
||||
DROP TABLE fancier_post;
|
||||
`
|
40
lib/goose/util.go
Normal file
40
lib/goose/util.go
Normal file
@ -0,0 +1,40 @@
|
||||
package goose
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// common routines
|
||||
|
||||
func writeTemplateToFile(path string, t *template.Template, data interface{}) (string, error) {
|
||||
f, e := os.Create(path)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
e = t.Execute(f, data)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user