github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/syncer/ddl_test.go (about) 1 // Copyright 2019 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package syncer 15 16 import ( 17 "errors" 18 "fmt" 19 "strings" 20 "testing" 21 22 "github.com/DATA-DOG/go-sqlmock" 23 "github.com/go-mysql-org/go-mysql/mysql" 24 . "github.com/pingcap/check" 25 "github.com/pingcap/tidb/pkg/parser" 26 "github.com/pingcap/tidb/pkg/parser/ast" 27 "github.com/pingcap/tidb/pkg/util/filter" 28 regexprrouter "github.com/pingcap/tidb/pkg/util/regexpr-router" 29 router "github.com/pingcap/tidb/pkg/util/table-router" 30 "github.com/pingcap/tiflow/dm/config" 31 "github.com/pingcap/tiflow/dm/config/dbconfig" 32 "github.com/pingcap/tiflow/dm/pkg/conn" 33 tcontext "github.com/pingcap/tiflow/dm/pkg/context" 34 "github.com/pingcap/tiflow/dm/pkg/log" 35 parserpkg "github.com/pingcap/tiflow/dm/pkg/parser" 36 "github.com/pingcap/tiflow/dm/pkg/terror" 37 "github.com/pingcap/tiflow/dm/syncer/metrics" 38 onlineddl "github.com/pingcap/tiflow/dm/syncer/online-ddl-tools" 39 "github.com/stretchr/testify/require" 40 "go.uber.org/zap" 41 ) 42 43 var _ = Suite(&testDDLSuite{}) 44 45 type testDDLSuite struct{} 46 47 func (s *testDDLSuite) newSubTaskCfg(dbCfg dbconfig.DBConfig) *config.SubTaskConfig { 48 return &config.SubTaskConfig{ 49 From: dbCfg, 50 To: dbCfg, 51 ServerID: 101, 52 MetaSchema: "test", 53 Name: "syncer_ut", 54 Mode: config.ModeIncrement, 55 Flavor: "mysql", 56 ShadowTableRules: []string{config.DefaultShadowTableRules}, 57 TrashTableRules: []string{config.DefaultTrashTableRules}, 58 } 59 } 60 61 func (s *testDDLSuite) TestAnsiQuotes(c *C) { 62 ansiQuotesCases := []string{ 63 "create database `test`", 64 "create table `test`.`test`(id int)", 65 "create table `test`.\"test\" (id int)", 66 "create table \"test\".`test` (id int)", 67 "create table \"test\".\"test\"", 68 "create table test.test (\"id\" int)", 69 "insert into test.test (\"id\") values('a')", 70 } 71 72 db, mock, err := sqlmock.New() 73 mock.ExpectQuery("SHOW VARIABLES LIKE"). 74 WillReturnRows(sqlmock.NewRows([]string{"Variable_name", "Value"}). 75 AddRow("sql_mode", "ANSI_QUOTES")) 76 c.Assert(err, IsNil) 77 78 parser, err := conn.GetParser(tcontext.Background(), conn.NewBaseDBForTest(db)) 79 c.Assert(err, IsNil) 80 81 for _, sql := range ansiQuotesCases { 82 _, err = parser.ParseOneStmt(sql, "", "") 83 c.Assert(err, IsNil) 84 } 85 } 86 87 func (s *testDDLSuite) TestDDLWithDashComments(c *C) { 88 sql := `-- 89 -- this is a comment. 90 -- 91 CREATE TABLE test.test_table_with_c (id int); 92 ` 93 parser := parser.New() 94 _, err := parserpkg.Parse(parser, sql, "", "") 95 c.Assert(err, IsNil) 96 } 97 98 func (s *testDDLSuite) TestCommentQuote(c *C) { 99 sql := "ALTER TABLE schemadb.ep_edu_course_message_auto_reply MODIFY answer JSON COMMENT '回复的内容-格式为list,有两个字段:\"answerType\"://''发送客服消息类型:1-文本消息,2-图片,3-图文链接''; answer:回复内容';" 100 expectedSQL := "ALTER TABLE `schemadb`.`ep_edu_course_message_auto_reply` MODIFY COLUMN `answer` JSON COMMENT '回复的内容-格式为list,有两个字段:\"answerType\"://''发送客服消息类型:1-文本消息,2-图片,3-图文链接''; answer:回复内容'" 101 102 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestCommentQuote"))) 103 testEC := &eventContext{ 104 tctx: tctx, 105 } 106 qec := &queryEventContext{ 107 eventContext: testEC, 108 ddlSchema: "schemadb", 109 originSQL: sql, 110 p: parser.New(), 111 } 112 stmt, err := parseOneStmt(qec) 113 c.Assert(err, IsNil) 114 115 qec.splitDDLs, err = parserpkg.SplitDDL(stmt, qec.ddlSchema) 116 c.Assert(err, IsNil) 117 118 syncer := NewSyncer(&config.SubTaskConfig{Flavor: mysql.MySQLFlavor}, nil, nil) 119 syncer.tctx = tctx 120 c.Assert(syncer.genRouter(), IsNil) 121 122 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 123 for _, sql := range qec.splitDDLs { 124 sqls, err := ddlWorker.processOneDDL(qec, sql) 125 c.Assert(err, IsNil) 126 qec.appliedDDLs = append(qec.appliedDDLs, sqls...) 127 } 128 c.Assert(len(qec.appliedDDLs), Equals, 1) 129 c.Assert(qec.appliedDDLs[0], Equals, expectedSQL) 130 } 131 132 func (s *testDDLSuite) TestResolveDDLSQL(c *C) { 133 // duplicate with pkg/parser 134 sqls := []string{ 135 "create schema `s1`", 136 "create schema if not exists `s1`", 137 "drop schema `s1`", 138 "drop schema if exists `s1`", 139 "drop table `s1`.`t1`", 140 "drop table `s1`.`t1`, `s2`.`t2`", 141 "drop table `s1`.`t1`, `s2`.`t2`, `xx`", 142 "create table `s1`.`t1` (id int)", 143 "create table `t1` (id int)", 144 "create table `t1` like `t2`", 145 "create table `s1`.`t1` like `t2`", 146 "create table `s1`.`t1` like `s1`.`t2`", 147 "create table `t1` like `xx`.`t2`", 148 "truncate table `t1`", 149 "truncate table `s1`.`t1`", 150 "rename table `s1`.`t1` to `s2`.`t2`", 151 "rename table `t1` to `t2`, `s1`.`t1` to `s1`.`t2`", 152 "drop index i1 on `s1`.`t1`", 153 "drop index i1 on `t1`", 154 "create index i1 on `t1`(`c1`)", 155 "create index i1 on `s1`.`t1`(`c1`)", 156 "alter table `t1` add column c1 int, drop column c2", 157 "alter table `s1`.`t1` add column c1 int, rename to `t2`, drop column c2", 158 "alter table `s1`.`t1` add column c1 int, rename to `xx`.`t2`, drop column c2", 159 } 160 161 expectedSQLs := [][]string{ 162 {"CREATE DATABASE IF NOT EXISTS `s1`"}, 163 {"CREATE DATABASE IF NOT EXISTS `s1`"}, 164 {"DROP DATABASE IF EXISTS `s1`"}, 165 {"DROP DATABASE IF EXISTS `s1`"}, 166 {"DROP TABLE IF EXISTS `s1`.`t1`"}, 167 {"DROP TABLE IF EXISTS `s1`.`t1`"}, 168 {"DROP TABLE IF EXISTS `s1`.`t1`"}, 169 {"CREATE TABLE IF NOT EXISTS `s1`.`t1` (`id` INT)"}, 170 {}, 171 {}, 172 {}, 173 {"CREATE TABLE IF NOT EXISTS `s1`.`t1` LIKE `s1`.`t2`"}, 174 {}, 175 {}, 176 {"TRUNCATE TABLE `s1`.`t1`"}, 177 {}, 178 {"RENAME TABLE `s1`.`t1` TO `s1`.`t2`"}, 179 {"DROP INDEX /*T! IF EXISTS */`i1` ON `s1`.`t1`"}, 180 {}, 181 {}, 182 {"CREATE INDEX `i1` ON `s1`.`t1` (`c1`)"}, 183 {}, 184 {"ALTER TABLE `s1`.`t1` ADD COLUMN `c1` INT"}, 185 {"ALTER TABLE `s1`.`t1` ADD COLUMN `c1` INT"}, 186 } 187 188 targetSQLs := [][]string{ 189 {"CREATE DATABASE IF NOT EXISTS `xs1`"}, 190 {"CREATE DATABASE IF NOT EXISTS `xs1`"}, 191 {"DROP DATABASE IF EXISTS `xs1`"}, 192 {"DROP DATABASE IF EXISTS `xs1`"}, 193 {"DROP TABLE IF EXISTS `xs1`.`t1`"}, 194 {"DROP TABLE IF EXISTS `xs1`.`t1`"}, 195 {"DROP TABLE IF EXISTS `xs1`.`t1`"}, 196 {"CREATE TABLE IF NOT EXISTS `xs1`.`t1` (`id` INT)"}, 197 {}, 198 {}, 199 {}, 200 {"CREATE TABLE IF NOT EXISTS `xs1`.`t1` LIKE `xs1`.`t2`"}, 201 {}, 202 {}, 203 {"TRUNCATE TABLE `xs1`.`t1`"}, 204 {}, 205 {"RENAME TABLE `xs1`.`t1` TO `xs1`.`t2`"}, 206 {"DROP INDEX /*T! IF EXISTS */`i1` ON `xs1`.`t1`"}, 207 {}, 208 {}, 209 {"CREATE INDEX `i1` ON `xs1`.`t1` (`c1`)"}, 210 {}, 211 {"ALTER TABLE `xs1`.`t1` ADD COLUMN `c1` INT"}, 212 {"ALTER TABLE `xs1`.`t1` ADD COLUMN `c1` INT"}, 213 } 214 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestResolveDDLSQL"))) 215 216 cfg := &config.SubTaskConfig{ 217 Flavor: mysql.MySQLFlavor, 218 BAList: &filter.Rules{ 219 DoDBs: []string{"s1"}, 220 }, 221 } 222 var err error 223 syncer := NewSyncer(cfg, nil, nil) 224 syncer.tctx = tctx 225 syncer.baList, err = filter.New(syncer.cfg.CaseSensitive, syncer.cfg.BAList) 226 syncer.metricsProxies = metrics.DefaultMetricsProxies.CacheForOneTask("task", "worker", "source") 227 c.Assert(err, IsNil) 228 229 syncer.tableRouter, err = regexprrouter.NewRegExprRouter(false, []*router.TableRule{ 230 { 231 SchemaPattern: "s1", 232 TargetSchema: "xs1", 233 }, 234 }) 235 c.Assert(err, IsNil) 236 237 testEC := &eventContext{ 238 tctx: tctx, 239 } 240 statusVars := []byte{4, 0, 0, 0, 0, 46, 0} 241 syncer.idAndCollationMap = map[int]string{46: "utf8mb4_bin"} 242 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 243 for i, sql := range sqls { 244 qec := &queryEventContext{ 245 eventContext: testEC, 246 ddlSchema: "test", 247 originSQL: sql, 248 appliedDDLs: make([]string, 0), 249 p: parser.New(), 250 eventStatusVars: statusVars, 251 } 252 stmt, err := parseOneStmt(qec) 253 c.Assert(err, IsNil) 254 255 qec.splitDDLs, err = parserpkg.SplitDDL(stmt, qec.ddlSchema) 256 c.Assert(err, IsNil) 257 for _, sql2 := range qec.splitDDLs { 258 sqls, err := ddlWorker.processOneDDL(qec, sql2) 259 c.Assert(err, IsNil) 260 for _, sql3 := range sqls { 261 if len(sql3) == 0 { 262 continue 263 } 264 qec.appliedDDLs = append(qec.appliedDDLs, sql3) 265 } 266 } 267 c.Assert(qec.appliedDDLs, DeepEquals, expectedSQLs[i]) 268 c.Assert(targetSQLs[i], HasLen, len(qec.appliedDDLs)) 269 for j, sql2 := range qec.appliedDDLs { 270 ddlInfo, err2 := ddlWorker.genDDLInfo(qec, sql2) 271 c.Assert(err2, IsNil) 272 c.Assert(targetSQLs[i][j], Equals, ddlInfo.routedDDL) 273 } 274 } 275 } 276 277 func (s *testDDLSuite) TestParseOneStmt(c *C) { 278 cases := []struct { 279 sql string 280 isDDL bool 281 hasError bool 282 }{ 283 { 284 sql: "FLUSH", 285 isDDL: false, 286 hasError: true, 287 }, 288 { 289 sql: "BEGIN", 290 isDDL: false, 291 hasError: false, 292 }, 293 { 294 sql: "CREATE TABLE do_db.do_table (c1 INT)", 295 isDDL: true, 296 hasError: false, 297 }, 298 { 299 sql: "INSERT INTO do_db.do_table VALUES (1)", 300 isDDL: false, 301 hasError: false, 302 }, 303 { 304 sql: "INSERT INTO ignore_db.ignore_table VALUES (1)", 305 isDDL: false, 306 hasError: false, 307 }, 308 { 309 sql: "UPDATE `ignore_db`.`ignore_table` SET c1=2 WHERE c1=1", 310 isDDL: false, 311 hasError: false, 312 }, 313 { 314 sql: "DELETE FROM `ignore_table` WHERE c1=2", 315 isDDL: false, 316 hasError: false, 317 }, 318 { 319 sql: "SELECT * FROM ignore_db.ignore_table", 320 isDDL: false, 321 hasError: false, 322 }, 323 { 324 sql: "#", 325 isDDL: false, 326 hasError: false, 327 }, 328 { 329 sql: "# this is a comment", 330 isDDL: false, 331 hasError: false, 332 }, 333 { 334 sql: "# a comment with DDL\nCREATE TABLE do_db.do_table (c1 INT)", 335 isDDL: true, 336 hasError: false, 337 }, 338 { 339 sql: "# a comment with DML\nUPDATE `ignore_db`.`ignore_table` SET c1=2 WHERE c1=1", 340 isDDL: false, 341 hasError: false, 342 }, 343 { 344 sql: "NOT A SQL", 345 isDDL: false, 346 hasError: true, 347 }, 348 } 349 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestparseOneStmt"))) 350 qec := &queryEventContext{ 351 eventContext: &eventContext{ 352 tctx: tctx, 353 }, 354 p: parser.New(), 355 } 356 357 for _, cs := range cases { 358 qec.originSQL = cs.sql 359 stmt, err := parseOneStmt(qec) 360 if cs.hasError { 361 c.Assert(err, NotNil) 362 } else { 363 c.Assert(err, IsNil) 364 } 365 _, ok := stmt.(ast.DDLNode) 366 c.Assert(ok, Equals, cs.isDDL) 367 } 368 } 369 370 func (s *testDDLSuite) TestResolveGeneratedColumnSQL(c *C) { 371 testCases := []struct { 372 sql string 373 expected string 374 }{ 375 { 376 "ALTER TABLE `test`.`test` ADD COLUMN d int(11) GENERATED ALWAYS AS (c + 1) VIRTUAL", 377 "ALTER TABLE `test`.`test` ADD COLUMN `d` INT(11) GENERATED ALWAYS AS(`c`+1) VIRTUAL", 378 }, 379 { 380 "ALTER TABLE `test`.`test` ADD COLUMN d int(11) AS (1 + 1) STORED", 381 "ALTER TABLE `test`.`test` ADD COLUMN `d` INT(11) GENERATED ALWAYS AS(1+1) STORED", 382 }, 383 } 384 385 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestResolveGeneratedColumnSQL"))) 386 syncer := NewSyncer(&config.SubTaskConfig{Flavor: mysql.MySQLFlavor}, nil, nil) 387 syncer.tctx = tctx 388 c.Assert(syncer.genRouter(), IsNil) 389 p := parser.New() 390 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 391 for _, tc := range testCases { 392 qec := &queryEventContext{ 393 eventContext: &eventContext{ 394 tctx: tctx, 395 }, 396 originSQL: tc.sql, 397 appliedDDLs: make([]string, 0), 398 ddlSchema: "test", 399 p: p, 400 } 401 stmt, err := parseOneStmt(qec) 402 c.Assert(err, IsNil) 403 404 qec.splitDDLs, err = parserpkg.SplitDDL(stmt, qec.ddlSchema) 405 c.Assert(err, IsNil) 406 for _, sql := range qec.splitDDLs { 407 sqls, err := ddlWorker.processOneDDL(qec, sql) 408 c.Assert(err, IsNil) 409 qec.appliedDDLs = append(qec.appliedDDLs, sqls...) 410 } 411 412 c.Assert(len(qec.appliedDDLs), Equals, 1) 413 c.Assert(qec.appliedDDLs[0], Equals, tc.expected) 414 } 415 } 416 417 func (s *testDDLSuite) TestResolveOnlineDDL(c *C) { 418 cases := []struct { 419 sql string 420 expectSQL string 421 }{ 422 // *****GHOST***** 423 // real table 424 { 425 sql: "ALTER TABLE `test`.`t1` ADD COLUMN `n` INT", 426 expectSQL: "ALTER TABLE `test`.`t1` ADD COLUMN `n` INT", 427 }, 428 // trash table 429 { 430 sql: "CREATE TABLE IF NOT EXISTS `test`.`_t1_del` (`n` INT)", 431 }, 432 // ghost table 433 { 434 sql: "ALTER TABLE `test`.`_t1_gho` ADD COLUMN `n` INT", 435 }, 436 // *****PT***** 437 // real table 438 { 439 sql: "ALTER TABLE `test`.`t1` ADD COLUMN `n` INT", 440 expectSQL: "ALTER TABLE `test`.`t1` ADD COLUMN `n` INT", 441 }, 442 // trash table 443 { 444 sql: "CREATE TABLE IF NOT EXISTS `test`.`_t1_old` (`n` INT)", 445 }, 446 // ghost table 447 { 448 sql: "ALTER TABLE `test`.`_t1_new` ADD COLUMN `n` INT", 449 }, 450 } 451 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestResolveOnlineDDL"))) 452 p := parser.New() 453 454 testEC := &eventContext{tctx: tctx} 455 cluster, err := conn.NewCluster() 456 c.Assert(err, IsNil) 457 c.Assert(cluster.Start(), IsNil) 458 mockClusterPort := cluster.Port 459 dbCfg := config.GetDBConfigForTest() 460 dbCfg.Port = mockClusterPort 461 dbCfg.Password = "" 462 cfg := s.newSubTaskCfg(dbCfg) 463 464 var qec *queryEventContext 465 for _, ca := range cases { 466 plugin, err := onlineddl.NewRealOnlinePlugin(tctx, cfg, nil) 467 c.Assert(err, IsNil) 468 syncer := NewSyncer(cfg, nil, nil) 469 syncer.tctx = tctx 470 syncer.onlineDDL = plugin 471 c.Assert(plugin.Clear(tctx), IsNil) 472 c.Assert(syncer.genRouter(), IsNil) 473 qec = &queryEventContext{ 474 eventContext: testEC, 475 ddlSchema: "test", 476 appliedDDLs: make([]string, 0), 477 p: p, 478 } 479 qec.originSQL = ca.sql 480 stmt, err := parseOneStmt(qec) 481 c.Assert(err, IsNil) 482 _, ok := stmt.(ast.DDLNode) 483 c.Assert(ok, IsTrue) 484 qec.splitDDLs, err = parserpkg.SplitDDL(stmt, qec.ddlSchema) 485 c.Assert(err, IsNil) 486 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 487 for _, sql := range qec.splitDDLs { 488 sqls, err := ddlWorker.processOneDDL(qec, sql) 489 c.Assert(err, IsNil) 490 qec.appliedDDLs = append(qec.appliedDDLs, sqls...) 491 } 492 if len(ca.expectSQL) != 0 { 493 c.Assert(qec.appliedDDLs, HasLen, 1) 494 c.Assert(qec.appliedDDLs[0], Equals, ca.expectSQL) 495 } 496 } 497 cluster.Stop() 498 } 499 500 func (s *testDDLSuite) TestMistakeOnlineDDLRegex(c *C) { 501 cases := []struct { 502 onlineType string 503 trashName string 504 ghostname string 505 matchGho bool 506 }{ 507 { 508 config.GHOST, 509 "_t1_del", 510 "_t1_gho_invalid", 511 false, 512 }, 513 { 514 config.GHOST, 515 "_t1_del_invalid", 516 "_t1_gho", 517 true, 518 }, 519 { 520 config.PT, 521 "_t1_old", 522 "_t1_new_invalid", 523 false, 524 }, 525 { 526 config.PT, 527 "_t1_old_invalid", 528 "_t1_new", 529 true, 530 }, 531 } 532 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestMistakeOnlineDDLRegex"))) 533 p := parser.New() 534 535 ec := eventContext{tctx: tctx} 536 cluster, err := conn.NewCluster() 537 c.Assert(err, IsNil) 538 c.Assert(cluster.Start(), IsNil) 539 mockClusterPort := cluster.Port 540 c.Assert(err, IsNil) 541 dbCfg := config.GetDBConfigForTest() 542 dbCfg.Port = mockClusterPort 543 dbCfg.Password = "" 544 cfg := s.newSubTaskCfg(dbCfg) 545 for _, ca := range cases { 546 plugin, err := onlineddl.NewRealOnlinePlugin(tctx, cfg, nil) 547 c.Assert(err, IsNil) 548 syncer := NewSyncer(cfg, nil, nil) 549 c.Assert(syncer.genRouter(), IsNil) 550 syncer.onlineDDL = plugin 551 c.Assert(plugin.Clear(tctx), IsNil) 552 553 // ghost table 554 sql := fmt.Sprintf("ALTER TABLE `test`.`%s` ADD COLUMN `n` INT", ca.ghostname) 555 qec := &queryEventContext{ 556 eventContext: &ec, 557 ddlSchema: "test", 558 p: p, 559 } 560 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 561 sqls, err := ddlWorker.processOneDDL(qec, sql) 562 c.Assert(err, IsNil) 563 table := ca.ghostname 564 matchRules := config.ShadowTableRules 565 if ca.matchGho { 566 c.Assert(sqls, HasLen, 0) 567 table = ca.trashName 568 matchRules = config.TrashTableRules 569 } else { 570 c.Assert(sqls, HasLen, 1) 571 c.Assert(sqls[0], Equals, sql) 572 } 573 sql = fmt.Sprintf("RENAME TABLE `test`.`t1` TO `test`.`%s`, `test`.`%s` TO `test`.`t1`", ca.trashName, ca.ghostname) 574 qec = &queryEventContext{ 575 eventContext: &ec, 576 ddlSchema: "test", 577 p: p, 578 } 579 sqls, err = ddlWorker.processOneDDL(qec, sql) 580 c.Assert(terror.ErrConfigOnlineDDLMistakeRegex.Equal(err), IsTrue) 581 c.Assert(sqls, HasLen, 0) 582 c.Assert(err, ErrorMatches, ".*"+sql+".*"+table+".*"+matchRules+".*") 583 } 584 cluster.Stop() 585 } 586 587 func (s *testDDLSuite) TestDropSchemaInSharding(c *C) { 588 var ( 589 targetTable = &filter.Table{ 590 Schema: "target_db", 591 Name: "tbl", 592 } 593 sourceDB = "db1" 594 source1 = "`db1`.`tbl1`" 595 source2 = "`db1`.`tbl2`" 596 tctx = tcontext.Background() 597 ) 598 dbCfg := config.GetDBConfigForTest() 599 cfg := s.newSubTaskCfg(dbCfg) 600 cfg.ShardMode = config.ShardPessimistic 601 syncer := NewSyncer(cfg, nil, nil) 602 // nolint:dogsled 603 _, _, _, _, err := syncer.sgk.AddGroup(targetTable, []string{source1}, nil, true) 604 c.Assert(err, IsNil) 605 // nolint:dogsled 606 _, _, _, _, err = syncer.sgk.AddGroup(targetTable, []string{source2}, nil, true) 607 c.Assert(err, IsNil) 608 c.Assert(syncer.sgk.Groups(), HasLen, 2) 609 pessimist := NewPessimistDDL(&syncer.tctx.Logger, syncer) 610 c.Assert(pessimist.dropSchemaInSharding(tctx, sourceDB), IsNil) 611 c.Assert(syncer.sgk.Groups(), HasLen, 0) 612 } 613 614 func (s *testDDLSuite) TestClearOnlineDDL(c *C) { 615 var ( 616 targetTable = &filter.Table{ 617 Schema: "target_db", 618 Name: "tbl", 619 } 620 source1 = "`db1`.`tbl1`" 621 key1 = "db1tbl1" 622 source2 = "`db1`.`tbl2`" 623 key2 = "db1tbl2" 624 tctx = tcontext.Background() 625 ) 626 dbCfg := config.GetDBConfigForTest() 627 cfg := s.newSubTaskCfg(dbCfg) 628 cfg.ShardMode = config.ShardPessimistic 629 syncer := NewSyncer(cfg, nil, nil) 630 mock := mockOnlinePlugin{ 631 map[string]struct{}{key1: {}, key2: {}}, 632 } 633 syncer.onlineDDL = mock 634 635 // nolint:dogsled 636 _, _, _, _, err := syncer.sgk.AddGroup(targetTable, []string{source1}, nil, true) 637 c.Assert(err, IsNil) 638 // nolint:dogsled 639 _, _, _, _, err = syncer.sgk.AddGroup(targetTable, []string{source2}, nil, true) 640 c.Assert(err, IsNil) 641 642 pessimist := NewPessimistDDL(&syncer.tctx.Logger, syncer) 643 c.Assert(pessimist.clearOnlineDDL(tctx, targetTable), IsNil) 644 c.Assert(mock.toFinish, HasLen, 0) 645 } 646 647 func (s *testDDLSuite) TestAdjustDatabaseCollation(c *C) { 648 statusVarsArray := [][]byte{ 649 { 650 4, 0, 0, 0, 0, 46, 0, 651 }, 652 { 653 4, 0, 0, 0, 0, 21, 1, 654 }, 655 } 656 657 sqls := []string{ 658 "create database if not exists `test`", 659 "create database `test` CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci", 660 "create database `test` CHARACTER SET=utf8mb4", 661 "create database `test` COLLATE=utf8mb4_general_ci", 662 } 663 664 expectedSQLs := [][]string{ 665 { 666 "CREATE DATABASE IF NOT EXISTS `test` COLLATE = utf8mb4_bin", 667 "CREATE DATABASE `test` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci", 668 "CREATE DATABASE `test` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci", 669 "CREATE DATABASE `test` COLLATE = utf8mb4_general_ci", 670 }, 671 { 672 "CREATE DATABASE IF NOT EXISTS `test` COLLATE = utf8mb4_vi_0900_ai_ci", 673 "CREATE DATABASE `test` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci", 674 "CREATE DATABASE `test` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci", 675 "CREATE DATABASE `test` COLLATE = utf8mb4_general_ci", 676 }, 677 } 678 679 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestAdjustTableCollation"))) 680 syncer := NewSyncer(&config.SubTaskConfig{ 681 Flavor: mysql.MySQLFlavor, 682 CollationCompatible: config.StrictCollationCompatible, 683 }, nil, nil) 684 syncer.tctx = tctx 685 p := parser.New() 686 tab := &filter.Table{ 687 Schema: "test", 688 Name: "t", 689 } 690 691 charsetAndDefaultCollationMap := map[string]string{"utf8mb4": "utf8mb4_general_ci"} 692 idAndCollationMap := map[int]string{46: "utf8mb4_bin", 277: "utf8mb4_vi_0900_ai_ci"} 693 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 694 for i, statusVars := range statusVarsArray { 695 for j, sql := range sqls { 696 ddlInfo := &ddlInfo{ 697 originDDL: sql, 698 routedDDL: sql, 699 sourceTables: []*filter.Table{tab}, 700 targetTables: []*filter.Table{tab}, 701 } 702 stmt, err := p.ParseOneStmt(sql, "", "") 703 c.Assert(err, IsNil) 704 c.Assert(stmt, NotNil) 705 ddlInfo.stmtCache = stmt 706 ddlWorker.adjustCollation(ddlInfo, statusVars, charsetAndDefaultCollationMap, idAndCollationMap) 707 routedDDL, err := parserpkg.RenameDDLTable(ddlInfo.stmtCache, ddlInfo.targetTables) 708 c.Assert(err, IsNil) 709 c.Assert(routedDDL, Equals, expectedSQLs[i][j]) 710 } 711 } 712 } 713 714 func TestAdjustCollation(t *testing.T) { 715 sqls := []string{ 716 "create table `test`.`t1` (id int) CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci", 717 "create table `test`.`t1` (id int) CHARSET=utf8mb4", 718 "create table `test`.`t1` (id int) COLLATE=utf8mb4_general_ci", 719 "create table `test`.`t1` (id int)", 720 "create table `test`.`t1` (name varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin)", 721 "create table `test`.`t1` (id int, name varchar(20) CHARACTER SET utf8mb4, work varchar(20))", 722 "create table `test`.`t1` (id int, name varchar(20), work varchar(20))", 723 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20))", 724 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20) CHARACTER SET utf8mb4)", 725 "create table `test`.`t1` (id int, name varchar(20) CHARACTER SET utf8mb4, work varchar(20)) COLLATE=utf8mb4_general_ci", 726 "create table `test`.`t1` (id int, name varchar(20) CHARACTER SET utf8mb4, work varchar(20)) COLLATE=latin1_swedish_ci", 727 "create table `test`.`t1` (id int, name varchar(20), work varchar(20)) COLLATE=utf8mb4_general_ci", 728 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20)) COLLATE=utf8mb4_general_ci", 729 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20) CHARACTER SET utf8mb4) COLLATE=utf8mb4_general_ci", 730 "create table `test`.`t1` (id int, name varchar(20) CHARACTER SET utf8mb4, work varchar(20)) CHARSET=utf8mb4 ", 731 "create table `test`.`t1` (id int, name varchar(20) CHARACTER SET latin1, work varchar(20)) CHARSET=utf8mb4 ", 732 "create table `test`.`t1` (id int, name varchar(20), work varchar(20)) CHARSET=utf8mb4", 733 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20)) CHARSET=utf8mb4", 734 "create table `test`.`t1` (id int, name varchar(20) COLLATE utf8mb4_general_ci, work varchar(20) CHARACTER SET utf8mb4) CHARSET=utf8mb4", 735 } 736 737 expectedSQLs := []string{ 738 "CREATE TABLE `test`.`t` (`id` INT) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 739 "CREATE TABLE `test`.`t` (`id` INT) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 740 "CREATE TABLE `test`.`t` (`id` INT) DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 741 "CREATE TABLE `test`.`t` (`id` INT)", 742 "CREATE TABLE `test`.`t` (`name` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_bin)", 743 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci,`work` VARCHAR(20))", 744 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20),`work` VARCHAR(20))", 745 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20))", 746 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci)", 747 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci,`work` VARCHAR(20)) DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 748 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci,`work` VARCHAR(20)) DEFAULT COLLATE = LATIN1_SWEDISH_CI", 749 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20),`work` VARCHAR(20)) DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 750 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20)) DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 751 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci) DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 752 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci,`work` VARCHAR(20)) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 753 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) CHARACTER SET LATIN1 COLLATE latin1_swedish_ci,`work` VARCHAR(20)) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 754 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20),`work` VARCHAR(20)) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 755 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20)) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 756 "CREATE TABLE `test`.`t` (`id` INT,`name` VARCHAR(20) COLLATE utf8mb4_general_ci,`work` VARCHAR(20) CHARACTER SET UTF8MB4 COLLATE utf8mb4_general_ci) DEFAULT CHARACTER SET = UTF8MB4 DEFAULT COLLATE = UTF8MB4_GENERAL_CI", 757 } 758 759 tctx := tcontext.Background().WithLogger(log.With(zap.String("test", "TestAdjustTableCollation"))) 760 syncer := NewSyncer(&config.SubTaskConfig{ 761 Flavor: mysql.MySQLFlavor, 762 CollationCompatible: config.StrictCollationCompatible, 763 }, nil, nil) 764 syncer.tctx = tctx 765 p := parser.New() 766 tab := &filter.Table{ 767 Schema: "test", 768 Name: "t", 769 } 770 statusVars := []byte{4, 0, 0, 0, 0, 46, 0} 771 charsetAndDefaultCollationMap := map[string]string{"utf8mb4": "utf8mb4_general_ci", "latin1": "latin1_swedish_ci"} 772 idAndCollationMap := map[int]string{46: "utf8mb4_bin"} 773 ddlWorker := NewDDLWorker(&tctx.Logger, syncer) 774 for i, sql := range sqls { 775 ddlInfo := &ddlInfo{ 776 originDDL: sql, 777 routedDDL: sql, 778 sourceTables: []*filter.Table{tab}, 779 targetTables: []*filter.Table{tab}, 780 } 781 stmt, err := p.ParseOneStmt(sql, "", "") 782 require.NoError(t, err) 783 require.NotNil(t, stmt) 784 ddlInfo.stmtCache = stmt 785 ddlWorker.adjustCollation(ddlInfo, statusVars, charsetAndDefaultCollationMap, idAndCollationMap) 786 routedDDL, err := parserpkg.RenameDDLTable(ddlInfo.stmtCache, ddlInfo.targetTables) 787 require.NoError(t, err) 788 require.Equal(t, expectedSQLs[i], routedDDL) 789 } 790 } 791 792 type mockOnlinePlugin struct { 793 toFinish map[string]struct{} 794 } 795 796 func (m mockOnlinePlugin) Apply(tctx *tcontext.Context, tables []*filter.Table, statement string, stmt ast.StmtNode, p *parser.Parser) ([]string, error) { 797 return nil, nil 798 } 799 800 func (m mockOnlinePlugin) Finish(tctx *tcontext.Context, table *filter.Table) error { 801 tableID := table.Schema + table.Name 802 if _, ok := m.toFinish[tableID]; !ok { 803 return errors.New("finish table not found") 804 } 805 delete(m.toFinish, tableID) 806 return nil 807 } 808 809 func (m mockOnlinePlugin) TableType(table string) onlineddl.TableType { 810 // 5 is _ _gho/ghc/del or _ _old/new 811 if len(table) > 5 && strings.HasPrefix(table, "_") { 812 if strings.HasSuffix(table, "_gho") || strings.HasSuffix(table, "_new") { 813 return onlineddl.GhostTable 814 } 815 816 if strings.HasSuffix(table, "_ghc") || strings.HasSuffix(table, "_del") || strings.HasSuffix(table, "_old") { 817 return onlineddl.TrashTable 818 } 819 } 820 return onlineddl.RealTable 821 } 822 823 func (m mockOnlinePlugin) RealName(table string) string { 824 return "" 825 } 826 827 func (m mockOnlinePlugin) ResetConn(tctx *tcontext.Context) error { 828 return nil 829 } 830 831 func (m mockOnlinePlugin) Clear(tctx *tcontext.Context) error { 832 return nil 833 } 834 835 func (m mockOnlinePlugin) Close() { 836 } 837 838 func (m mockOnlinePlugin) CheckAndUpdate(tctx *tcontext.Context, schemas map[string]string, tables map[string]map[string]string) error { 839 return nil 840 } 841 842 func (m mockOnlinePlugin) CheckRegex(stmt ast.StmtNode, schema string, flavor conn.LowerCaseTableNamesFlavor) error { 843 return nil 844 }