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"
)
2013-03-08 03:07:24 +04:00
const (
TABLE_BASE_TABLE = "BASE TABLE"
TABLE_VIEW = "VIEW"
)
2012-10-22 20:49:05 +04:00
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
2013-03-08 03:07:24 +04:00
Type string // TABLE_BASE_TABLE or TABLE_VIEW
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 ) )
}
2013-02-15 05:25:03 +04:00
func ( sd * SchemaDefinition ) GetTable ( table string ) ( td * TableDefinition , ok bool ) {
for _ , td := range sd . TableDefinitions {
if td . Name == table {
return & td , true
}
}
return nil , false
}
2012-10-22 20:49:05 +04:00
// generates a report on what's different between two SchemaDefinition
2013-03-08 03:07:24 +04:00
// for now, we skip the VIEW entirely.
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 ) {
2013-03-08 03:07:24 +04:00
// skip views
if left . TableDefinitions [ leftIndex ] . Type == TABLE_VIEW {
leftIndex ++
continue
}
if right . TableDefinitions [ rightIndex ] . Type == TABLE_VIEW {
rightIndex ++
continue
}
2012-10-22 20:49:05 +04:00
// 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 {
2013-03-07 04:25:09 +04:00
result <- leftName + " and " + rightName + " disagree on schema for table " + left . TableDefinitions [ leftIndex ] . Name + ":\n" + left . TableDefinitions [ leftIndex ] . Schema + "\n differs from:\n" + right . TableDefinitions [ rightIndex ] . Schema
2012-10-22 20:49:05 +04:00
}
leftIndex ++
rightIndex ++
}
for leftIndex < len ( left . TableDefinitions ) {
2013-03-08 03:07:24 +04:00
if left . TableDefinitions [ leftIndex ] . Type == TABLE_BASE_TABLE {
result <- leftName + " has an extra table named " + left . TableDefinitions [ leftIndex ] . Name
leftIndex ++
}
2012-10-22 20:49:05 +04:00
}
for rightIndex < len ( right . TableDefinitions ) {
2013-03-08 03:07:24 +04:00
if right . TableDefinitions [ rightIndex ] . Type == TABLE_BASE_TABLE {
result <- rightName + " has an extra table named " + right . TableDefinitions [ rightIndex ] . Name
rightIndex ++
}
2012-10-22 20:49:05 +04:00
}
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
}
2013-03-07 04:25:09 +04:00
var autoIncr = regexp . MustCompile ( " auto_increment=\\d+" )
2012-10-24 21:30:10 +04:00
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.
2013-03-08 03:07:24 +04:00
func ( mysqld * Mysqld ) GetSchema ( dbName string , tables [ ] string , includeViews bool ) ( * SchemaDefinition , error ) {
sql := "SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = '" + dbName + "'"
if len ( tables ) != 0 {
sql += " AND table_name IN ('" + strings . Join ( tables , "','" ) + "')"
2012-10-22 20:49:05 +04:00
}
2013-03-08 03:07:24 +04:00
if ! includeViews {
sql += " AND table_type = '" + TABLE_BASE_TABLE + "'"
}
rows , err := mysqld . fetchSuperQuery ( sql )
if err != nil {
return nil , err
}
if len ( rows ) == 0 {
return & SchemaDefinition { } , nil
}
sd := & SchemaDefinition { TableDefinitions : make ( [ ] TableDefinition , len ( rows ) ) }
for i , row := range rows {
tableName := row [ 0 ] . String ( )
tableType := row [ 1 ] . String ( )
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
2013-02-18 02:25:17 +04:00
columns , err := mysqld . GetColumns ( dbName , tableName )
2013-02-13 00:23:00 +04:00
if err != nil {
return nil , err
}
sd . TableDefinitions [ i ] . Columns = columns
2013-03-08 03:07:24 +04:00
sd . TableDefinitions [ i ] . Type = tableType
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.
2013-02-18 02:25:17 +04:00
func ( mysqld * Mysqld ) GetColumns ( dbName , table string ) ( [ ] string , error ) {
2013-02-13 00:23:00 +04:00
conn , err := mysqld . createConnection ( )
if err != nil {
return nil , err
}
defer conn . Close ( )
2013-02-18 02:25:17 +04:00
qr , err := conn . ExecuteFetch ( [ ] byte ( fmt . Sprintf ( "select * from %v.%v where 1=0" , dbName , table ) ) , 0 , true )
2013-02-13 00:23:00 +04:00
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 {
BeforeSchema * SchemaDefinition
AfterSchema * SchemaDefinition
}
func ( scr * SchemaChangeResult ) String ( ) string {
return jscfg . ToJson ( scr )
}
2013-02-20 22:15:25 +04:00
func ( mysqld * Mysqld ) PreflightSchemaChange ( dbName string , change string ) ( * SchemaChangeResult , error ) {
2012-11-08 23:05:26 +04:00
// gather current schema on real database
2013-03-08 03:07:24 +04:00
beforeSchema , err := mysqld . GetSchema ( dbName , nil , false )
2012-11-08 23:05:26 +04:00
if err != nil {
2013-02-20 22:15:25 +04:00
return nil , err
2012-11-08 23:05:26 +04:00
}
// 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"
2013-02-20 22:15:25 +04:00
for _ , td := range beforeSchema . TableDefinitions {
2013-03-08 03:07:24 +04:00
if td . Type == TABLE_BASE_TABLE {
sql += td . Schema + ";\n"
}
2012-11-08 23:05:26 +04:00
}
2013-02-20 22:15:25 +04:00
if err = mysqld . ExecuteMysqlCommand ( sql ) ; err != nil {
return nil , err
2012-11-08 23:05:26 +04:00
}
// apply schema change to the temporary database
sql = "SET sql_log_bin = 0;\n"
sql += "USE _vt_preflight;\n"
sql += change
2013-02-20 22:15:25 +04:00
if err = mysqld . ExecuteMysqlCommand ( sql ) ; err != nil {
return nil , err
2012-11-08 23:05:26 +04:00
}
// get the result
2013-03-08 03:07:24 +04:00
afterSchema , err := mysqld . GetSchema ( "_vt_preflight" , nil , false )
2012-11-08 23:05:26 +04:00
if err != nil {
2013-02-20 22:15:25 +04:00
return nil , err
2012-11-08 23:05:26 +04:00
}
// and clean up the extra database
sql = "SET sql_log_bin = 0;\n"
sql += "DROP DATABASE _vt_preflight;\n"
2013-02-20 22:15:25 +04:00
if err = mysqld . ExecuteMysqlCommand ( sql ) ; err != nil {
return nil , err
2012-11-08 23:05:26 +04:00
}
2013-02-20 22:15:25 +04:00
return & SchemaChangeResult { beforeSchema , afterSchema } , nil
2012-11-08 23:05:26 +04:00
}
2013-02-20 22:15:25 +04:00
func ( mysqld * Mysqld ) ApplySchemaChange ( dbName string , change * SchemaChange ) ( * SchemaChangeResult , error ) {
2012-11-06 03:06:00 +04:00
// check current schema matches
2013-03-08 03:07:24 +04:00
beforeSchema , err := mysqld . GetSchema ( dbName , nil , false )
2012-11-06 03:06:00 +04:00
if err != nil {
2013-02-20 22:15:25 +04:00
return nil , err
2012-11-06 03:06:00 +04:00
}
if change . BeforeSchema != nil {
2013-02-20 22:15:25 +04:00
schemaDiffs := beforeSchema . DiffSchemaToArray ( "actual" , "expected" , change . BeforeSchema )
2012-11-08 23:05:26 +04:00
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 {
2013-02-20 22:15:25 +04:00
schemaDiffs = beforeSchema . DiffSchemaToArray ( "actual" , "expected" , change . AfterSchema )
2012-11-08 23:05:26 +04:00
if len ( schemaDiffs ) == 0 {
// no diff between the schema we expect
// after the change and the current
// schema, we already applied it
2013-02-20 22:15:25 +04:00
return & SchemaChangeResult { beforeSchema , beforeSchema } , nil
2012-11-08 23:05:26 +04:00
}
}
2012-11-06 03:06:00 +04:00
if change . Force {
relog . Warning ( "BeforeSchema differs, applying anyway" )
} else {
2013-02-20 22:15:25 +04:00
return nil , fmt . Errorf ( "BeforeSchema differs" )
2012-11-06 03:06:00 +04:00
}
}
}
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)
2013-02-20 22:15:25 +04:00
if err = mysqld . ExecuteMysqlCommand ( sql ) ; err != nil {
return nil , err
2012-11-06 03:06:00 +04:00
}
2013-02-20 22:15:25 +04:00
// get AfterSchema
2013-03-08 03:07:24 +04:00
afterSchema , err := mysqld . GetSchema ( dbName , nil , false )
2012-11-06 03:06:00 +04:00
if err != nil {
2013-02-20 22:15:25 +04:00
return nil , err
2012-11-06 03:06:00 +04:00
}
// compare to the provided AfterSchema
if change . AfterSchema != nil {
2013-02-20 22:15:25 +04:00
schemaDiffs := afterSchema . DiffSchemaToArray ( "actual" , "expected" , change . AfterSchema )
2012-11-08 23:05:26 +04:00
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 {
2013-02-20 22:15:25 +04:00
return nil , fmt . Errorf ( "AfterSchema differs" )
2012-11-06 03:06:00 +04:00
}
}
}
2013-02-20 22:15:25 +04:00
return & SchemaChangeResult { beforeSchema , afterSchema } , nil
2012-11-06 03:06:00 +04:00
}