Commit d8e5be57 by Torkel Ödegaard

Worked on database agnostic table creation for db migrations

parent 7d70ffe2
......@@ -4,4 +4,8 @@ app_mode = development
router_logging = false
static_root_path = grafana/src
[log]
level = Trace
......@@ -12,19 +12,17 @@ var (
// Directly mapped to db schema, Do not change field names lighly
type Account struct {
Id int64
Login string `xorm:"UNIQUE NOT NULL"`
Email string `xorm:"UNIQUE NOT NULL"`
Name string
FullName string
Password string
IsAdmin bool
Salt string `xorm:"VARCHAR(10)"`
Company string
NextDashboardId int
UsingAccountId int64
Created time.Time
Updated time.Time
Id int64
Login string `xorm:"UNIQUE NOT NULL"`
Email string `xorm:"UNIQUE NOT NULL"`
Name string
Password string
IsAdmin bool
Salt string `xorm:"VARCHAR(10)"`
Company string
UsingAccountId int64
Created time.Time
Updated time.Time
}
// ---------------------
......
package migrations
import (
"fmt"
"strings"
)
const (
POSTGRES = "postgres"
SQLITE = "sqlite3"
MYSQL = "mysql"
)
type Migration interface {
Sql(dialect Dialect) string
Id() string
SetId(string)
}
type ColumnType string
const (
DB_TYPE_STRING ColumnType = "String"
)
type MigrationBase struct {
id string
}
......@@ -65,10 +42,8 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
type AddColumnMigration struct {
MigrationBase
tableName string
columnName string
columnType ColumnType
length int
tableName string
column *Column
}
func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
......@@ -76,35 +51,23 @@ func (m *AddColumnMigration) Table(tableName string) *AddColumnMigration {
return m
}
func (m *AddColumnMigration) Length(length int) *AddColumnMigration {
m.length = length
return m
}
func (m *AddColumnMigration) Column(columnName string) *AddColumnMigration {
m.columnName = columnName
return m
}
func (m *AddColumnMigration) Type(columnType ColumnType) *AddColumnMigration {
m.columnType = columnType
func (m *AddColumnMigration) Column(col *Column) *AddColumnMigration {
m.column = col
return m
}
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 dialect.AddColumnSql(m.tableName, m.column)
}
type AddIndexMigration struct {
MigrationBase
tableName string
columns string
indexName string
unique string
index Index
}
func (m *AddIndexMigration) Name(name string) *AddIndexMigration {
m.indexName = name
m.index.Name = name
return m
}
......@@ -114,15 +77,47 @@ func (m *AddIndexMigration) Table(tableName string) *AddIndexMigration {
}
func (m *AddIndexMigration) Unique() *AddIndexMigration {
m.unique = "UNIQUE"
m.index.Type = UniqueIndex
return m
}
func (m *AddIndexMigration) Columns(columns ...string) *AddIndexMigration {
m.columns = strings.Join(columns, ",")
m.index.Cols = columns
return m
}
func (m *AddIndexMigration) Sql(dialect Dialect) string {
return fmt.Sprintf("CREATE %s INDEX %s ON %s(%s)", m.unique, m.indexName, m.tableName, m.columns)
return dialect.CreateIndexSql(m.tableName, &m.index)
}
type AddTableMigration struct {
MigrationBase
table Table
}
func (m *AddTableMigration) Sql(d Dialect) string {
return d.CreateTableSql(&m.table)
}
func (m *AddTableMigration) Name(name string) *AddTableMigration {
m.table.Name = name
return m
}
func (m *AddTableMigration) WithColumns(columns ...*Column) *AddTableMigration {
for _, col := range columns {
m.table.Columns = append(m.table.Columns, col)
if col.IsPrimaryKey {
m.table.PrimaryKeys = append(m.table.PrimaryKeys, col.Name)
}
}
return m
}
func (m *AddTableMigration) WithColumn(col *Column) *AddTableMigration {
m.table.Columns = append(m.table.Columns, col)
if col.IsPrimaryKey {
m.table.PrimaryKeys = append(m.table.PrimaryKeys, col.Name)
}
return m
}
package migrations
// Notice
// code based on parts from from https://github.com/go-xorm/core/blob/3e0fa232ab5c90996406c0cd7ae86ad0e5ecf85f/column.go
type Column struct {
Name string
Type string
Length int
Length2 int
Nullable bool
IsPrimaryKey bool
IsAutoIncrement bool
Default string
}
func (col *Column) String(d Dialect) string {
sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
sql += d.SqlType(col) + " "
if col.IsPrimaryKey {
sql += "PRIMARY KEY "
if col.IsAutoIncrement {
sql += d.AutoIncrStr() + " "
}
}
if d.ShowCreateNull() {
if col.Nullable {
sql += "NULL "
} else {
sql += "NOT NULL "
}
}
if col.Default != "" {
sql += "DEFAULT " + col.Default + " "
}
return sql
}
func (col *Column) StringNoPk(d Dialect) string {
sql := d.QuoteStr() + col.Name + d.QuoteStr() + " "
sql += d.SqlType(col) + " "
if d.ShowCreateNull() {
if col.Nullable {
sql += "NULL "
} else {
sql += "NOT NULL "
}
}
if col.Default != "" {
sql += "DEFAULT " + col.Default + " "
}
return sql
}
package migrations
import "fmt"
import (
"fmt"
"strings"
)
type Dialect interface {
DriverName() string
ToDBTypeSql(columnType ColumnType, length int) string
QuoteStr() string
Quote(string) string
AndStr() string
AutoIncrStr() string
OrStr() string
EqStr() string
ShowCreateNull() bool
SqlType(col *Column) string
CreateIndexSql(tableName string, index *Index) string
CreateTableSql(table *Table) string
AddColumnSql(tableName string, Col *Column) string
TableCheckSql(tableName string) (string, []interface{})
}
type Sqlite3 struct {
type BaseDialect struct {
dialect Dialect
driverName string
}
type Mysql struct {
func (d *BaseDialect) DriverName() string {
return d.driverName
}
func (db *Sqlite3) DriverName() string {
return SQLITE
func (b *BaseDialect) ShowCreateNull() bool {
return true
}
func (db *Mysql) DriverName() string {
return MYSQL
func (b *BaseDialect) AndStr() string {
return "AND"
}
func (db *Sqlite3) ToDBTypeSql(columnType ColumnType, length int) string {
switch columnType {
case DB_TYPE_STRING:
return "TEXT"
}
func (b *BaseDialect) OrStr() string {
return "OR"
}
panic("Unsupported db type")
func (b *BaseDialect) EqStr() string {
return "="
}
func (db *Mysql) ToDBTypeSql(columnType ColumnType, length int) string {
switch columnType {
case DB_TYPE_STRING:
return fmt.Sprintf("NVARCHAR(%d)", length)
func (b *BaseDialect) CreateTableSql(table *Table) string {
var sql string
sql = "CREATE TABLE IF NOT EXISTS "
sql += b.dialect.Quote(table.Name) + " (\n"
pkList := table.PrimaryKeys
for _, col := range table.Columns {
if col.IsPrimaryKey && len(pkList) == 1 {
sql += col.String(b.dialect)
} else {
sql += col.StringNoPk(b.dialect)
}
sql = strings.TrimSpace(sql)
sql += "\n, "
}
if len(pkList) > 1 {
sql += "PRIMARY KEY ( "
sql += b.dialect.Quote(strings.Join(pkList, b.dialect.Quote(",")))
sql += " ), "
}
panic("Unsupported db type")
sql = sql[:len(sql)-2] + ")"
sql += ";"
return sql
}
func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{tableName}
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
func (db *BaseDialect) AddColumnSql(tableName string, col *Column) string {
return fmt.Sprintf("alter table %s ADD COLUMN %s", tableName, col.StringNoPk(db.dialect))
}
func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{"grafana", tableName}
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
return sql, args
func (db *BaseDialect) CreateIndexSql(tableName string, index *Index) string {
quote := db.dialect.Quote
var unique string
var idxName string
if index.Type == UniqueIndex {
unique = " UNIQUE"
idxName = fmt.Sprintf("UQE_%v_%v", tableName, index.Name)
} else {
idxName = fmt.Sprintf("IDX_%v_%v", tableName, index.Name)
}
return fmt.Sprintf("CREATE%s INDEX %v ON %v (%v);", unique,
quote(idxName), quote(tableName),
quote(strings.Join(index.Cols, quote(","))))
}
......@@ -2,61 +2,40 @@ package migrations
import "time"
// Id int64
// Login string `xorm:"UNIQUE NOT NULL"`
// Email string `xorm:"UNIQUE NOT NULL"`
// Name string
// FullName string
// Password string
// IsAdmin bool
// Salt string `xorm:"VARCHAR(10)"`
// Company string
// NextDashboardId int
// UsingAccountId int64
// Created time.Time
// Updated time.Time
func AddMigrations(mg *Migrator) {
// TABLE Account
// -------------------------------
mg.AddMigration("create account table", new(RawSqlMigration).
Sqlite(`
CREATE TABLE account (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT NOT NULL,
email TEXT NOT NULL,
name TEXT NULL,
password TEXT NULL,
salt TEXT NULL,
company TEXT NULL,
using_account_id INTEGER NULL,
is_admin INTEGER NOT NULL,
created INTEGER NOT NULL,
updated INTEGER NOT NULL
)
`).
Mysql(`
CREATE TABLE account (
id BIGINT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id),
login VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255) NULL,
password VARCHAR(50) NULL,
salt VARCHAR(50) NULL,
company VARCHAR(255) NULL,
using_account_id BIGINT NULL,
is_admin BOOL NOT NULL,
created DATETIME NOT NULL,
update DATETIME NOT NULL
)
`))
// ------------------------------
mg.AddMigration("add index UIX_account.login", new(AddIndexMigration).
//------- migration_log table -------------------
mg.AddMigration("create migration_log table", new(AddTableMigration).
Name("migration_log").WithColumns(
&Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
&Column{Name: "migration_id", Type: DB_NVarchar, Length: 255},
&Column{Name: "sql", Type: DB_Text},
&Column{Name: "success", Type: DB_Bool},
&Column{Name: "error", Type: DB_Text},
&Column{Name: "timestamp", Type: DB_DateTime},
))
//------- account table -------------------
mg.AddMigration("create account table", new(AddTableMigration).
Name("account").WithColumns(
&Column{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
&Column{Name: "login", Type: DB_NVarchar, Length: 255},
&Column{Name: "email", Type: DB_NVarchar, Length: 255},
&Column{Name: "name", Type: DB_NVarchar, Length: 255},
&Column{Name: "password", Type: DB_NVarchar, Length: 50},
&Column{Name: "salt", Type: DB_NVarchar, Length: 50},
&Column{Name: "company", Type: DB_NVarchar, Length: 255},
&Column{Name: "using_account_id", Type: DB_BigInt},
&Column{Name: "is_admin", Type: DB_Bool},
&Column{Name: "created", Type: DB_DateTime},
&Column{Name: "updated", Type: DB_DateTime},
))
//------- account table indexes ------------------
mg.AddMigration("add unique index UIX_account.login", new(AddIndexMigration).
Name("UIX_account_login").Table("account").Columns("login"))
// ------------------------------
// mg.AddMigration("add column", new(AddColumnMigration).
// Table("account").Column("name").Type(DB_TYPE_STRING).Length(255))
mg.AddMigration("add unique index UIX_account.email", new(AddIndexMigration).
Name("UIX_account_email").Table("account").Columns("email"))
}
type MigrationLog struct {
......
......@@ -35,7 +35,7 @@ func TestMigrations(t *testing.T) {
log.NewLogger(0, "console", `{"level": 0}`)
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:"},
}
......
......@@ -23,9 +23,9 @@ func NewMigrator(engine *xorm.Engine) *Migrator {
switch mg.x.DriverName() {
case MYSQL:
mg.dialect = new(Mysql)
mg.dialect = NewMysqlDialect()
case SQLITE:
mg.dialect = new(Sqlite3)
mg.dialect = NewSqlite3Dialect()
}
return mg
......@@ -37,20 +37,18 @@ func (mg *Migrator) AddMigration(id string, m Migration) {
}
func (mg *Migrator) GetMigrationLog() (map[string]MigrationLog, error) {
logMap := make(map[string]MigrationLog)
logItems := make([]MigrationLog, 0)
exists, err := mg.x.IsTableExist(new(MigrationLog))
if err != nil {
return nil, err
}
if !exists {
if err := mg.x.CreateTables(new(MigrationLog)); err != nil {
return nil, err
}
return nil, nil
return logMap, nil
}
logMap := make(map[string]MigrationLog)
logItems := make([]MigrationLog, 0)
if err = mg.x.Find(&logItems); err != nil {
return nil, err
}
......@@ -66,7 +64,7 @@ func (mg *Migrator) GetMigrationLog() (map[string]MigrationLog, error) {
}
func (mg *Migrator) Start() error {
log.Info("Migrator::Start DB migration")
log.Info("Migrator::Starting DB migration")
logMap, err := mg.GetMigrationLog()
if err != nil {
......@@ -76,13 +74,15 @@ func (mg *Migrator) Start() error {
for _, m := range mg.migrations {
_, exists := logMap[m.Id()]
if exists {
log.Info("Migrator:: Skipping migration: %v, Already executed", m.Id())
log.Debug("Migrator:: Skipping migration: %v, Already executed", m.Id())
continue
}
sql := m.Sql(mg.dialect)
record := MigrationLog{
MigrationId: m.Id(),
Sql: m.Sql(mg.dialect),
Sql: sql,
Timestamp: time.Now(),
}
......
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])
// }
// }
// }
})
}
package migrations
import (
"fmt"
"strconv"
)
type Mysql struct {
BaseDialect
}
func NewMysqlDialect() *Mysql {
d := Mysql{}
d.BaseDialect.dialect = &d
d.BaseDialect.driverName = MYSQL
return &d
}
func (db *Mysql) Quote(name string) string {
return "`" + name + "`"
}
func (db *Mysql) QuoteStr() string {
return "`"
}
func (db *Mysql) AutoIncrStr() string {
return "AUTO_INCREMENT"
}
func (db *Mysql) SqlType(c *Column) string {
var res string
switch c.Type {
case DB_Bool:
res = DB_TinyInt
c.Length = 1
case DB_Serial:
c.IsAutoIncrement = true
c.IsPrimaryKey = true
c.Nullable = false
res = DB_Int
case DB_BigSerial:
c.IsAutoIncrement = true
c.IsPrimaryKey = true
c.Nullable = false
res = DB_BigInt
case DB_Bytea:
res = DB_Blob
case DB_TimeStampz:
res = DB_Char
c.Length = 64
case DB_NVarchar:
res = DB_Varchar
default:
res = c.Type
}
var hasLen1 bool = (c.Length > 0)
var hasLen2 bool = (c.Length2 > 0)
if res == DB_BigInt && !hasLen1 && !hasLen2 {
c.Length = 20
hasLen1 = true
}
if hasLen2 {
res += "(" + strconv.Itoa(c.Length) + "," + strconv.Itoa(c.Length2) + ")"
} else if hasLen1 {
res += "(" + strconv.Itoa(c.Length) + ")"
}
return res
}
func (db *Mysql) ToDBTypeSql(columnType ColumnType, length int) string {
switch columnType {
case DB_TYPE_STRING:
return fmt.Sprintf("NVARCHAR(%d)", length)
}
panic("Unsupported db type")
}
func (db *Mysql) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{"grafana", tableName}
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
return sql, args
}
package migrations
import "github.com/go-xorm/core"
type Sqlite3 struct {
BaseDialect
}
func NewSqlite3Dialect() *Sqlite3 {
d := Sqlite3{}
d.BaseDialect.dialect = &d
d.BaseDialect.driverName = SQLITE
return &d
}
func (db *Sqlite3) Quote(name string) string {
return "`" + name + "`"
}
func (db *Sqlite3) QuoteStr() string {
return "`"
}
func (db *Sqlite3) AutoIncrStr() string {
return "AUTOINCREMENT"
}
func (db *Sqlite3) SqlType(c *Column) string {
switch c.Type {
case DB_Date, DB_DateTime, DB_TimeStamp, DB_Time:
return DB_DateTime
case DB_TimeStampz:
return DB_Text
case DB_Char, DB_Varchar, DB_NVarchar, DB_TinyText, DB_Text, DB_MediumText, DB_LongText:
return core.Text
case DB_Bit, DB_TinyInt, DB_SmallInt, DB_MediumInt, DB_Int, DB_Integer, DB_BigInt, DB_Bool:
return DB_Integer
case DB_Float, DB_Double, DB_Real:
return DB_Real
case DB_Decimal, DB_Numeric:
return DB_Numeric
case DB_TinyBlob, DB_Blob, DB_MediumBlob, DB_LongBlob, DB_Bytea, DB_Binary, DB_VarBinary:
return DB_Blob
case DB_Serial, DB_BigSerial:
c.IsPrimaryKey = true
c.IsAutoIncrement = true
c.Nullable = false
return core.Integer
default:
return c.Type
}
}
func (db *Sqlite3) TableCheckSql(tableName string) (string, []interface{}) {
args := []interface{}{tableName}
return "SELECT name FROM sqlite_master WHERE type='table' and name = ?", args
}
package migrations
const (
POSTGRES = "postgres"
SQLITE = "sqlite3"
MYSQL = "mysql"
)
type Migration interface {
Sql(dialect Dialect) string
Id() string
SetId(string)
}
type SQLType string
type ColumnType string
const (
DB_TYPE_STRING ColumnType = "String"
)
type Table struct {
Name string
Columns []*Column
PrimaryKeys []string
}
const (
IndexType = iota + 1
UniqueIndex
)
type Index struct {
Name string
Type int
Cols []string
}
var (
DB_Bit = "BIT"
DB_TinyInt = "TINYINT"
DB_SmallInt = "SMALLINT"
DB_MediumInt = "MEDIUMINT"
DB_Int = "INT"
DB_Integer = "INTEGER"
DB_BigInt = "BIGINT"
DB_Enum = "ENUM"
DB_Set = "SET"
DB_Char = "CHAR"
DB_Varchar = "VARCHAR"
DB_NVarchar = "NVARCHAR"
DB_TinyText = "TINYTEXT"
DB_Text = "TEXT"
DB_MediumText = "MEDIUMTEXT"
DB_LongText = "LONGTEXT"
DB_Uuid = "UUID"
DB_Date = "DATE"
DB_DateTime = "DATETIME"
DB_Time = "TIME"
DB_TimeStamp = "TIMESTAMP"
DB_TimeStampz = "TIMESTAMPZ"
DB_Decimal = "DECIMAL"
DB_Numeric = "NUMERIC"
DB_Real = "REAL"
DB_Float = "FLOAT"
DB_Double = "DOUBLE"
DB_Binary = "BINARY"
DB_VarBinary = "VARBINARY"
DB_TinyBlob = "TINYBLOB"
DB_Blob = "BLOB"
DB_MediumBlob = "MEDIUMBLOB"
DB_LongBlob = "LONGBLOB"
DB_Bytea = "BYTEA"
DB_Bool = "BOOL"
DB_Serial = "SERIAL"
DB_BigSerial = "BIGSERIAL"
)
......@@ -9,6 +9,7 @@ import (
"github.com/torkelo/grafana-pro/pkg/bus"
"github.com/torkelo/grafana-pro/pkg/log"
m "github.com/torkelo/grafana-pro/pkg/models"
"github.com/torkelo/grafana-pro/pkg/services/sqlstore/migrations"
"github.com/torkelo/grafana-pro/pkg/setting"
"github.com/torkelo/grafana-pro/pkg/util"
......@@ -34,7 +35,7 @@ var (
func init() {
tables = make([]interface{}, 0)
tables = append(tables, new(m.Account), new(m.Dashboard),
tables = append(tables, new(m.Dashboard),
new(m.Collaborator), new(m.DataSource), new(DashboardTag),
new(m.Token))
}
......@@ -77,6 +78,13 @@ func NewEngine() {
func SetEngine(engine *xorm.Engine, enableLog bool) (err error) {
x = engine
migrator := migrations.NewMigrator(x)
migrations.AddMigrations(migrator)
if err := migrator.Start(); err != nil {
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
}
if err := x.Sync2(tables...); err != nil {
return fmt.Errorf("sync database struct error: %v\n", err)
}
......
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