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)