2012-10-22 20:49:05 +04:00
|
|
|
// Copyright 2012, Google Inc. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package mysqlctl
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/md5"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2012-10-24 21:30:10 +04:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
2012-10-22 20:49:05 +04:00
|
|
|
|
2012-10-23 22:51:09 +04:00
|
|
|
"code.google.com/p/vitess/go/jscfg"
|
2012-10-22 20:49:05 +04:00
|
|
|
"code.google.com/p/vitess/go/relog"
|
|
|
|
)
|
|
|
|
|
|
|
|
type TableDefinition struct {
|
2013-02-13 00:23:00 +04:00
|
|
|
Name string // the table name
|
|
|
|
Schema string // the SQL to run to create the table
|
|
|
|
Columns []string // the columns in the order that will be used to dump and load the data
|
2012-10-22 20:49:05 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
type SchemaDefinition struct {
|
|
|
|
// ordered by TableDefinition.Name
|
|
|
|
TableDefinitions []TableDefinition
|
|
|
|
|
|
|
|
// the md5 of the concatenation of TableDefinition.Schema
|
|
|
|
Version string
|
|
|
|
}
|
|
|
|
|
2012-10-23 22:51:09 +04:00
|
|
|
func (sd *SchemaDefinition) String() string {
|
|
|
|
return jscfg.ToJson(sd)
|
|
|
|
}
|
|
|
|
|
2012-10-22 20:49:05 +04:00
|
|
|
func (sd *SchemaDefinition) generateSchemaVersion() {
|
|
|
|
hasher := md5.New()
|
|
|
|
for _, td := range sd.TableDefinitions {
|
|
|
|
if _, err := hasher.Write([]byte(td.Schema)); err != nil {
|
|
|
|
// extremely unlikely
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sd.Version = hex.EncodeToString(hasher.Sum(nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
// generates a report on what's different between two SchemaDefinition
|
2012-10-23 22:51:09 +04:00
|
|
|
func (left *SchemaDefinition) DiffSchema(leftName, rightName string, right *SchemaDefinition, result chan string) {
|
2012-10-22 20:49:05 +04:00
|
|
|
leftIndex := 0
|
|
|
|
rightIndex := 0
|
|
|
|
for leftIndex < len(left.TableDefinitions) && rightIndex < len(right.TableDefinitions) {
|
|
|
|
// extra table on the left side
|
|
|
|
if left.TableDefinitions[leftIndex].Name < right.TableDefinitions[rightIndex].Name {
|
2012-10-23 22:51:09 +04:00
|
|
|
result <- leftName + " has an extra table named " + left.TableDefinitions[leftIndex].Name
|
2012-10-22 20:49:05 +04:00
|
|
|
leftIndex++
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// extra table on the right side
|
|
|
|
if left.TableDefinitions[leftIndex].Name > right.TableDefinitions[rightIndex].Name {
|
2012-10-23 22:51:09 +04:00
|
|
|
result <- rightName + " has an extra table named " + right.TableDefinitions[rightIndex].Name
|
2012-10-22 20:49:05 +04:00
|
|
|
rightIndex++
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// same name, let's see content
|
|
|
|
if left.TableDefinitions[leftIndex].Schema != right.TableDefinitions[rightIndex].Schema {
|
2012-10-23 22:51:09 +04:00
|
|
|
result <- leftName + " and " + rightName + " disagree on schema for table " + left.TableDefinitions[leftIndex].Name
|
2012-10-22 20:49:05 +04:00
|
|
|
}
|
|
|
|
leftIndex++
|
|
|
|
rightIndex++
|
|
|
|
}
|
|
|
|
|
|
|
|
for leftIndex < len(left.TableDefinitions) {
|
2012-10-23 22:51:09 +04:00
|
|
|
result <- leftName + " has an extra table named " + left.TableDefinitions[leftIndex].Name
|
2012-10-22 20:49:05 +04:00
|
|
|
leftIndex++
|
|
|
|
}
|
|
|
|
for rightIndex < len(right.TableDefinitions) {
|
2012-10-23 22:51:09 +04:00
|
|
|
result <- rightName + " has an extra table named " + right.TableDefinitions[rightIndex].Name
|
2012-10-22 20:49:05 +04:00
|
|
|
rightIndex++
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2012-11-08 23:05:26 +04:00
|
|
|
func (left *SchemaDefinition) DiffSchemaToArray(leftName, rightName string, right *SchemaDefinition) (result []string) {
|
|
|
|
schemaDiffs := make(chan string, 10)
|
|
|
|
go func() {
|
|
|
|
left.DiffSchema(leftName, rightName, right, schemaDiffs)
|
|
|
|
close(schemaDiffs)
|
|
|
|
}()
|
|
|
|
result = make([]string, 0, 10)
|
|
|
|
for msg := range schemaDiffs {
|
|
|
|
result = append(result, msg)
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2012-10-24 21:30:10 +04:00
|
|
|
var autoIncr = regexp.MustCompile("auto_increment=\\d+")
|
|
|
|
|
2013-01-25 01:54:22 +04:00
|
|
|
// GetSchema returns the schema for database for tables listed in
|
|
|
|
// tables. If tables is empty, return the schema for all tables.
|
|
|
|
func (mysqld *Mysqld) GetSchema(dbName string, tables []string) (*SchemaDefinition, error) {
|
|
|
|
if len(tables) == 0 {
|
|
|
|
rows, err := mysqld.fetchSuperQuery("SHOW TABLES IN " + dbName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
|
|
return &SchemaDefinition{}, nil
|
|
|
|
}
|
|
|
|
tables = make([]string, len(rows))
|
|
|
|
for i, row := range rows {
|
|
|
|
tables[i] = row[0].String()
|
|
|
|
}
|
2012-10-22 20:49:05 +04:00
|
|
|
}
|
2013-01-25 01:54:22 +04:00
|
|
|
sd := &SchemaDefinition{TableDefinitions: make([]TableDefinition, len(tables))}
|
|
|
|
for i, tableName := range tables {
|
2012-10-22 20:49:05 +04:00
|
|
|
relog.Info("GetSchema(table: %v)", tableName)
|
|
|
|
|
|
|
|
rows, fetchErr := mysqld.fetchSuperQuery("SHOW CREATE TABLE " + dbName + "." + tableName)
|
|
|
|
if fetchErr != nil {
|
|
|
|
return nil, fetchErr
|
|
|
|
}
|
|
|
|
if len(rows) == 0 {
|
|
|
|
return nil, fmt.Errorf("empty create table statement for %v", tableName)
|
|
|
|
}
|
|
|
|
|
2012-10-24 21:30:10 +04:00
|
|
|
// Normalize & remove auto_increment because it changes on every insert
|
|
|
|
// FIXME(alainjobart) find a way to share this with
|
|
|
|
// vt/tabletserver/table_info.go:162
|
2012-11-06 02:28:37 +04:00
|
|
|
norm1 := strings.ToLower(rows[0][1].String())
|
2012-10-24 21:30:10 +04:00
|
|
|
norm2 := autoIncr.ReplaceAllLiteralString(norm1, "")
|
|
|
|
|
2012-10-22 20:49:05 +04:00
|
|
|
sd.TableDefinitions[i].Name = tableName
|
2012-10-24 21:30:10 +04:00
|
|
|
sd.TableDefinitions[i].Schema = norm2
|
2013-02-13 00:23:00 +04:00
|
|
|
|
|
|
|
columns, err := mysqld.GetColumns(tableName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
sd.TableDefinitions[i].Columns = columns
|
2012-10-22 20:49:05 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
sd.generateSchemaVersion()
|
|
|
|
return sd, nil
|
|
|
|
}
|
2012-11-06 03:06:00 +04:00
|
|
|
|
2013-02-13 00:23:00 +04:00
|
|
|
// GetColumns returns the columns of table.
|
|
|
|
func (mysqld *Mysqld) GetColumns(table string) ([]string, error) {
|
|
|
|
conn, err := mysqld.createConnection()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
qr, err := conn.ExecuteFetch([]byte(fmt.Sprintf("select * from %v where 1=0", table)), 0, true)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
columns := make([]string, len(qr.Fields))
|
|
|
|
for i, field := range qr.Fields {
|
|
|
|
columns[i] = field.Name
|
|
|
|
}
|
|
|
|
return columns, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2012-11-06 03:06:00 +04:00
|
|
|
type SchemaChange struct {
|
|
|
|
Sql string
|
|
|
|
Force bool
|
|
|
|
AllowReplication bool
|
|
|
|
BeforeSchema *SchemaDefinition
|
|
|
|
AfterSchema *SchemaDefinition
|
|
|
|
}
|
|
|
|
|
|
|
|
type SchemaChangeResult struct {
|
|
|
|
Error string
|
|
|
|
BeforeSchema *SchemaDefinition
|
|
|
|
AfterSchema *SchemaDefinition
|
|
|
|
}
|
|
|
|
|
|
|
|
func (scr *SchemaChangeResult) String() string {
|
|
|
|
return jscfg.ToJson(scr)
|
|
|
|
}
|
|
|
|
|
2012-11-08 23:05:26 +04:00
|
|
|
func (mysqld *Mysqld) PreflightSchemaChange(dbName string, change string) (result *SchemaChangeResult) {
|
|
|
|
result = &SchemaChangeResult{}
|
|
|
|
|
|
|
|
// gather current schema on real database
|
|
|
|
var err error
|
2013-01-25 01:54:22 +04:00
|
|
|
result.BeforeSchema, err = mysqld.GetSchema(dbName, nil)
|
2012-11-08 23:05:26 +04:00
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// populate temporary database with it
|
|
|
|
sql := "SET sql_log_bin = 0;\n"
|
|
|
|
sql += "DROP DATABASE IF EXISTS _vt_preflight;\n"
|
|
|
|
sql += "CREATE DATABASE _vt_preflight;\n"
|
|
|
|
sql += "USE _vt_preflight;\n"
|
|
|
|
for _, td := range result.BeforeSchema.TableDefinitions {
|
|
|
|
sql += td.Schema + ";\n"
|
|
|
|
}
|
|
|
|
err = mysqld.ExecuteMysqlCommand(sql)
|
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// apply schema change to the temporary database
|
|
|
|
sql = "SET sql_log_bin = 0;\n"
|
|
|
|
sql += "USE _vt_preflight;\n"
|
|
|
|
sql += change
|
|
|
|
err = mysqld.ExecuteMysqlCommand(sql)
|
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the result
|
2013-01-25 01:54:22 +04:00
|
|
|
result.AfterSchema, err = mysqld.GetSchema("_vt_preflight", nil)
|
2012-11-08 23:05:26 +04:00
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// and clean up the extra database
|
|
|
|
sql = "SET sql_log_bin = 0;\n"
|
|
|
|
sql += "DROP DATABASE _vt_preflight;\n"
|
|
|
|
err = mysqld.ExecuteMysqlCommand(sql)
|
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2012-11-06 03:06:00 +04:00
|
|
|
func (mysqld *Mysqld) ApplySchemaChange(dbName string, change *SchemaChange) (result *SchemaChangeResult) {
|
|
|
|
result = &SchemaChangeResult{}
|
|
|
|
|
|
|
|
// check current schema matches
|
|
|
|
var err error
|
2013-01-25 01:54:22 +04:00
|
|
|
result.BeforeSchema, err = mysqld.GetSchema(dbName, nil)
|
2012-11-06 03:06:00 +04:00
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
if change.BeforeSchema != nil {
|
2012-11-08 23:05:26 +04:00
|
|
|
schemaDiffs := result.BeforeSchema.DiffSchemaToArray("actual", "expected", change.BeforeSchema)
|
|
|
|
if len(schemaDiffs) > 0 {
|
|
|
|
for _, msg := range schemaDiffs {
|
|
|
|
relog.Warning("BeforeSchema differs: %v", msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
// let's see if the schema was already applied
|
|
|
|
if change.AfterSchema != nil {
|
|
|
|
schemaDiffs = result.BeforeSchema.DiffSchemaToArray("actual", "expected", change.AfterSchema)
|
|
|
|
if len(schemaDiffs) == 0 {
|
|
|
|
// no diff between the schema we expect
|
|
|
|
// after the change and the current
|
|
|
|
// schema, we already applied it
|
|
|
|
result.AfterSchema = result.BeforeSchema
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-11-06 03:06:00 +04:00
|
|
|
if change.Force {
|
|
|
|
relog.Warning("BeforeSchema differs, applying anyway")
|
|
|
|
} else {
|
|
|
|
result.Error = "BeforeSchema differs"
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sql := change.Sql
|
|
|
|
if !change.AllowReplication {
|
|
|
|
sql = "SET sql_log_bin = 0;\n" + sql
|
|
|
|
}
|
|
|
|
|
|
|
|
// add a 'use XXX' in front of the SQL
|
2012-11-08 23:05:26 +04:00
|
|
|
sql = "USE " + dbName + ";\n" + sql
|
2012-11-06 03:06:00 +04:00
|
|
|
|
|
|
|
// execute the schema change using an external mysql process
|
|
|
|
// (to benefit from the extra commands in mysql cli)
|
|
|
|
err = mysqld.ExecuteMysqlCommand(sql)
|
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// populate AfterSchema
|
2013-01-25 01:54:22 +04:00
|
|
|
result.AfterSchema, err = mysqld.GetSchema(dbName, nil)
|
2012-11-06 03:06:00 +04:00
|
|
|
if err != nil {
|
|
|
|
result.Error = err.Error()
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
// compare to the provided AfterSchema
|
|
|
|
if change.AfterSchema != nil {
|
2012-11-08 23:05:26 +04:00
|
|
|
schemaDiffs := result.AfterSchema.DiffSchemaToArray("actual", "expected", change.AfterSchema)
|
|
|
|
if len(schemaDiffs) > 0 {
|
|
|
|
for _, msg := range schemaDiffs {
|
|
|
|
relog.Warning("AfterSchema differs: %v", msg)
|
|
|
|
}
|
2012-11-06 03:06:00 +04:00
|
|
|
if change.Force {
|
|
|
|
relog.Warning("AfterSchema differs, not reporting error")
|
|
|
|
} else {
|
|
|
|
result.Error = "AfterSchema differs"
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|