vitess.io/vitess@v0.16.2/go/vt/mysqlctl/tmutils/schema_test.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 tmutils 18 19 import ( 20 "errors" 21 "fmt" 22 "testing" 23 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 "google.golang.org/protobuf/proto" 27 28 tabletmanagerdatapb "vitess.io/vitess/go/vt/proto/tabletmanagerdata" 29 ) 30 31 var basicTable1 = &tabletmanagerdatapb.TableDefinition{ 32 Name: "table1", 33 Schema: "table schema 1", 34 Type: TableBaseTable, 35 } 36 var basicTable2 = &tabletmanagerdatapb.TableDefinition{ 37 Name: "table2", 38 Schema: "table schema 2", 39 Type: TableBaseTable, 40 } 41 42 var table3 = &tabletmanagerdatapb.TableDefinition{ 43 Name: "table2", 44 Schema: "CREATE TABLE `table3` (\n" + 45 "id bigint not null,\n" + 46 ") Engine=InnoDB", 47 Type: TableBaseTable, 48 } 49 50 var view1 = &tabletmanagerdatapb.TableDefinition{ 51 Name: "view1", 52 Schema: "view schema 1", 53 Type: TableView, 54 } 55 56 var view2 = &tabletmanagerdatapb.TableDefinition{ 57 Name: "view2", 58 Schema: "view schema 2", 59 Type: TableView, 60 } 61 62 func TestToSQLStrings(t *testing.T) { 63 var testcases = []struct { 64 input *tabletmanagerdatapb.SchemaDefinition 65 want []string 66 }{ 67 { 68 // basic SchemaDefinition with create db statement, basic table and basic view 69 input: &tabletmanagerdatapb.SchemaDefinition{ 70 DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", 71 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 72 basicTable1, 73 view1, 74 }, 75 }, 76 want: []string{"CREATE DATABASE `{{.DatabaseName}}`", basicTable1.Schema, view1.Schema}, 77 }, 78 { 79 // SchemaDefinition doesn't need any tables or views 80 input: &tabletmanagerdatapb.SchemaDefinition{ 81 DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", 82 }, 83 want: []string{"CREATE DATABASE `{{.DatabaseName}}`"}, 84 }, 85 { 86 // and can even have an empty DatabaseSchema 87 input: &tabletmanagerdatapb.SchemaDefinition{}, 88 want: []string{""}, 89 }, 90 { 91 // with tables but no views 92 input: &tabletmanagerdatapb.SchemaDefinition{ 93 DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", 94 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 95 basicTable1, 96 basicTable2, 97 }, 98 }, 99 want: []string{"CREATE DATABASE `{{.DatabaseName}}`", basicTable1.Schema, basicTable2.Schema}, 100 }, 101 { 102 // multiple tables and views should be ordered with all tables before views 103 input: &tabletmanagerdatapb.SchemaDefinition{ 104 DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", 105 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 106 view1, 107 view2, 108 basicTable1, 109 basicTable2, 110 }, 111 }, 112 want: []string{ 113 "CREATE DATABASE `{{.DatabaseName}}`", 114 basicTable1.Schema, basicTable2.Schema, 115 view1.Schema, view2.Schema, 116 }, 117 }, 118 { 119 // valid table schema gets correctly rewritten to include DatabaseName 120 input: &tabletmanagerdatapb.SchemaDefinition{ 121 DatabaseSchema: "CREATE DATABASE {{.DatabaseName}}", 122 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 123 basicTable1, 124 table3, 125 }, 126 }, 127 want: []string{ 128 "CREATE DATABASE `{{.DatabaseName}}`", 129 basicTable1.Schema, 130 "CREATE TABLE `{{.DatabaseName}}`.`table3` (\n" + 131 "id bigint not null,\n" + 132 ") Engine=InnoDB", 133 }, 134 }, 135 } 136 137 for _, tc := range testcases { 138 got := SchemaDefinitionToSQLStrings(tc.input) 139 assert.Equal(t, tc.want, got) 140 } 141 } 142 143 func testDiff(t *testing.T, left, right *tabletmanagerdatapb.SchemaDefinition, leftName, rightName string, expected []string) { 144 t.Helper() 145 146 actual := DiffSchemaToArray(leftName, left, rightName, right) 147 148 equal := false 149 if len(actual) == len(expected) { 150 equal = true 151 for i, val := range actual { 152 if val != expected[i] { 153 equal = false 154 break 155 } 156 } 157 } 158 assert.Truef(t, equal, "expected: %v, actual: %v", expected, actual) 159 } 160 161 func TestSchemaDiff(t *testing.T) { 162 sd1 := &tabletmanagerdatapb.SchemaDefinition{ 163 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 164 { 165 Name: "table1", 166 Schema: "schema1", 167 Type: TableBaseTable, 168 }, 169 { 170 Name: "table2", 171 Schema: "schema2", 172 Type: TableBaseTable, 173 }, 174 }, 175 } 176 177 sd2 := &tabletmanagerdatapb.SchemaDefinition{TableDefinitions: make([]*tabletmanagerdatapb.TableDefinition, 0, 2)} 178 179 sd3 := &tabletmanagerdatapb.SchemaDefinition{ 180 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 181 { 182 Name: "table2", 183 Schema: "schema2", 184 Type: TableBaseTable, 185 }, 186 }, 187 } 188 189 sd4 := &tabletmanagerdatapb.SchemaDefinition{ 190 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 191 { 192 Name: "table2", 193 Schema: "table2", 194 Type: TableView, 195 }, 196 }, 197 } 198 199 sd5 := &tabletmanagerdatapb.SchemaDefinition{ 200 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 201 { 202 Name: "table2", 203 Schema: "table2", 204 Type: TableBaseTable, 205 }, 206 }, 207 } 208 209 testDiff(t, sd1, sd1, "sd1", "sd2", []string{}) 210 211 testDiff(t, sd2, sd2, "sd2", "sd2", []string{}) 212 213 // two schemas are considered the same if both nil 214 testDiff(t, nil, nil, "sd1", "sd2", nil) 215 216 testDiff(t, sd1, nil, "sd1", "sd2", []string{ 217 fmt.Sprintf("schemas are different:\nsd1: %v, sd2: <nil>", sd1), 218 }) 219 220 testDiff(t, sd1, sd3, "sd1", "sd3", []string{ 221 "sd1 has an extra table named table1", 222 }) 223 224 testDiff(t, sd3, sd1, "sd3", "sd1", []string{ 225 "sd1 has an extra table named table1", 226 }) 227 228 testDiff(t, sd2, sd4, "sd2", "sd4", []string{ 229 "sd4 has an extra view named table2", 230 }) 231 232 testDiff(t, sd4, sd2, "sd4", "sd2", []string{ 233 "sd4 has an extra view named table2", 234 }) 235 236 testDiff(t, sd4, sd5, "sd4", "sd5", []string{ 237 fmt.Sprintf("schemas differ on table type for table table2:\nsd4: VIEW\n differs from:\nsd5: BASE TABLE"), //nolint 238 }) 239 240 sd1.DatabaseSchema = "CREATE DATABASE {{.DatabaseName}}" 241 sd2.DatabaseSchema = "DONT CREATE DATABASE {{.DatabaseName}}" 242 testDiff(t, sd1, sd2, "sd1", "sd2", []string{"schemas are different:\nsd1: CREATE DATABASE {{.DatabaseName}}\n differs from:\nsd2: DONT CREATE DATABASE {{.DatabaseName}}", "sd1 has an extra table named table1", "sd1 has an extra table named table2"}) 243 sd2.DatabaseSchema = "CREATE DATABASE {{.DatabaseName}}" 244 testDiff(t, sd2, sd1, "sd2", "sd1", []string{"sd1 has an extra table named table1", "sd1 has an extra table named table2"}) 245 246 sd2.TableDefinitions = append(sd2.TableDefinitions, &tabletmanagerdatapb.TableDefinition{Name: "table1", Schema: "schema1", Type: TableBaseTable}) 247 testDiff(t, sd1, sd2, "sd1", "sd2", []string{"sd1 has an extra table named table2"}) 248 249 sd2.TableDefinitions = append(sd2.TableDefinitions, &tabletmanagerdatapb.TableDefinition{Name: "table2", Schema: "schema3", Type: TableBaseTable}) 250 testDiff(t, sd1, sd2, "sd1", "sd2", []string{"schemas differ on table table2:\nsd1: schema2\n differs from:\nsd2: schema3"}) 251 } 252 253 func TestTableFilter(t *testing.T) { 254 includedTable := "t1" 255 includedTable2 := "t2" 256 excludedTable := "e1" 257 view := "v1" 258 259 includedTableRE := "/t.*/" 260 excludedTableRE := "/e.*/" 261 262 tcs := []struct { 263 desc string 264 tables []string 265 excludeTables []string 266 includeViews bool 267 268 tableName string 269 tableType string 270 271 hasErr bool 272 included bool 273 }{ 274 { 275 desc: "everything allowed includes table", 276 includeViews: true, 277 278 tableName: includedTable, 279 tableType: TableBaseTable, 280 281 included: true, 282 }, 283 { 284 desc: "everything allowed includes view", 285 includeViews: true, 286 287 tableName: view, 288 tableType: TableView, 289 290 included: true, 291 }, 292 { 293 desc: "table list includes matching 1st table", 294 tables: []string{includedTable, includedTable2}, 295 includeViews: true, 296 297 tableName: includedTable, 298 tableType: TableBaseTable, 299 300 included: true, 301 }, 302 { 303 desc: "table list includes matching 2nd table", 304 tables: []string{includedTable, includedTable2}, 305 includeViews: true, 306 307 tableName: includedTable2, 308 tableType: TableBaseTable, 309 310 included: true, 311 }, 312 { 313 desc: "table list excludes non-matching table", 314 tables: []string{includedTable, includedTable2}, 315 includeViews: true, 316 317 tableName: excludedTable, 318 tableType: TableBaseTable, 319 320 included: false, 321 }, 322 { 323 desc: "table list include view includes matching view", 324 tables: []string{view}, 325 includeViews: true, 326 327 tableName: view, 328 tableType: TableView, 329 330 included: true, 331 }, 332 { 333 desc: "table list exclude view excludes matching view", 334 tables: []string{view}, 335 includeViews: false, 336 337 tableName: view, 338 tableType: TableView, 339 340 included: false, 341 }, 342 { 343 desc: "table regexp list includes matching table", 344 tables: []string{includedTableRE}, 345 includeViews: false, 346 347 tableName: includedTable, 348 tableType: TableBaseTable, 349 350 included: true, 351 }, 352 { 353 desc: "exclude table list excludes matching table", 354 excludeTables: []string{excludedTable}, 355 356 tableName: excludedTable, 357 tableType: TableBaseTable, 358 359 included: false, 360 }, 361 { 362 desc: "exclude table list includes non-matching table", 363 excludeTables: []string{excludedTable}, 364 365 tableName: includedTable, 366 tableType: TableBaseTable, 367 368 included: true, 369 }, 370 { 371 desc: "exclude table list includes non-matching view", 372 excludeTables: []string{excludedTable}, 373 includeViews: true, 374 375 tableName: view, 376 tableType: TableView, 377 378 included: true, 379 }, 380 { 381 desc: "exclude table list excludes matching view", 382 excludeTables: []string{excludedTable}, 383 includeViews: true, 384 385 tableName: excludedTable, 386 tableType: TableView, 387 388 included: false, 389 }, 390 { 391 desc: "exclude table list excludes matching view", 392 excludeTables: []string{excludedTable}, 393 includeViews: true, 394 395 tableName: excludedTable, 396 tableType: TableView, 397 398 included: false, 399 }, 400 { 401 desc: "exclude table regexp list excludes matching table", 402 excludeTables: []string{excludedTableRE}, 403 includeViews: false, 404 405 tableName: excludedTable, 406 tableType: TableBaseTable, 407 408 included: false, 409 }, 410 { 411 desc: "table list with excludes includes matching table", 412 tables: []string{includedTable}, 413 excludeTables: []string{excludedTable}, 414 415 tableName: includedTable, 416 tableType: TableBaseTable, 417 418 included: true, 419 }, 420 { 421 desc: "table list with excludes excludes matching excluded table", 422 tables: []string{includedTable}, 423 excludeTables: []string{excludedTable}, 424 425 tableName: excludedTable, 426 tableType: TableBaseTable, 427 428 included: false, 429 }, 430 { 431 desc: "exclude table list does not list table", 432 excludeTables: []string{"nomatch1", "nomatch2", "/nomatch3/", "/nomatch4/", "/nomatch5/"}, 433 includeViews: true, 434 435 tableName: excludedTable, 436 tableType: TableBaseTable, 437 438 included: true, 439 }, 440 { 441 desc: "exclude table list with re match", 442 excludeTables: []string{"nomatch1", "nomatch2", "/nomatch3/", "/" + excludedTable + "/", "/nomatch5/"}, 443 includeViews: true, 444 445 tableName: excludedTable, 446 tableType: TableBaseTable, 447 448 included: false, 449 }, 450 { 451 desc: "bad table regexp", 452 tables: []string{"/*/"}, 453 454 hasErr: true, 455 }, 456 { 457 desc: "bad exclude table regexp", 458 excludeTables: []string{"/*/"}, 459 460 hasErr: true, 461 }, 462 } 463 464 for _, tc := range tcs { 465 t.Run(tc.desc, func(t *testing.T) { 466 f, err := NewTableFilter(tc.tables, tc.excludeTables, tc.includeViews) 467 if tc.hasErr { 468 assert.Error(t, err) 469 return 470 } 471 assert.NoError(t, err) 472 473 assert.Equal(t, len(tc.tables), len(f.tableNames)+len(f.tableREs)) 474 assert.Equal(t, len(tc.excludeTables), len(f.excludeTableNames)+len(f.excludeTableREs)) 475 included := f.Includes(tc.tableName, tc.tableType) 476 assert.Equalf(t, tc.included, included, "filter: %v", f) 477 }) 478 } 479 } 480 481 func TestFilterTables(t *testing.T) { 482 var testcases = []struct { 483 desc string 484 input *tabletmanagerdatapb.SchemaDefinition 485 tables []string 486 excludeTables []string 487 includeViews bool 488 want *tabletmanagerdatapb.SchemaDefinition 489 wantError error 490 }{ 491 { 492 desc: "filter based on tables (whitelist)", 493 input: &tabletmanagerdatapb.SchemaDefinition{ 494 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 495 basicTable1, 496 basicTable2, 497 }, 498 }, 499 tables: []string{basicTable1.Name}, 500 want: &tabletmanagerdatapb.SchemaDefinition{ 501 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 502 basicTable1, 503 }, 504 }, 505 }, 506 { 507 desc: "filter based on excludeTables (denylist)", 508 input: &tabletmanagerdatapb.SchemaDefinition{ 509 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 510 basicTable1, 511 basicTable2, 512 }, 513 }, 514 excludeTables: []string{basicTable1.Name}, 515 want: &tabletmanagerdatapb.SchemaDefinition{ 516 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 517 basicTable2, 518 }, 519 }, 520 }, 521 { 522 desc: "excludeTables may filter out a whitelisted item from tables", 523 input: &tabletmanagerdatapb.SchemaDefinition{ 524 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 525 basicTable1, 526 basicTable2, 527 }, 528 }, 529 tables: []string{basicTable1.Name, basicTable2.Name}, 530 excludeTables: []string{basicTable1.Name}, 531 want: &tabletmanagerdatapb.SchemaDefinition{ 532 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 533 basicTable2, 534 }, 535 }, 536 }, 537 { 538 desc: "exclude views", 539 input: &tabletmanagerdatapb.SchemaDefinition{ 540 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 541 basicTable1, 542 basicTable2, 543 view1, 544 }, 545 }, 546 includeViews: false, 547 want: &tabletmanagerdatapb.SchemaDefinition{ 548 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 549 basicTable1, 550 basicTable2, 551 }, 552 }, 553 }, 554 { 555 desc: "update schema version hash when list of tables has changed", 556 input: &tabletmanagerdatapb.SchemaDefinition{ 557 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 558 basicTable1, 559 basicTable2, 560 }, 561 Version: "dummy-version", 562 }, 563 excludeTables: []string{basicTable1.Name}, 564 want: &tabletmanagerdatapb.SchemaDefinition{ 565 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 566 basicTable2, 567 }, 568 Version: "6d1d294def9febdb21b35dd19a1dd4c6", 569 }, 570 }, 571 { 572 desc: "invalid regex for tables returns an error", 573 input: &tabletmanagerdatapb.SchemaDefinition{ 574 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 575 basicTable1, 576 }, 577 }, 578 tables: []string{"/(/"}, 579 wantError: errors.New("cannot compile regexp ( for table: error parsing regexp: missing closing ): `(`"), 580 }, 581 { 582 desc: "invalid regex for excludeTables returns an error", 583 input: &tabletmanagerdatapb.SchemaDefinition{ 584 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 585 basicTable1, 586 }, 587 }, 588 excludeTables: []string{"/(/"}, 589 wantError: errors.New("cannot compile regexp ( for excludeTable: error parsing regexp: missing closing ): `(`"), 590 }, 591 { 592 desc: "table substring doesn't match without regexp (include)", 593 input: &tabletmanagerdatapb.SchemaDefinition{ 594 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 595 basicTable1, 596 basicTable2, 597 }, 598 }, 599 tables: []string{basicTable1.Name[1:]}, 600 want: &tabletmanagerdatapb.SchemaDefinition{ 601 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{}, 602 }, 603 }, 604 { 605 desc: "table substring matches with regexp (include)", 606 input: &tabletmanagerdatapb.SchemaDefinition{ 607 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 608 basicTable1, 609 basicTable2, 610 }, 611 }, 612 tables: []string{"/" + basicTable1.Name[1:] + "/"}, 613 want: &tabletmanagerdatapb.SchemaDefinition{ 614 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 615 basicTable1, 616 }, 617 }, 618 }, 619 { 620 desc: "table substring doesn't match without regexp (exclude)", 621 input: &tabletmanagerdatapb.SchemaDefinition{ 622 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 623 basicTable1, 624 basicTable2, 625 }, 626 }, 627 excludeTables: []string{basicTable1.Name[1:]}, 628 want: &tabletmanagerdatapb.SchemaDefinition{ 629 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 630 basicTable1, 631 basicTable2, 632 }, 633 }, 634 }, 635 { 636 desc: "table substring matches with regexp (exclude)", 637 input: &tabletmanagerdatapb.SchemaDefinition{ 638 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 639 basicTable1, 640 basicTable2, 641 }, 642 }, 643 excludeTables: []string{"/" + basicTable1.Name[1:] + "/"}, 644 want: &tabletmanagerdatapb.SchemaDefinition{ 645 TableDefinitions: []*tabletmanagerdatapb.TableDefinition{ 646 basicTable2, 647 }, 648 }, 649 }, 650 } 651 652 for _, tc := range testcases { 653 t.Run(tc.desc, func(t *testing.T) { 654 got, err := FilterTables(tc.input, tc.tables, tc.excludeTables, tc.includeViews) 655 if tc.wantError != nil { 656 require.Error(t, err) 657 require.Equal(t, tc.wantError, err) 658 } else { 659 assert.NoError(t, err) 660 assert.Truef(t, proto.Equal(tc.want, got), "wanted: %v, got: %v", tc.want, got) 661 } 662 }) 663 } 664 }