// 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 ( "fmt" "regexp" "strings" log "github.com/golang/glog" "github.com/youtube/vitess/go/vt/mysqlctl/tmutils" tabletmanagerdatapb "github.com/youtube/vitess/go/vt/proto/tabletmanagerdata" ) var autoIncr = regexp.MustCompile(" AUTO_INCREMENT=\\d+") // 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, excludeTables []string, includeViews bool) (*tabletmanagerdatapb.SchemaDefinition, error) { sd := &tabletmanagerdatapb.SchemaDefinition{} // get the database creation command qr, fetchErr := mysqld.FetchSuperQuery("SHOW CREATE DATABASE IF NOT EXISTS " + dbName) if fetchErr != nil { return nil, fetchErr } if len(qr.Rows) == 0 { return nil, fmt.Errorf("empty create database statement for %v", dbName) } sd.DatabaseSchema = strings.Replace(qr.Rows[0][1].String(), "`"+dbName+"`", "`{{.DatabaseName}}`", 1) // get the list of tables we're interested in sql := "SELECT table_name, table_type, data_length, table_rows FROM information_schema.tables WHERE table_schema = '" + dbName + "'" if !includeViews { sql += " AND table_type = '" + tmutils.TableBaseTable + "'" } qr, err := mysqld.FetchSuperQuery(sql) if err != nil { return nil, err } if len(qr.Rows) == 0 { return sd, nil } sd.TableDefinitions = make([]*tabletmanagerdatapb.TableDefinition, 0, len(qr.Rows)) for _, row := range qr.Rows { tableName := row[0].String() tableType := row[1].String() // compute dataLength 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 } } // get row count var rowCount uint64 if !row[3].IsNull() { rowCount, err = row[3].ParseUint64() if err != nil { return nil, err } } qr, fetchErr := mysqld.FetchSuperQuery("SHOW CREATE TABLE " + dbName + "." + tableName) if fetchErr != nil { return nil, fetchErr } if len(qr.Rows) == 0 { return nil, fmt.Errorf("empty create table statement for %v", tableName) } // 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 norm := qr.Rows[0][1].String() norm = autoIncr.ReplaceAllLiteralString(norm, "") if tableType == tmutils.TableView { // Views will have the dbname in there, replace it // with {{.DatabaseName}} norm = strings.Replace(norm, "`"+dbName+"`", "`{{.DatabaseName}}`", -1) } td := &tabletmanagerdatapb.TableDefinition{} td.Name = tableName td.Schema = norm td.Columns, err = mysqld.GetColumns(dbName, tableName) if err != nil { return nil, err } td.PrimaryKeyColumns, err = mysqld.GetPrimaryKeyColumns(dbName, tableName) if err != nil { return nil, err } td.Type = tableType td.DataLength = dataLength td.RowCount = rowCount sd.TableDefinitions = append(sd.TableDefinitions, td) } sd, err = tmutils.FilterTables(sd, tables, excludeTables, includeViews) if err != nil { return nil, err } tmutils.GenerateSchemaVersion(sd) return sd, nil } // ResolveTables returns a list of actual tables+views matching a list // of regexps func ResolveTables(mysqld MysqlDaemon, dbName string, tables []string) ([]string, error) { sd, err := mysqld.GetSchema(dbName, tables, nil, true) if err != nil { return nil, err } result := make([]string, len(sd.TableDefinitions)) for i, td := range sd.TableDefinitions { result[i] = td.Name } return result, nil } // GetColumns returns the columns of table. func (mysqld *Mysqld) GetColumns(dbName, table string) ([]string, error) { conn, err := mysqld.dbaPool.Get(0) if err != nil { return nil, err } defer conn.Recycle() qr, err := conn.ExecuteFetch(fmt.Sprintf("select * from %v.%v where 1=0", dbName, 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 } // GetPrimaryKeyColumns returns the primary key columns of table. func (mysqld *Mysqld) GetPrimaryKeyColumns(dbName, table string) ([]string, error) { conn, err := mysqld.dbaPool.Get(0) if err != nil { return nil, err } defer conn.Recycle() qr, err := conn.ExecuteFetch(fmt.Sprintf("show index from %v.%v", dbName, table), 100, true) if err != nil { return nil, err } keyNameIndex := -1 seqInIndexIndex := -1 columnNameIndex := -1 for i, field := range qr.Fields { switch field.Name { case "Key_name": keyNameIndex = i case "Seq_in_index": seqInIndexIndex = i case "Column_name": columnNameIndex = i } } if keyNameIndex == -1 || seqInIndexIndex == -1 || columnNameIndex == -1 { return nil, fmt.Errorf("Unknown columns in 'show index' result: %v", qr.Fields) } columns := make([]string, 0, 5) var expectedIndex int64 = 1 for _, row := range qr.Rows { // skip non-primary keys if row[keyNameIndex].String() != "PRIMARY" { continue } // check the Seq_in_index is always increasing seqInIndex, err := row[seqInIndexIndex].ParseInt64() if err != nil { return nil, err } if seqInIndex != expectedIndex { return nil, fmt.Errorf("Unexpected index: %v != %v", seqInIndex, expectedIndex) } expectedIndex++ columns = append(columns, row[columnNameIndex].String()) } return columns, err } // PreflightSchemaChange will apply the schema change to a fake // database that has the same schema as the target database, see if it // works. func (mysqld *Mysqld) PreflightSchemaChange(dbName string, change string) (*tmutils.SchemaChangeResult, error) { // gather current schema on real database beforeSchema, err := mysqld.GetSchema(dbName, nil, nil, true) if err != nil { return nil, err } // 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 beforeSchema.TableDefinitions { if td.Type == tmutils.TableBaseTable { sql += td.Schema + ";\n" } } if err = mysqld.executeMysqlCommands(mysqld.dba.Uname, sql); err != nil { return nil, err } // apply schema change to the temporary database sql = "SET sql_log_bin = 0;\n" sql += "USE _vt_preflight;\n" sql += change if err = mysqld.executeMysqlCommands(mysqld.dba.Uname, sql); err != nil { return nil, err } // get the result afterSchema, err := mysqld.GetSchema("_vt_preflight", nil, nil, true) if err != nil { return nil, err } // and clean up the extra database sql = "SET sql_log_bin = 0;\n" sql += "DROP DATABASE _vt_preflight;\n" if err = mysqld.executeMysqlCommands(mysqld.dba.Uname, sql); err != nil { return nil, err } return &tmutils.SchemaChangeResult{BeforeSchema: beforeSchema, AfterSchema: afterSchema}, nil } // ApplySchemaChange will apply the schema change to the given database. func (mysqld *Mysqld) ApplySchemaChange(dbName string, change *tmutils.SchemaChange) (*tmutils.SchemaChangeResult, error) { // check current schema matches beforeSchema, err := mysqld.GetSchema(dbName, nil, nil, false) if err != nil { return nil, err } if change.BeforeSchema != nil { schemaDiffs := tmutils.DiffSchemaToArray("actual", beforeSchema, "expected", change.BeforeSchema) if len(schemaDiffs) > 0 { for _, msg := range schemaDiffs { log.Warningf("BeforeSchema differs: %v", msg) } // let's see if the schema was already applied if change.AfterSchema != nil { schemaDiffs = tmutils.DiffSchemaToArray("actual", beforeSchema, "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 return &tmutils.SchemaChangeResult{ BeforeSchema: beforeSchema, AfterSchema: beforeSchema}, nil } } if change.Force { log.Warningf("BeforeSchema differs, applying anyway") } else { return nil, fmt.Errorf("BeforeSchema differs") } } } sql := change.SQL if !change.AllowReplication { sql = "SET sql_log_bin = 0;\n" + sql } // add a 'use XXX' in front of the SQL sql = "USE " + dbName + ";\n" + sql // execute the schema change using an external mysql process // (to benefit from the extra commands in mysql cli) if err = mysqld.executeMysqlCommands(mysqld.dba.Uname, sql); err != nil { return nil, err } // get AfterSchema afterSchema, err := mysqld.GetSchema(dbName, nil, nil, false) if err != nil { return nil, err } // compare to the provided AfterSchema if change.AfterSchema != nil { schemaDiffs := tmutils.DiffSchemaToArray("actual", afterSchema, "expected", change.AfterSchema) if len(schemaDiffs) > 0 { for _, msg := range schemaDiffs { log.Warningf("AfterSchema differs: %v", msg) } if change.Force { log.Warningf("AfterSchema differs, not reporting error") } else { return nil, fmt.Errorf("AfterSchema differs") } } } return &tmutils.SchemaChangeResult{BeforeSchema: beforeSchema, AfterSchema: afterSchema}, nil }