2017-05-04 20:58:23 -07:00
|
|
|
package dbmate
|
2015-11-25 10:57:58 -08:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
2016-08-15 22:42:07 -07:00
|
|
|
"sort"
|
2015-11-25 10:57:58 -08:00
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// DefaultMigrationsDir specifies default directory to find migration files
|
|
|
|
|
var DefaultMigrationsDir = "./db/migrations"
|
|
|
|
|
|
|
|
|
|
// DB allows dbmate actions to be performed on a specified database
|
|
|
|
|
type DB struct {
|
|
|
|
|
DatabaseURL *url.URL
|
|
|
|
|
MigrationsDir string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewDB initializes a new dbmate database
|
|
|
|
|
func NewDB(databaseURL *url.URL) *DB {
|
|
|
|
|
return &DB{
|
|
|
|
|
DatabaseURL: databaseURL,
|
|
|
|
|
MigrationsDir: DefaultMigrationsDir,
|
2015-11-27 10:34:42 -08:00
|
|
|
}
|
2017-05-04 20:58:23 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetDriver loads the required database driver
|
|
|
|
|
func (db *DB) GetDriver() (Driver, error) {
|
|
|
|
|
return GetDriver(db.DatabaseURL.Scheme)
|
|
|
|
|
}
|
2015-11-27 10:34:42 -08:00
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Up creates the database (if necessary) and runs migrations
|
|
|
|
|
func (db *DB) Up() error {
|
|
|
|
|
drv, err := db.GetDriver()
|
2015-11-27 10:34:42 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// create database if it does not already exist
|
|
|
|
|
// skip this step if we cannot determine status
|
|
|
|
|
// (e.g. user does not have list database permission)
|
2017-05-04 20:58:23 -07:00
|
|
|
exists, err := drv.DatabaseExists(db.DatabaseURL)
|
2015-11-27 10:34:42 -08:00
|
|
|
if err == nil && !exists {
|
2017-05-04 20:58:23 -07:00
|
|
|
if err := drv.CreateDatabase(db.DatabaseURL); err != nil {
|
2015-11-27 10:34:42 -08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migrate
|
2017-05-04 20:58:23 -07:00
|
|
|
return db.Migrate()
|
2015-11-27 10:34:42 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Create creates the current database
|
|
|
|
|
func (db *DB) Create() error {
|
|
|
|
|
drv, err := db.GetDriver()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
return drv.CreateDatabase(db.DatabaseURL)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Drop drops the current database (if it exists)
|
|
|
|
|
func (db *DB) Drop() error {
|
|
|
|
|
drv, err := db.GetDriver()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
return drv.DropDatabase(db.DatabaseURL)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// New creates a new migration file
|
|
|
|
|
func (db *DB) New(name string) error {
|
2015-11-25 10:57:58 -08:00
|
|
|
// new migration name
|
|
|
|
|
timestamp := time.Now().UTC().Format("20060102150405")
|
|
|
|
|
if name == "" {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("please specify a name for the new migration")
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
|
|
|
|
|
|
|
|
|
// create migrations dir if missing
|
2017-05-04 20:58:23 -07:00
|
|
|
if err := os.MkdirAll(db.MigrationsDir, 0755); err != nil {
|
|
|
|
|
return fmt.Errorf("unable to create directory `%s`", db.MigrationsDir)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check file does not already exist
|
2017-05-04 20:58:23 -07:00
|
|
|
path := filepath.Join(db.MigrationsDir, name)
|
2015-11-25 10:57:58 -08:00
|
|
|
fmt.Printf("Creating migration: %s\n", path)
|
|
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("file already exists")
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write new migration
|
|
|
|
|
file, err := os.Create(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-27 14:27:44 -08:00
|
|
|
defer mustClose(file)
|
2015-11-25 10:57:58 -08:00
|
|
|
_, err = file.WriteString(migrationTemplate)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-12-01 09:43:35 -08:00
|
|
|
func doTransaction(db *sql.DB, txFunc func(Transaction) error) error {
|
2015-11-25 10:57:58 -08:00
|
|
|
tx, err := db.Begin()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := txFunc(tx); err != nil {
|
2015-11-27 14:27:44 -08:00
|
|
|
if err1 := tx.Rollback(); err1 != nil {
|
|
|
|
|
return err1
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tx.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
func (db *DB) openDatabaseForMigration() (Driver, *sql.DB, error) {
|
|
|
|
|
drv, err := db.GetDriver()
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
sqlDB, err := drv.Open(db.DatabaseURL)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
2015-11-25 12:26:57 -08:00
|
|
|
return nil, nil, err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
if err := drv.CreateMigrationsTable(sqlDB); err != nil {
|
|
|
|
|
mustClose(sqlDB)
|
2015-11-25 12:26:57 -08:00
|
|
|
return nil, nil, err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
return drv, sqlDB, nil
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Migrate migrates database to the latest version
|
|
|
|
|
func (db *DB) Migrate() error {
|
2016-08-15 22:42:07 -07:00
|
|
|
re := regexp.MustCompile(`^\d.*\.sql$`)
|
2017-05-04 20:58:23 -07:00
|
|
|
files, err := findMigrationFiles(db.MigrationsDir, re)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
if len(files) == 0 {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("no migration files found")
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
drv, sqlDB, err := db.openDatabaseForMigration()
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
2015-11-25 10:57:58 -08:00
|
|
|
return err
|
|
|
|
|
}
|
2017-05-04 20:58:23 -07:00
|
|
|
defer mustClose(sqlDB)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
applied, err := drv.SelectMigrations(sqlDB, -1)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
for _, filename := range files {
|
2015-11-25 10:57:58 -08:00
|
|
|
ver := migrationVersion(filename)
|
2015-11-28 22:31:55 -07:00
|
|
|
if ok := applied[ver]; ok {
|
2015-11-25 10:57:58 -08:00
|
|
|
// migration already applied
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Applying: %s\n", filename)
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
migration, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// begin transaction
|
2017-05-04 20:58:23 -07:00
|
|
|
err = doTransaction(sqlDB, func(tx Transaction) error {
|
2015-11-25 10:57:58 -08:00
|
|
|
// run actual migration
|
|
|
|
|
if _, err := tx.Exec(migration["up"]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// record migration
|
|
|
|
|
if err := drv.InsertMigration(tx, ver); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
2015-11-27 13:55:14 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2015-11-25 10:57:58 -08:00
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
|
2015-11-25 10:57:58 -08:00
|
|
|
files, err := ioutil.ReadDir(dir)
|
|
|
|
|
if err != nil {
|
2017-05-04 20:58:23 -07:00
|
|
|
return nil, fmt.Errorf("could not find migrations directory `%s`", dir)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
matches := []string{}
|
2015-11-25 10:57:58 -08:00
|
|
|
for _, file := range files {
|
|
|
|
|
if file.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := file.Name()
|
2015-11-25 12:26:57 -08:00
|
|
|
if !re.MatchString(name) {
|
2015-11-25 10:57:58 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
matches = append(matches, name)
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
sort.Strings(matches)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
return matches, nil
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
func findMigrationFile(dir string, ver string) (string, error) {
|
|
|
|
|
if ver == "" {
|
|
|
|
|
panic("migration version is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ver = regexp.QuoteMeta(ver)
|
|
|
|
|
re := regexp.MustCompile(fmt.Sprintf(`^%s.*\.sql$`, ver))
|
|
|
|
|
|
|
|
|
|
files, err := findMigrationFiles(dir, re)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(files) == 0 {
|
2017-05-04 20:58:23 -07:00
|
|
|
return "", fmt.Errorf("can't find migration file: %s*.sql", ver)
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return files[0], nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
func migrationVersion(filename string) string {
|
|
|
|
|
return regexp.MustCompile(`^\d+`).FindString(filename)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parseMigration reads a migration file into a map with up/down keys
|
|
|
|
|
// implementation is similar to regexp.Split()
|
|
|
|
|
func parseMigration(path string) (map[string]string, error) {
|
|
|
|
|
// read migration file into string
|
|
|
|
|
data, err := ioutil.ReadFile(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
contents := string(data)
|
|
|
|
|
|
|
|
|
|
// split string on our trigger comment
|
|
|
|
|
separatorRegexp := regexp.MustCompile(`(?m)^-- migrate:(.*)$`)
|
|
|
|
|
matches := separatorRegexp.FindAllStringSubmatchIndex(contents, -1)
|
|
|
|
|
|
|
|
|
|
migrations := map[string]string{}
|
|
|
|
|
direction := ""
|
|
|
|
|
beg := 0
|
|
|
|
|
end := 0
|
|
|
|
|
|
|
|
|
|
for _, match := range matches {
|
|
|
|
|
end = match[0]
|
|
|
|
|
if direction != "" {
|
|
|
|
|
// write previous direction to output map
|
|
|
|
|
migrations[direction] = contents[beg:end]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// each match records the start of a new direction
|
|
|
|
|
direction = contents[match[2]:match[3]]
|
|
|
|
|
beg = match[1]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write final direction to output map
|
|
|
|
|
migrations[direction] = contents[beg:]
|
|
|
|
|
|
|
|
|
|
return migrations, nil
|
|
|
|
|
}
|
2015-11-25 12:26:57 -08:00
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Rollback rolls back the most recent migration
|
|
|
|
|
func (db *DB) Rollback() error {
|
|
|
|
|
drv, sqlDB, err := db.openDatabaseForMigration()
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2017-05-04 20:58:23 -07:00
|
|
|
defer mustClose(sqlDB)
|
2015-11-25 12:26:57 -08:00
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
applied, err := drv.SelectMigrations(sqlDB, 1)
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// grab most recent applied migration (applied has len=1)
|
|
|
|
|
latest := ""
|
|
|
|
|
for ver := range applied {
|
|
|
|
|
latest = ver
|
|
|
|
|
}
|
|
|
|
|
if latest == "" {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("can't rollback: no migrations have been applied")
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
filename, err := findMigrationFile(db.MigrationsDir, latest)
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Rolling back: %s\n", filename)
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
migration, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// begin transaction
|
2017-05-04 20:58:23 -07:00
|
|
|
err = doTransaction(sqlDB, func(tx Transaction) error {
|
2015-11-25 12:26:57 -08:00
|
|
|
// rollback migration
|
|
|
|
|
if _, err := tx.Exec(migration["down"]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// remove migration record
|
|
|
|
|
if err := drv.DeleteMigration(tx, latest); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
2015-11-27 13:55:14 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2015-11-25 12:26:57 -08:00
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|