vitess.io/vitess@v0.16.2/go/vt/mysqlctl/schema.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mysqlctl
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  	"sync"
    26  
    27  	"vitess.io/vitess/go/mysql"
    28  	"vitess.io/vitess/go/sqltypes"
    29  	"vitess.io/vitess/go/vt/concurrency"
    30  	"vitess.io/vitess/go/vt/vterrors"
    31  	"vitess.io/vitess/go/vt/vtgate/evalengine"
    32  
    33  	"vitess.io/vitess/go/sqlescape"
    34  	"vitess.io/vitess/go/vt/log"
    35  	"vitess.io/vitess/go/vt/mysqlctl/tmutils"
    36  
    37  	querypb "vitess.io/vitess/go/vt/proto/query"
    38  	tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata"
    39  	"vitess.io/vitess/go/vt/proto/vtrpc"
    40  )
    41  
    42  var autoIncr = regexp.MustCompile(` AUTO_INCREMENT=\d+`)
    43  
    44  // executeSchemaCommands executes some SQL commands, using the mysql
    45  // command line tool. It uses the dba connection parameters, with credentials.
    46  func (mysqld *Mysqld) executeSchemaCommands(sql string) error {
    47  	params, err := mysqld.dbcfgs.DbaConnector().MysqlParams()
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	return mysqld.executeMysqlScript(params, strings.NewReader(sql))
    53  }
    54  
    55  func encodeTableName(tableName string) string {
    56  	var buf strings.Builder
    57  	sqltypes.NewVarChar(tableName).EncodeSQL(&buf)
    58  	return buf.String()
    59  }
    60  
    61  // tableListSQL returns an IN clause "('t1', 't2'...) for a list of tables."
    62  func tableListSQL(tables []string) (string, error) {
    63  	if len(tables) == 0 {
    64  		return "", vterrors.New(vtrpc.Code_INTERNAL, "no tables for tableListSQL")
    65  	}
    66  
    67  	encodedTables := make([]string, len(tables))
    68  	for i, tableName := range tables {
    69  		encodedTables[i] = encodeTableName(tableName)
    70  	}
    71  
    72  	return "(" + strings.Join(encodedTables, ", ") + ")", nil
    73  }
    74  
    75  // GetSchema returns the schema for database for tables listed in
    76  // tables. If tables is empty, return the schema for all tables.
    77  func (mysqld *Mysqld) GetSchema(ctx context.Context, dbName string, request *tabletmanagerdatapb.GetSchemaRequest) (*tabletmanagerdatapb.SchemaDefinition, error) {
    78  	sd := &tabletmanagerdatapb.SchemaDefinition{}
    79  	backtickDBName := sqlescape.EscapeID(dbName)
    80  
    81  	// get the database creation command
    82  	qr, fetchErr := mysqld.FetchSuperQuery(ctx, fmt.Sprintf("SHOW CREATE DATABASE IF NOT EXISTS %s", backtickDBName))
    83  	if fetchErr != nil {
    84  		return nil, fetchErr
    85  	}
    86  	if len(qr.Rows) == 0 {
    87  		return nil, fmt.Errorf("empty create database statement for %v", dbName)
    88  	}
    89  	sd.DatabaseSchema = strings.Replace(qr.Rows[0][1].ToString(), backtickDBName, "{{.DatabaseName}}", 1)
    90  
    91  	tds, err := mysqld.collectBasicTableData(ctx, dbName, request.Tables, request.ExcludeTables, request.IncludeViews)
    92  	if err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	ctx, cancel := context.WithCancel(ctx)
    97  	defer cancel()
    98  
    99  	var wg sync.WaitGroup
   100  	allErrors := &concurrency.AllErrorRecorder{}
   101  
   102  	// Get per-table schema concurrently.
   103  	tableNames := make([]string, 0, len(tds))
   104  	for _, td := range tds {
   105  		tableNames = append(tableNames, td.Name)
   106  
   107  		wg.Add(1)
   108  		go func(td *tabletmanagerdatapb.TableDefinition) {
   109  			defer wg.Done()
   110  
   111  			fields, columns, schema, err := mysqld.collectSchema(ctx, dbName, td.Name, td.Type, request.TableSchemaOnly)
   112  			if err != nil {
   113  				// There's a possible race condition: it could happen that a table was dropped in between reading
   114  				// the list of tables (collectBasicTableData(), earlier) and the point above where we investigate
   115  				// the table.
   116  				// This is fine. We identify the situation and keep the table without any fields/columns/key information
   117  				sqlErr, isSQLErr := mysql.NewSQLErrorFromError(err).(*mysql.SQLError)
   118  				if isSQLErr && sqlErr != nil && sqlErr.Number() == mysql.ERNoSuchTable {
   119  					return
   120  				}
   121  
   122  				allErrors.RecordError(err)
   123  				cancel()
   124  				return
   125  			}
   126  
   127  			td.Fields = fields
   128  			td.Columns = columns
   129  			td.Schema = schema
   130  		}(td)
   131  	}
   132  
   133  	// Get primary columns concurrently.
   134  	// The below runs a single query on `INFORMATION_SCHEMA` and does not interact with the actual tables.
   135  	// It is therefore safe to run even if some tables are dropped in the interim.
   136  	colMap := map[string][]string{}
   137  	if len(tableNames) > 0 {
   138  		wg.Add(1)
   139  		go func() {
   140  			defer wg.Done()
   141  
   142  			var err error
   143  			colMap, err = mysqld.getPrimaryKeyColumns(ctx, dbName, tableNames...)
   144  			if err != nil {
   145  				allErrors.RecordError(err)
   146  				cancel()
   147  				return
   148  			}
   149  		}()
   150  	}
   151  
   152  	wg.Wait()
   153  	if err := allErrors.AggrError(vterrors.Aggregate); err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	for _, td := range tds {
   158  		td.PrimaryKeyColumns = colMap[td.Name]
   159  	}
   160  
   161  	sd.TableDefinitions = tds
   162  
   163  	tmutils.GenerateSchemaVersion(sd)
   164  	return sd, nil
   165  }
   166  
   167  func (mysqld *Mysqld) collectBasicTableData(ctx context.Context, dbName string, tables, excludeTables []string, includeViews bool) ([]*tabletmanagerdatapb.TableDefinition, error) {
   168  	// get the list of tables we're interested in
   169  	sql := "SELECT table_name, table_type, data_length, table_rows FROM information_schema.tables WHERE table_schema = '" + dbName + "'"
   170  	if !includeViews {
   171  		sql += " AND table_type = '" + tmutils.TableBaseTable + "'"
   172  	}
   173  	qr, err := mysqld.FetchSuperQuery(ctx, sql)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	if len(qr.Rows) == 0 {
   178  		return nil, nil
   179  	}
   180  
   181  	filter, err := tmutils.NewTableFilter(tables, excludeTables, includeViews)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	tds := make(tableDefinitions, 0, len(qr.Rows))
   187  	for _, row := range qr.Rows {
   188  		tableName := row[0].ToString()
   189  		tableType := row[1].ToString()
   190  
   191  		if !filter.Includes(tableName, tableType) {
   192  			continue
   193  		}
   194  
   195  		// compute dataLength
   196  		var dataLength uint64
   197  		if !row[2].IsNull() {
   198  			// dataLength is NULL for views, then we use 0
   199  			dataLength, err = evalengine.ToUint64(row[2])
   200  			if err != nil {
   201  				return nil, err
   202  			}
   203  		}
   204  
   205  		// get row count
   206  		var rowCount uint64
   207  		if !row[3].IsNull() {
   208  			rowCount, err = evalengine.ToUint64(row[3])
   209  			if err != nil {
   210  				return nil, err
   211  			}
   212  		}
   213  
   214  		tds = append(tds, &tabletmanagerdatapb.TableDefinition{
   215  			Name:       tableName,
   216  			Type:       tableType,
   217  			DataLength: dataLength,
   218  			RowCount:   rowCount,
   219  		})
   220  	}
   221  
   222  	sort.Sort(tds)
   223  
   224  	return tds, nil
   225  }
   226  
   227  func (mysqld *Mysqld) collectSchema(ctx context.Context, dbName, tableName, tableType string, tableSchemaOnly bool) (fields []*querypb.Field, columns []string, schema string, err error) {
   228  	schema, err = mysqld.normalizedSchema(ctx, dbName, tableName, tableType)
   229  	if err != nil {
   230  		return nil, nil, "", err
   231  	}
   232  	if !tableSchemaOnly {
   233  		fields, columns, err = mysqld.GetColumns(ctx, dbName, tableName)
   234  		if err != nil {
   235  			return nil, nil, "", err
   236  		}
   237  	}
   238  
   239  	return fields, columns, schema, nil
   240  }
   241  
   242  // normalizedSchema returns a table schema with database names replaced, and auto_increment annotations removed.
   243  func (mysqld *Mysqld) normalizedSchema(ctx context.Context, dbName, tableName, tableType string) (string, error) {
   244  	backtickDBName := sqlescape.EscapeID(dbName)
   245  	qr, fetchErr := mysqld.FetchSuperQuery(ctx, fmt.Sprintf("SHOW CREATE TABLE %s.%s", backtickDBName, sqlescape.EscapeID(tableName)))
   246  	if fetchErr != nil {
   247  		return "", vterrors.Wrapf(fetchErr, "in Mysqld.normalizedSchema()")
   248  	}
   249  	if len(qr.Rows) == 0 {
   250  		return "", fmt.Errorf("empty create table statement for %v", tableName)
   251  	}
   252  
   253  	// Normalize & remove auto_increment because it changes on every insert
   254  	// FIXME(alainjobart) find a way to share this with
   255  	// vt/tabletserver/table_info.go:162
   256  	norm := qr.Rows[0][1].ToString()
   257  	norm = autoIncr.ReplaceAllLiteralString(norm, "")
   258  	if tableType == tmutils.TableView {
   259  		// Views will have the dbname in there, replace it
   260  		// with {{.DatabaseName}}
   261  		norm = strings.Replace(norm, backtickDBName, "{{.DatabaseName}}", -1)
   262  	}
   263  
   264  	return norm, nil
   265  }
   266  
   267  // ResolveTables returns a list of actual tables+views matching a list
   268  // of regexps
   269  func ResolveTables(ctx context.Context, mysqld MysqlDaemon, dbName string, tables []string) ([]string, error) {
   270  	req := &tabletmanagerdatapb.GetSchemaRequest{Tables: tables, IncludeViews: true, TableSchemaOnly: true}
   271  	sd, err := mysqld.GetSchema(ctx, dbName, req)
   272  	if err != nil {
   273  		return nil, err
   274  	}
   275  	result := make([]string, len(sd.TableDefinitions))
   276  	for i, td := range sd.TableDefinitions {
   277  		result[i] = td.Name
   278  	}
   279  	return result, nil
   280  }
   281  
   282  const (
   283  	GetColumnNamesQuery = `SELECT COLUMN_NAME as column_name
   284  		FROM INFORMATION_SCHEMA.COLUMNS
   285  		WHERE TABLE_SCHEMA = %s AND TABLE_NAME = '%s'
   286  		ORDER BY ORDINAL_POSITION`
   287  	GetFieldsQuery = "SELECT %s FROM %s WHERE 1 != 1"
   288  )
   289  
   290  func GetColumnsList(dbName, tableName string, exec func(string, int, bool) (*sqltypes.Result, error)) (string, error) {
   291  	var dbName2 string
   292  	if dbName == "" {
   293  		dbName2 = "database()"
   294  	} else {
   295  		dbName2 = fmt.Sprintf("'%s'", dbName)
   296  	}
   297  	query := fmt.Sprintf(GetColumnNamesQuery, dbName2, sqlescape.UnescapeID(tableName))
   298  	qr, err := exec(query, -1, true)
   299  	if err != nil {
   300  		return "", err
   301  	}
   302  	if qr == nil || len(qr.Rows) == 0 {
   303  		err = fmt.Errorf("unable to get columns for table %s.%s using query %s", dbName, tableName, query)
   304  		log.Errorf("%s", fmt.Errorf("unable to get columns for table %s.%s using query %s", dbName, tableName, query))
   305  		return "", err
   306  	}
   307  	selectColumns := ""
   308  
   309  	for _, row := range qr.Named().Rows {
   310  		col := row["column_name"].ToString()
   311  		if col == "" {
   312  			continue
   313  		}
   314  		if selectColumns != "" {
   315  			selectColumns += ", "
   316  		}
   317  		selectColumns += sqlescape.EscapeID(col)
   318  	}
   319  	return selectColumns, nil
   320  }
   321  
   322  func GetColumns(dbName, table string, exec func(string, int, bool) (*sqltypes.Result, error)) ([]*querypb.Field, []string, error) {
   323  	selectColumns, err := GetColumnsList(dbName, table, exec)
   324  	if err != nil {
   325  		return nil, nil, err
   326  	}
   327  	if selectColumns == "" {
   328  		selectColumns = "*"
   329  	}
   330  	tableSpec := sqlescape.EscapeID(sqlescape.UnescapeID(table))
   331  	if dbName != "" {
   332  		tableSpec = fmt.Sprintf("%s.%s", sqlescape.EscapeID(sqlescape.UnescapeID(dbName)), tableSpec)
   333  	}
   334  	query := fmt.Sprintf(GetFieldsQuery, selectColumns, tableSpec)
   335  	qr, err := exec(query, 0, true)
   336  	if err != nil {
   337  		return nil, nil, vterrors.Wrapf(err, "in Mysqld.GetColumns()")
   338  	}
   339  
   340  	columns := make([]string, len(qr.Fields))
   341  	for i, field := range qr.Fields {
   342  		columns[i] = field.Name
   343  	}
   344  	return qr.Fields, columns, nil
   345  }
   346  
   347  // GetColumns returns the columns of table.
   348  func (mysqld *Mysqld) GetColumns(ctx context.Context, dbName, table string) ([]*querypb.Field, []string, error) {
   349  	conn, err := getPoolReconnect(ctx, mysqld.dbaPool)
   350  	if err != nil {
   351  		return nil, nil, err
   352  	}
   353  	defer conn.Recycle()
   354  	return GetColumns(dbName, table, conn.ExecuteFetch)
   355  }
   356  
   357  // GetPrimaryKeyColumns returns the primary key columns of table.
   358  func (mysqld *Mysqld) GetPrimaryKeyColumns(ctx context.Context, dbName, table string) ([]string, error) {
   359  	cs, err := mysqld.getPrimaryKeyColumns(ctx, dbName, table)
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  
   364  	return cs[dbName], nil
   365  }
   366  
   367  func (mysqld *Mysqld) getPrimaryKeyColumns(ctx context.Context, dbName string, tables ...string) (map[string][]string, error) {
   368  	conn, err := getPoolReconnect(ctx, mysqld.dbaPool)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	defer conn.Recycle()
   373  
   374  	tableList, err := tableListSQL(tables)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  	// sql uses column name aliases to guarantee lower case sensitivity.
   379  	sql := `
   380              SELECT TABLE_NAME as table_name, COLUMN_NAME as column_name
   381              FROM information_schema.STATISTICS
   382              WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME IN %s AND LOWER(INDEX_NAME) = 'primary'
   383              ORDER BY table_name, SEQ_IN_INDEX`
   384  	sql = fmt.Sprintf(sql, dbName, tableList)
   385  	qr, err := conn.ExecuteFetch(sql, len(tables)*100, true)
   386  	if err != nil {
   387  		return nil, err
   388  	}
   389  
   390  	named := qr.Named()
   391  	colMap := map[string][]string{}
   392  	for _, row := range named.Rows {
   393  		tableName := row.AsString("table_name", "")
   394  		colMap[tableName] = append(colMap[tableName], row.AsString("column_name", ""))
   395  	}
   396  	return colMap, err
   397  }
   398  
   399  // PreflightSchemaChange checks the schema changes in "changes" by applying them
   400  // to an intermediate database that has the same schema as the target database.
   401  func (mysqld *Mysqld) PreflightSchemaChange(ctx context.Context, dbName string, changes []string) ([]*tabletmanagerdatapb.SchemaChangeResult, error) {
   402  	results := make([]*tabletmanagerdatapb.SchemaChangeResult, len(changes))
   403  
   404  	// Get current schema from the real database.
   405  	req := &tabletmanagerdatapb.GetSchemaRequest{IncludeViews: true, TableSchemaOnly: true}
   406  	originalSchema, err := mysqld.GetSchema(ctx, dbName, req)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	// Populate temporary database with it.
   412  	initialCopySQL := "SET sql_log_bin = 0;\n"
   413  	initialCopySQL += "DROP DATABASE IF EXISTS _vt_preflight;\n"
   414  	initialCopySQL += "CREATE DATABASE _vt_preflight;\n"
   415  	initialCopySQL += "USE _vt_preflight;\n"
   416  	// We're not smart enough to create the tables in a foreign-key-compatible way,
   417  	// so we temporarily disable foreign key checks while adding the existing tables.
   418  	initialCopySQL += "SET foreign_key_checks = 0;\n"
   419  	for _, td := range originalSchema.TableDefinitions {
   420  		if td.Type == tmutils.TableBaseTable {
   421  			initialCopySQL += td.Schema + ";\n"
   422  		}
   423  	}
   424  	for _, td := range originalSchema.TableDefinitions {
   425  		if td.Type == tmutils.TableView {
   426  			// Views will have {{.DatabaseName}} in there, replace
   427  			// it with _vt_preflight
   428  			s := strings.Replace(td.Schema, "{{.DatabaseName}}", "`_vt_preflight`", -1)
   429  			initialCopySQL += s + ";\n"
   430  		}
   431  	}
   432  	if err = mysqld.executeSchemaCommands(initialCopySQL); err != nil {
   433  		return nil, err
   434  	}
   435  
   436  	// For each change, record the schema before and after.
   437  	for i, change := range changes {
   438  		req := &tabletmanagerdatapb.GetSchemaRequest{IncludeViews: true}
   439  		beforeSchema, err := mysqld.GetSchema(ctx, "_vt_preflight", req)
   440  		if err != nil {
   441  			return nil, err
   442  		}
   443  
   444  		// apply schema change to the temporary database
   445  		sql := "SET sql_log_bin = 0;\n"
   446  		sql += "USE _vt_preflight;\n"
   447  		sql += change
   448  		if err = mysqld.executeSchemaCommands(sql); err != nil {
   449  			return nil, err
   450  		}
   451  
   452  		// get the result
   453  		afterSchema, err := mysqld.GetSchema(ctx, "_vt_preflight", req)
   454  		if err != nil {
   455  			return nil, err
   456  		}
   457  
   458  		results[i] = &tabletmanagerdatapb.SchemaChangeResult{BeforeSchema: beforeSchema, AfterSchema: afterSchema}
   459  	}
   460  
   461  	// and clean up the extra database
   462  	dropSQL := "SET sql_log_bin = 0;\n"
   463  	dropSQL += "DROP DATABASE _vt_preflight;\n"
   464  	if err = mysqld.executeSchemaCommands(dropSQL); err != nil {
   465  		return nil, err
   466  	}
   467  
   468  	return results, nil
   469  }
   470  
   471  // ApplySchemaChange will apply the schema change to the given database.
   472  func (mysqld *Mysqld) ApplySchemaChange(ctx context.Context, dbName string, change *tmutils.SchemaChange) (*tabletmanagerdatapb.SchemaChangeResult, error) {
   473  	// check current schema matches
   474  	req := &tabletmanagerdatapb.GetSchemaRequest{IncludeViews: true}
   475  	beforeSchema, err := mysqld.GetSchema(ctx, dbName, req)
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  	if change.BeforeSchema != nil {
   480  		schemaDiffs := tmutils.DiffSchemaToArray("actual", beforeSchema, "expected", change.BeforeSchema)
   481  		if len(schemaDiffs) > 0 {
   482  			for _, msg := range schemaDiffs {
   483  				log.Warningf("BeforeSchema differs: %v", msg)
   484  			}
   485  
   486  			// let's see if the schema was already applied
   487  			if change.AfterSchema != nil {
   488  				schemaDiffs = tmutils.DiffSchemaToArray("actual", beforeSchema, "expected", change.AfterSchema)
   489  				if len(schemaDiffs) == 0 {
   490  					// no diff between the schema we expect
   491  					// after the change and the current
   492  					// schema, we already applied it
   493  					return &tabletmanagerdatapb.SchemaChangeResult{
   494  						BeforeSchema: beforeSchema,
   495  						AfterSchema:  beforeSchema}, nil
   496  				}
   497  			}
   498  
   499  			if change.Force {
   500  				log.Warningf("BeforeSchema differs, applying anyway")
   501  			} else {
   502  				return nil, fmt.Errorf("BeforeSchema differs")
   503  			}
   504  		}
   505  	}
   506  
   507  	sql := change.SQL
   508  
   509  	// The session used is closed after applying the schema change so we do not need
   510  	// to worry about saving and restoring the session state here
   511  	if change.SQLMode != "" {
   512  		sql = fmt.Sprintf("SET @@session.sql_mode='%s';\n%s", change.SQLMode, sql)
   513  	}
   514  
   515  	if !change.AllowReplication {
   516  		sql = "SET sql_log_bin = 0;\n" + sql
   517  	}
   518  
   519  	// add a 'use XXX' in front of the SQL
   520  	sql = fmt.Sprintf("USE %s;\n%s", sqlescape.EscapeID(dbName), sql)
   521  
   522  	// execute the schema change using an external mysql process
   523  	// (to benefit from the extra commands in mysql cli)
   524  	if err = mysqld.executeSchemaCommands(sql); err != nil {
   525  		return nil, err
   526  	}
   527  
   528  	// get AfterSchema
   529  	afterSchema, err := mysqld.GetSchema(ctx, dbName, req)
   530  	if err != nil {
   531  		return nil, err
   532  	}
   533  
   534  	// compare to the provided AfterSchema
   535  	if change.AfterSchema != nil {
   536  		schemaDiffs := tmutils.DiffSchemaToArray("actual", afterSchema, "expected", change.AfterSchema)
   537  		if len(schemaDiffs) > 0 {
   538  			for _, msg := range schemaDiffs {
   539  				log.Warningf("AfterSchema differs: %v", msg)
   540  			}
   541  			if change.Force {
   542  				log.Warningf("AfterSchema differs, not reporting error")
   543  			} else {
   544  				return nil, fmt.Errorf("AfterSchema differs")
   545  			}
   546  		}
   547  	}
   548  
   549  	return &tabletmanagerdatapb.SchemaChangeResult{BeforeSchema: beforeSchema, AfterSchema: afterSchema}, nil
   550  }
   551  
   552  // GetPrimaryKeyEquivalentColumns can be used if the table has
   553  // no defined PRIMARY KEY. It will return the columns in a
   554  // viable PRIMARY KEY equivalent (PKE) -- a NON-NULL UNIQUE
   555  // KEY -- in the specified table. When multiple PKE indexes
   556  // are available it will attempt to choose the most efficient
   557  // one based on the column data types and the number of columns
   558  // in the index. See here for the data type storage sizes:
   559  //
   560  //	https://dev.mysql.com/doc/refman/en/storage-requirements.html
   561  //
   562  // If this function is used on a table that DOES have a
   563  // defined PRIMARY KEY then it may return the columns for
   564  // that index if it is likely the most efficient one amongst
   565  // the available PKE indexes on the table.
   566  func (mysqld *Mysqld) GetPrimaryKeyEquivalentColumns(ctx context.Context, dbName, table string) ([]string, error) {
   567  	conn, err := getPoolReconnect(ctx, mysqld.dbaPool)
   568  	if err != nil {
   569  		return nil, err
   570  	}
   571  	defer conn.Recycle()
   572  
   573  	// We use column name aliases to guarantee lower case for our named results.
   574  	sql := `
   575              SELECT COLUMN_NAME AS column_name FROM information_schema.STATISTICS AS index_cols INNER JOIN
   576              (
   577                  SELECT stats.INDEX_NAME, SUM(
   578                                                CASE LOWER(cols.DATA_TYPE)
   579                                                  WHEN 'enum' THEN 0
   580                                                  WHEN 'tinyint' THEN 1
   581                                                  WHEN 'year' THEN 2
   582                                                  WHEN 'smallint' THEN 3
   583                                                  WHEN 'date' THEN 4
   584                                                  WHEN 'mediumint' THEN 5
   585                                                  WHEN 'time' THEN 6
   586                                                  WHEN 'int' THEN 7
   587                                                  WHEN 'set' THEN 8
   588                                                  WHEN 'timestamp' THEN 9
   589                                                  WHEN 'bigint' THEN 10
   590                                                  WHEN 'float' THEN 11
   591                                                  WHEN 'double' THEN 12
   592                                                  WHEN 'decimal' THEN 13
   593                                                  WHEN 'datetime' THEN 14
   594                                                  WHEN 'binary' THEN 30
   595                                                  WHEN 'char' THEN 31
   596                                                  WHEN 'varbinary' THEN 60
   597                                                  WHEN 'varchar' THEN 61
   598                                                  WHEN 'tinyblob' THEN 80
   599                                                  WHEN 'tinytext' THEN 81
   600                                                  ELSE 1000
   601                                                END
   602                                              ) AS type_cost, COUNT(stats.COLUMN_NAME) AS col_count FROM information_schema.STATISTICS AS stats INNER JOIN
   603                    information_schema.COLUMNS AS cols ON stats.TABLE_SCHEMA = cols.TABLE_SCHEMA AND stats.TABLE_NAME = cols.TABLE_NAME AND stats.COLUMN_NAME = cols.COLUMN_NAME
   604                  WHERE stats.TABLE_SCHEMA = '%s' AND stats.TABLE_NAME = '%s' AND stats.INDEX_NAME NOT IN
   605                  (
   606                      SELECT DISTINCT INDEX_NAME FROM information_schema.STATISTICS
   607                      WHERE TABLE_SCHEMA = '%s' AND TABLE_NAME = '%s' AND (NON_UNIQUE = 1 OR NULLABLE = 'YES')
   608                  )
   609                  GROUP BY INDEX_NAME ORDER BY type_cost ASC, col_count ASC LIMIT 1
   610              ) AS pke ON index_cols.INDEX_NAME = pke.INDEX_NAME
   611              WHERE index_cols.TABLE_SCHEMA = '%s' AND index_cols.TABLE_NAME = '%s' AND NON_UNIQUE = 0 AND NULLABLE != 'YES'
   612              ORDER BY SEQ_IN_INDEX ASC`
   613  	sql = fmt.Sprintf(sql, dbName, table, dbName, table, dbName, table)
   614  	qr, err := conn.ExecuteFetch(sql, 1000, true)
   615  	if err != nil {
   616  		return nil, err
   617  	}
   618  
   619  	named := qr.Named()
   620  	cols := make([]string, len(qr.Rows))
   621  	for i, row := range named.Rows {
   622  		cols[i] = row.AsString("column_name", "")
   623  	}
   624  	return cols, err
   625  }
   626  
   627  // tableDefinitions is a sortable collection of table definitions
   628  type tableDefinitions []*tabletmanagerdatapb.TableDefinition
   629  
   630  func (t tableDefinitions) Len() int {
   631  	return len(t)
   632  }
   633  
   634  func (t tableDefinitions) Less(i, j int) bool {
   635  	return t[i].Name < t[j].Name
   636  }
   637  
   638  func (t tableDefinitions) Swap(i, j int) {
   639  	t[i], t[j] = t[j], t[i]
   640  }
   641  
   642  var _ sort.Interface = (tableDefinitions)(nil)