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"
2013-03-19 22:55:52 +04:00
"sort"
2012-10-24 21:30:10 +04:00
"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-03-19 22:55:52 +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
Type string // TABLE_BASE_TABLE or TABLE_VIEW
DataLength uint64 // how much space the data file takes.
}
// helper methods for sorting
type TableDefinitions [ ] TableDefinition
func ( tds TableDefinitions ) Len ( ) int {
return len ( tds )
}
func ( tds TableDefinitions ) Swap ( i , j int ) {
tds [ i ] , tds [ j ] = tds [ j ] , tds [ i ]
}
// sort by reverse DataLength
type ByReverseDataLength struct {
TableDefinitions
}
func ( bdl ByReverseDataLength ) Less ( i , j int ) bool {
return bdl . TableDefinitions [ j ] . DataLength < bdl . TableDefinitions [ i ] . DataLength
2012-10-22 20:49:05 +04:00
}
type SchemaDefinition struct {
2013-03-26 02:26:47 +04:00
// the 'CREATE DATABASE...' statement, with db name as {{.DatabaseName}}
DatabaseSchema string
2013-03-19 22:55:52 +04:00
// ordered by TableDefinition.Name by default
TableDefinitions TableDefinitions
2012-10-22 20:49:05 +04:00
// 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 )
}
2013-03-19 22:55:52 +04:00
func ( sd * SchemaDefinition ) SortByReverseDataLength ( ) {
sort . Sort ( ByReverseDataLength { sd . TableDefinitions } )
}
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 {
2013-03-13 02:43:59 +04:00
panic ( err ) // extremely unlikely
2012-10-22 20:49:05 +04:00
}
}
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 ) {
2013-03-26 02:26:47 +04:00
if left . DatabaseSchema != right . DatabaseSchema {
result <- leftName + " and " + rightName + " don't agree on database creation command:\n" + left . DatabaseSchema + "\n differs from:\n" + right . DatabaseSchema
}
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
}
2013-03-09 04:04:27 +04:00
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
}
2013-03-09 04:04:27 +04:00
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-23 04:11:43 +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 ) {
2013-03-26 02:26:47 +04:00
sd := & SchemaDefinition { }
// get the database creation command
rows , fetchErr := mysqld . fetchSuperQuery ( "SHOW CREATE DATABASE " + dbName )
if fetchErr != nil {
return nil , fetchErr
}
if len ( rows ) == 0 {
return nil , fmt . Errorf ( "empty create database statement for %v" , dbName )
}
sd . DatabaseSchema = strings . Replace ( rows [ 0 ] [ 1 ] . String ( ) , "`" + dbName + "`" , "`{{.DatabaseName}}`" , 1 )
// get the list of tables we're interested in
2013-03-19 22:55:52 +04:00
sql := "SELECT table_name, table_type, data_length FROM information_schema.tables WHERE table_schema = '" + dbName + "'"
2013-03-08 03:07:24 +04:00
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 {
2013-03-26 02:26:47 +04:00
return sd , nil
2013-03-08 03:07:24 +04:00
}
2013-03-26 02:26:47 +04:00
sd . TableDefinitions = make ( [ ] TableDefinition , len ( rows ) )
2013-03-08 03:07:24 +04:00
for i , row := range rows {
tableName := row [ 0 ] . String ( )
tableType := row [ 1 ] . String ( )
2013-03-19 22:55:52 +04:00
var dataLength uint64
if ! row [ 2 ] . IsNull ( ) {
// dataLength is NULL for views, then we use 0
dataLength , err = row [ 2 ] . ParseUint64 ( )
if err != nil {
return nil , err
}
}
2012-10-22 20:49:05 +04:00
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
2013-05-02 02:03:04 +04:00
norm := rows [ 0 ] [ 1 ] . String ( )
norm = autoIncr . ReplaceAllLiteralString ( norm , "" )
if tableType == TABLE_VIEW {
// Views will have the dbname in there, replace it
// with {{.DatabaseName}}
norm = strings . Replace ( norm , "`" + dbName + "`" , "`{{.DatabaseName}}`" , - 1 )
}
2012-10-24 21:30:10 +04:00
2012-10-22 20:49:05 +04:00
sd . TableDefinitions [ i ] . Name = tableName
2013-05-02 02:03:04 +04:00
sd . TableDefinitions [ i ] . Schema = norm
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
2013-03-19 22:55:52 +04:00
sd . TableDefinitions [ i ] . DataLength = dataLength
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-05-01 00:00:43 +04:00
qr , err := conn . ExecuteFetch ( 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
}