Commit a64a38d7 by Torkel Ödegaard

Added migration log and migration id, do not execute already executed migrations

parent 8bfed750
...@@ -13,6 +13,8 @@ const ( ...@@ -13,6 +13,8 @@ const (
type Migration interface { type Migration interface {
Sql(dialect Dialect) string Sql(dialect Dialect) string
Id() string
SetId(string)
} }
type ColumnType string type ColumnType string
...@@ -22,7 +24,15 @@ const ( ...@@ -22,7 +24,15 @@ const (
) )
type MigrationBase struct { type MigrationBase struct {
desc string id string
}
func (m *MigrationBase) Id() string {
return m.id
}
func (m *MigrationBase) SetId(id string) {
m.id = id
} }
type RawSqlMigration struct { type RawSqlMigration struct {
...@@ -53,11 +63,6 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration { ...@@ -53,11 +63,6 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
return m return m
} }
func (m *RawSqlMigration) Desc(desc string) *RawSqlMigration {
m.desc = desc
return m
}
type AddColumnMigration struct { type AddColumnMigration struct {
MigrationBase MigrationBase
tableName string tableName string
...@@ -90,16 +95,12 @@ func (m *AddColumnMigration) Sql(dialect Dialect) string { ...@@ -90,16 +95,12 @@ func (m *AddColumnMigration) Sql(dialect Dialect) string {
return fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", m.tableName, m.columnName, dialect.ToDBTypeSql(m.columnType, m.length)) return fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", m.tableName, m.columnName, dialect.ToDBTypeSql(m.columnType, m.length))
} }
func (m *AddColumnMigration) Desc(desc string) *AddColumnMigration {
m.desc = desc
return m
}
type AddIndexMigration struct { type AddIndexMigration struct {
MigrationBase MigrationBase
tableName string tableName string
columns string columns string
indexName string indexName string
unique string
} }
func (m *AddIndexMigration) Name(name string) *AddIndexMigration { func (m *AddIndexMigration) Name(name string) *AddIndexMigration {
...@@ -112,11 +113,16 @@ func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration { ...@@ -112,11 +113,16 @@ func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration {
return m return m
} }
func (m *AddIndexMigration) Unique() *AddIndexMigration {
m.unique = "UNIQUE"
return m
}
func (m *AddIndexMigration) Columns(columns ...string) *AddIndexMigration { func (m *AddIndexMigration) Columns(columns ...string) *AddIndexMigration {
m.columns = strings.Join(columns, ",") m.columns = strings.Join(columns, ",")
return m return m
} }
func (m *AddIndexMigration) Sql(dialect Dialect) string { func (m *AddIndexMigration) Sql(dialect Dialect) string {
return fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s(%s)", m.indexName, m.tableName, m.columns) return fmt.Sprintf("CREATE %s INDEX %s ON %s(%s)", m.unique, m.indexName, m.tableName, m.columns)
} }
package migrations package migrations
var migrationList []Migration import "time"
// Id int64 // Id int64
// Login string `xorm:"UNIQUE NOT NULL"` // Login string `xorm:"UNIQUE NOT NULL"`
...@@ -16,9 +16,11 @@ var migrationList []Migration ...@@ -16,9 +16,11 @@ var migrationList []Migration
// Created time.Time // Created time.Time
// Updated time.Time // Updated time.Time
func init() { func AddMigrations(mg *Migrator) {
// ------------------------------
addMigration(new(RawSqlMigration).Desc("Create account table"). // TABLE Account
// -------------------------------
mg.AddMigration("create account table", new(RawSqlMigration).
Sqlite(` Sqlite(`
CREATE TABLE account ( CREATE TABLE account (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
...@@ -34,25 +36,18 @@ func init() { ...@@ -34,25 +36,18 @@ func init() {
) )
`)) `))
// ------------------------------ // ------------------------------
addMigration(new(AddIndexMigration). mg.AddMigration("add index UIX_account.login", new(AddIndexMigration).
Name("UIX_account_login").Table("account").Columns("login")) Name("UIX_account_login").Table("account").Columns("login"))
// ------------------------------ // ------------------------------
addMigration(new(AddColumnMigration).Desc("Add name column"). mg.AddMigration("add column", new(AddColumnMigration).
Table("account").Column("name").Type(DB_TYPE_STRING).Length(255)) Table("account").Column("name").Type(DB_TYPE_STRING).Length(255))
} }
func addMigration(m Migration) { type MigrationLog struct {
migrationList = append(migrationList, m)
}
type SchemaVersion struct {
Version int
}
type SchemaLog struct {
Id int64 Id int64
Version int64 MigrationId string
Desc string Sql string
Info string Success bool
Error bool Error string
Timestamp time.Time
} }
...@@ -6,6 +6,7 @@ import ( ...@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
"github.com/torkelo/grafana-pro/pkg/log"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
...@@ -31,8 +32,10 @@ func cleanDB(x *xorm.Engine) { ...@@ -31,8 +32,10 @@ func cleanDB(x *xorm.Engine) {
var indexTypes = []string{"Unknown", "", "UNIQUE"} var indexTypes = []string{"Unknown", "", "UNIQUE"}
func TestMigrations(t *testing.T) { func TestMigrations(t *testing.T) {
log.NewLogger(0, "console", `{"level": 0}`)
testDBs := [][]string{ testDBs := [][]string{
//[]string{"mysql", "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"}, []string{"mysql", "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8"},
[]string{"sqlite3", ":memory:"}, []string{"sqlite3", ":memory:"},
} }
...@@ -46,7 +49,10 @@ func TestMigrations(t *testing.T) { ...@@ -46,7 +49,10 @@ func TestMigrations(t *testing.T) {
cleanDB(x) cleanDB(x)
} }
err = StartMigration(x) mg := NewMigrator(x)
AddMigrations(mg)
err = mg.Start()
So(err, ShouldBeNil) So(err, ShouldBeNil)
tables, err := x.DBMetas() tables, err := x.DBMetas()
......
package migrations package migrations
import ( import (
"time"
_ "github.com/go-sql-driver/mysql" _ "github.com/go-sql-driver/mysql"
"github.com/go-xorm/xorm" "github.com/go-xorm/xorm"
_ "github.com/lib/pq" _ "github.com/lib/pq"
...@@ -8,60 +10,102 @@ import ( ...@@ -8,60 +10,102 @@ import (
"github.com/torkelo/grafana-pro/pkg/log" "github.com/torkelo/grafana-pro/pkg/log"
) )
var x *xorm.Engine type Migrator struct {
var dialect Dialect x *xorm.Engine
dialect Dialect
migrations []Migration
}
func NewMigrator(engine *xorm.Engine) *Migrator {
mg := &Migrator{}
mg.x = engine
mg.migrations = make([]Migration, 0)
switch mg.x.DriverName() {
case MYSQL:
mg.dialect = new(Mysql)
case SQLITE:
mg.dialect = new(Sqlite3)
}
return mg
}
func (mg *Migrator) AddMigration(id string, m Migration) {
m.SetId(id)
mg.migrations = append(mg.migrations, m)
}
func getSchemaVersion() (int, error) { func (mg *Migrator) GetMigrationLog() (map[string]MigrationLog, error) {
exists, err := x.IsTableExist(new(SchemaVersion)) exists, err := mg.x.IsTableExist(new(MigrationLog))
if err != nil { if err != nil {
return 0, err return nil, err
} }
if !exists { if !exists {
if err := x.CreateTables(new(SchemaVersion)); err != nil { if err := mg.x.CreateTables(new(MigrationLog)); err != nil {
return 0, err return nil, err
} }
return 0, nil return nil, nil
} }
v := SchemaVersion{} logMap := make(map[string]MigrationLog)
_, err = x.Table("schema_version").Limit(1, 0).Desc("version").Get(&v) logItems := make([]MigrationLog, 0)
return v.Version, err if err = mg.x.Find(&logItems); err != nil {
} return nil, err
}
func setEngineAndDialect(engine *xorm.Engine) { for _, logItem := range logItems {
x = engine if !logItem.Success {
switch x.DriverName() { continue
case MYSQL: }
dialect = new(Mysql) logMap[logItem.MigrationId] = logItem
case SQLITE:
dialect = new(Sqlite3)
} }
}
func StartMigration(engine *xorm.Engine) error { return logMap, nil
log.Info("Starting database schema migration: DB: %v", engine.DriverName()) }
setEngineAndDialect(engine) func (mg *Migrator) Start() error {
log.Info("Migrator::Start DB migration")
_, err := getSchemaVersion() logMap, err := mg.GetMigrationLog()
if err != nil { if err != nil {
return err return err
} }
for _, m := range migrationList { for _, m := range mg.migrations {
if err := execMigration(m); err != nil { _, exists := logMap[m.Id()]
if exists {
log.Info("Migrator:: Skipping migration: %v, Already executed", m.Id())
continue
}
record := MigrationLog{
MigrationId: m.Id(),
Sql: m.Sql(mg.dialect),
Timestamp: time.Now(),
}
if err := mg.exec(m); err != nil {
record.Error = err.Error()
mg.x.Insert(&record)
return err return err
} else {
record.Success = true
mg.x.Insert(&record)
} }
} }
return nil return nil
} }
func execMigration(m Migration) error { func (mg *Migrator) exec(m Migration) error {
err := inTransaction(func(sess *xorm.Session) error { log.Info("Migrator::exec migration id: %v", m.Id())
_, err := sess.Exec(m.Sql(dialect))
err := mg.inTransaction(func(sess *xorm.Session) error {
_, err := sess.Exec(m.Sql(mg.dialect))
if err != nil { if err != nil {
log.Error(3, "Migrator::exec FAILED migration id: %v, err: %v", m.Id(), err)
return err return err
} }
return nil return nil
...@@ -76,10 +120,10 @@ func execMigration(m Migration) error { ...@@ -76,10 +120,10 @@ func execMigration(m Migration) error {
type dbTransactionFunc func(sess *xorm.Session) error type dbTransactionFunc func(sess *xorm.Session) error
func inTransaction(callback dbTransactionFunc) error { func (mg *Migrator) inTransaction(callback dbTransactionFunc) error {
var err error var err error
sess := x.NewSession() sess := mg.x.NewSession()
defer sess.Close() defer sess.Close()
if err = sess.Begin(); err != nil { if err = sess.Begin(); err != nil {
......
package migrations
import (
"testing"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
)
// func cleanDB(x *xorm.Engine) {
// tables, _ := x.DBMetas()
// sess := x.NewSession()
// defer sess.Close()
//
// for _, table := range tables {
// if _, err := sess.Exec("SET FOREIGN_KEY_CHECKS = 0"); err != nil {
// panic("Failed to disable foreign key checks")
// }
// if _, err := sess.Exec("DROP TABLE " + table.Name); err != nil {
// panic(fmt.Sprintf("Failed to delete table: %v, err: %v", table.Name, err))
// }
// if _, err := sess.Exec("SET FOREIGN_KEY_CHECKS = 1"); err != nil {
// panic("Failed to disable foreign key checks")
// }
// }
// }
//
// var indexTypes = []string{"Unknown", "", "UNIQUE"}
//
func TestMigrator(t *testing.T) {
Convey("Migrator", t, func() {
x, err := xorm.NewEngine(SQLITE, ":memory:")
So(err, ShouldBeNil)
mg := NewMigrator(x)
Convey("Given one migration", func() {
mg.AddMigration("test migration", new(RawSqlMigration).
Sqlite(`
CREATE TABLE account (
id INTEGER PRIMARY KEY AUTOINCREMENT
)`).
Mysql(`
CREATE TABLE account (
id BIGINT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id)
)`))
err := mg.Start()
So(err, ShouldBeNil)
log, err := mg.GetMigrationLog()
So(err, ShouldBeNil)
So(len(log), ShouldEqual, 1)
})
// So(err, ShouldBeNil)
//
// So(len(tables), ShouldEqual, 2)
// fmt.Printf("\nDB Schema after migration: table count: %v\n", len(tables))
//
// for _, table := range tables {
// fmt.Printf("\nTable: %v \n", table.Name)
// for _, column := range table.Columns() {
// fmt.Printf("\t %v \n", column.String(x.Dialect()))
// }
//
// if len(table.Indexes) > 0 {
// fmt.Printf("\n\tIndexes:\n")
// for _, index := range table.Indexes {
// fmt.Printf("\t %v (%v) %v \n", index.Name, strings.Join(index.Cols, ","), indexTypes[index.Type])
// }
// }
// }
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment