2014-09-16 21:44:34 +02:00
// Package mysql implements the Driver interface.
package mysql
2014-09-16 18:04:50 +02:00
import (
2014-09-16 21:44:34 +02:00
"bufio"
"bytes"
2014-09-16 18:04:50 +02:00
"database/sql"
"errors"
"fmt"
2014-09-16 21:44:34 +02:00
"regexp"
2014-09-16 18:04:50 +02:00
"strconv"
2014-10-11 01:37:00 +02:00
"strings"
2015-10-22 16:29:26 -04:00
"github.com/go-sql-driver/mysql"
"github.com/mattes/migrate/driver"
"github.com/mattes/migrate/file"
"github.com/mattes/migrate/migrate/direction"
2014-09-16 18:04:50 +02:00
)
type Driver struct {
db * sql . DB
}
const tableName = "schema_migrations"
func ( driver * Driver ) Initialize ( url string ) error {
2014-10-17 11:34:43 -07:00
urlWithoutScheme := strings . SplitN ( url , "mysql://" , 2 )
if len ( urlWithoutScheme ) != 2 {
return errors . New ( "invalid mysql:// scheme" )
}
db , err := sql . Open ( "mysql" , urlWithoutScheme [ 1 ] )
2014-09-16 18:04:50 +02:00
if err != nil {
return err
}
if err := db . Ping ( ) ; err != nil {
return err
}
driver . db = db
if err := driver . ensureVersionTableExists ( ) ; err != nil {
return err
}
return nil
}
func ( driver * Driver ) Close ( ) error {
if err := driver . db . Close ( ) ; err != nil {
return err
}
return nil
}
func ( driver * Driver ) ensureVersionTableExists ( ) error {
2014-12-04 13:49:26 -08:00
_ , err := driver . db . Exec ( "CREATE TABLE IF NOT EXISTS " + tableName + " (version int not null primary key);" )
if _ , isWarn := err . ( mysql . MySQLWarnings ) ; err != nil && ! isWarn {
2014-09-16 18:04:50 +02:00
return err
}
2014-12-04 13:49:26 -08:00
2014-09-16 18:04:50 +02:00
return nil
}
func ( driver * Driver ) FilenameExtension ( ) string {
return "sql"
}
func ( driver * Driver ) Migrate ( f file . File , pipe chan interface { } ) {
defer close ( pipe )
pipe <- f
2014-09-16 21:44:34 +02:00
// http://go-database-sql.org/modifying.html, Working with Transactions
// You should not mingle the use of transaction-related functions such as Begin() and Commit() with SQL statements such as BEGIN and COMMIT in your SQL code.
2014-09-16 18:04:50 +02:00
tx , err := driver . db . Begin ( )
if err != nil {
pipe <- err
return
}
if f . Direction == direction . Up {
2014-09-16 21:44:34 +02:00
if _ , err := tx . Exec ( "INSERT INTO " + tableName + " (version) VALUES (?)" , f . Version ) ; err != nil {
2014-09-16 18:04:50 +02:00
pipe <- err
if err := tx . Rollback ( ) ; err != nil {
pipe <- err
}
return
}
} else if f . Direction == direction . Down {
2014-09-16 21:44:34 +02:00
if _ , err := tx . Exec ( "DELETE FROM " + tableName + " WHERE version = ?" , f . Version ) ; err != nil {
2014-09-16 18:04:50 +02:00
pipe <- err
if err := tx . Rollback ( ) ; err != nil {
pipe <- err
}
return
}
}
if err := f . ReadContent ( ) ; err != nil {
pipe <- err
return
}
2014-10-11 00:49:40 +02:00
// TODO this is not good! unfortunately there is no mysql driver that
// supports multiple statements per query.
2014-10-11 00:50:48 +02:00
sqlStmts := bytes . Split ( f . Content , [ ] byte ( ";" ) )
2014-09-16 21:44:34 +02:00
2014-10-11 00:49:40 +02:00
for _ , sqlStmt := range sqlStmts {
2014-12-04 14:33:52 -08:00
sqlStmt = bytes . TrimSpace ( sqlStmt )
if len ( sqlStmt ) > 0 {
2014-10-11 01:09:40 +02:00
if _ , err := tx . Exec ( string ( sqlStmt ) ) ; err != nil {
2014-12-04 13:59:40 -08:00
mysqlErr , isErr := err . ( * mysql . MySQLError )
2014-10-11 00:49:40 +02:00
2014-12-04 13:59:40 -08:00
if isErr {
re , err := regexp . Compile ( ` at line ([0-9]+)$ ` )
if err != nil {
2014-10-11 01:09:40 +02:00
pipe <- err
2014-12-04 13:59:40 -08:00
if err := tx . Rollback ( ) ; err != nil {
pipe <- err
}
2014-10-11 01:09:40 +02:00
}
2014-09-16 21:44:34 +02:00
2014-12-04 13:59:40 -08:00
var lineNo int
lineNoRe := re . FindStringSubmatch ( mysqlErr . Message )
if len ( lineNoRe ) == 2 {
lineNo , err = strconv . Atoi ( lineNoRe [ 1 ] )
2014-10-11 01:09:40 +02:00
}
2014-12-04 13:59:40 -08:00
if err == nil {
// get white-space offset
// TODO this is broken, because we use sqlStmt instead of f.Content
wsLineOffset := 0
b := bufio . NewReader ( bytes . NewBuffer ( sqlStmt ) )
for {
line , _ , err := b . ReadLine ( )
if err != nil {
break
}
if bytes . TrimSpace ( line ) == nil {
wsLineOffset += 1
} else {
break
}
}
2014-09-16 21:44:34 +02:00
2014-12-04 13:59:40 -08:00
message := mysqlErr . Error ( )
message = re . ReplaceAllString ( message , fmt . Sprintf ( "at line %v" , lineNo + wsLineOffset ) )
2014-09-16 21:44:34 +02:00
2014-12-04 13:59:40 -08:00
errorPart := file . LinesBeforeAndAfter ( sqlStmt , lineNo , 5 , 5 , true )
pipe <- errors . New ( fmt . Sprintf ( "%s\n\n%s" , message , string ( errorPart ) ) )
} else {
pipe <- errors . New ( mysqlErr . Error ( ) )
}
2014-09-16 18:04:50 +02:00
2014-12-04 14:33:52 -08:00
if err := tx . Rollback ( ) ; err != nil {
pipe <- err
}
return
2014-10-11 01:09:40 +02:00
}
2014-10-11 00:49:40 +02:00
}
2014-09-16 18:04:50 +02:00
}
}
if err := tx . Commit ( ) ; err != nil {
pipe <- err
return
}
}
func ( driver * Driver ) Version ( ) ( uint64 , error ) {
var version uint64
err := driver . db . QueryRow ( "SELECT version FROM " + tableName + " ORDER BY version DESC" ) . Scan ( & version )
switch {
case err == sql . ErrNoRows :
return 0 , nil
case err != nil :
return 0 , err
default :
return version , nil
}
}
2015-06-11 11:11:28 +01:00
func init ( ) {
2015-10-22 16:29:26 -04:00
driver . RegisterDriver ( "mysql" , & Driver { } )
2015-06-11 11:11:28 +01:00
}