Skip to content
This repository has been archived by the owner on Jun 28, 2018. It is now read-only.

Commit

Permalink
add mysql driver, add ENV to docker containers
Browse files Browse the repository at this point in the history
  • Loading branch information
mattes committed Feb 28, 2017
1 parent 760bc3e commit be1ba92
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 53 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SOURCE ?= file go-bindata github
DATABASE ?= postgres
DATABASE ?= postgres mysql
VERSION ?= $(shell git describe --tags 2>/dev/null | cut -c 2-)
TEST_FLAGS ?=
REPO_OWNER ?= $(shell cd .. && basename "$$(pwd)")
Expand Down
7 changes: 7 additions & 0 deletions cli/build_mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build mysql

package main

import (
_ "github.com/mattes/migrate/database/mysql"
)
272 changes: 272 additions & 0 deletions database/mysql/mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
package mysql

import (
"database/sql"
"fmt"
"io"
"io/ioutil"
nurl "net/url"
"strings"

"github.com/go-sql-driver/mysql"
"github.com/mattes/migrate"
"github.com/mattes/migrate/database"
)

func init() {
database.Register("mysql", &Mysql{})
}

var DefaultMigrationsTable = "schema_migrations"

var (
ErrDatabaseDirty = fmt.Errorf("database is dirty")
ErrNilConfig = fmt.Errorf("no config")
ErrNoDatabaseName = fmt.Errorf("no database name")
)

type Config struct {
MigrationsTable string
DatabaseName string
}

type Mysql struct {
db *sql.DB
isLocked bool

config *Config
}

// instance must have `multiStatements` set to true
func WithInstance(instance *sql.DB, config *Config) (database.Driver, error) {
if config == nil {
return nil, ErrNilConfig
}

if err := instance.Ping(); err != nil {
return nil, err
}

query := `SELECT DATABASE()`
var databaseName sql.NullString
if err := instance.QueryRow(query).Scan(&databaseName); err != nil {
return nil, &database.Error{OrigErr: err, Query: []byte(query)}
}

if len(databaseName.String) == 0 {
return nil, ErrNoDatabaseName
}

config.DatabaseName = databaseName.String

if len(config.MigrationsTable) == 0 {
config.MigrationsTable = DefaultMigrationsTable
}

mx := &Mysql{
db: instance,
config: config,
}

if err := mx.ensureVersionTable(); err != nil {
return nil, err
}

return mx, nil
}

func (m *Mysql) Open(url string) (database.Driver, error) {
purl, err := nurl.Parse(url)
if err != nil {
return nil, err
}

purl.Query().Set("multiStatements", "true")

db, err := sql.Open("mysql", strings.Replace(
migrate.FilterCustomQuery(purl).String(), "mysql://", "", 1))
if err != nil {
return nil, err
}

migrationsTable := purl.Query().Get("x-migrations-table")
if len(migrationsTable) == 0 {
migrationsTable = DefaultMigrationsTable
}

mx, err := WithInstance(db, &Config{
DatabaseName: purl.Path,
MigrationsTable: migrationsTable,
})
if err != nil {
return nil, err
}

return mx, nil
}

func (m *Mysql) Close() error {
return m.db.Close()
}

func (m *Mysql) Lock() error {
if m.isLocked {
return database.ErrLocked
}

aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
if err != nil {
return err
}

query := "SELECT GET_LOCK(?, 1)"
var success bool
if err := m.db.QueryRow(query, aid).Scan(&success); err != nil {
return &database.Error{OrigErr: err, Err: "try lock failed", Query: []byte(query)}
}

if success {
m.isLocked = true
return nil
}

return database.ErrLocked
}

func (m *Mysql) Unlock() error {
if !m.isLocked {
return nil
}

aid, err := database.GenerateAdvisoryLockId(m.config.DatabaseName)
if err != nil {
return err
}

query := `SELECT RELEASE_LOCK(?)`
if _, err := m.db.Exec(query, aid); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}

m.isLocked = false
return nil
}

func (m *Mysql) Run(migration io.Reader) error {
migr, err := ioutil.ReadAll(migration)
if err != nil {
return err
}

query := string(migr[:])
if _, err := m.db.Exec(query); err != nil {
return database.Error{OrigErr: err, Err: "migration failed", Query: migr}
}

return nil
}

func (m *Mysql) SetVersion(version int, dirty bool) error {
tx, err := m.db.Begin()
if err != nil {
return &database.Error{OrigErr: err, Err: "transaction start failed"}
}

query := "TRUNCATE `" + m.config.MigrationsTable + "`"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}

if version >= 0 {
query := "INSERT INTO `" + m.config.MigrationsTable + "` (version, dirty) VALUES (?, ?)"
if _, err := m.db.Exec(query, version, dirty); err != nil {
tx.Rollback()
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}

if err := tx.Commit(); err != nil {
return &database.Error{OrigErr: err, Err: "transaction commit failed"}
}

return nil
}

func (m *Mysql) Version() (version int, dirty bool, err error) {
query := "SELECT version, dirty FROM `" + m.config.MigrationsTable + "` LIMIT 1"
err = m.db.QueryRow(query).Scan(&version, &dirty)
switch {
case err == sql.ErrNoRows:
return database.NilVersion, false, nil

case err != nil:
if e, ok := err.(*mysql.MySQLError); ok {
if e.Number == 0 {
return database.NilVersion, false, nil
}
}
return 0, false, &database.Error{OrigErr: err, Query: []byte(query)}

default:
return version, dirty, nil
}
}

func (m *Mysql) Drop() error {
// select all tables
query := `SHOW TABLES LIKE '%'`
tables, err := m.db.Query(query)
if err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
defer tables.Close()

// delete one table after another
tableNames := make([]string, 0)
for tables.Next() {
var tableName string
if err := tables.Scan(&tableName); err != nil {
return err
}
if len(tableName) > 0 {
tableNames = append(tableNames, tableName)
}
}

if len(tableNames) > 0 {
// delete one by one ...
for _, t := range tableNames {
query = "DROP TABLE IF EXISTS `" + t + "` CASCADE"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}
if err := m.ensureVersionTable(); err != nil {
return err
}
}

return nil
}

func (m *Mysql) ensureVersionTable() error {
// check if migration table exists
var count int
query := `SHOW TABLES WHERE ?`
if err := m.db.QueryRow(query, m.config.MigrationsTable).Scan(&count); err != nil {
if err != sql.ErrNoRows {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
}

if count == 1 {
return nil
}

// if not, create the empty migration table
query = "CREATE TABLE `" + m.config.MigrationsTable + "` (version bigint not null primary key, dirty boolean not null)"
if _, err := m.db.Exec(query); err != nil {
return &database.Error{OrigErr: err, Query: []byte(query)}
}
return nil
}
51 changes: 51 additions & 0 deletions database/mysql/mysql_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package mysql

import (
"database/sql"
sqldriver "database/sql/driver"
"fmt"
// "io/ioutil"
// "log"
"testing"

// "github.com/go-sql-driver/mysql"
dt "github.com/mattes/migrate/database/testing"
mt "github.com/mattes/migrate/testing"
)

var versions = []mt.Version{
{"mysql:8", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.7", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.6", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
{"mysql:5.5", []string{"MYSQL_ROOT_PASSWORD=root", "MYSQL_DATABASE=public"}},
}

func isReady(i mt.Instance) bool {
db, err := sql.Open("mysql", fmt.Sprintf("root:root@tcp(%v:%v)/public", i.Host(), i.Port()))
if err != nil {
return false
}
defer db.Close()
err = db.Ping()

if err == sqldriver.ErrBadConn {
return false
}

return true
}

func Test(t *testing.T) {
// mysql.SetLogger(mysql.Logger(log.New(ioutil.Discard, "", log.Ltime)))

mt.ParallelTest(t, versions, isReady,
func(t *testing.T, i mt.Instance) {
p := &Mysql{}
addr := fmt.Sprintf("mysql://root:root@tcp(%v:%v)/public", i.Host(), i.Port())
d, err := p.Open(addr)
if err != nil {
t.Fatalf("%v", err)
}
dt.Test(t, d, []byte("SELECT 1"))
})
}
Loading

0 comments on commit be1ba92

Please sign in to comment.