зеркало из https://github.com/github/vitess-gh.git
321 строка
9.2 KiB
Go
321 строка
9.2 KiB
Go
// 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
|
|
}
|