github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/cdc/sink/dmlsink/txn/mysql/mysql_test.go (about) 1 // Copyright 2022 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 mysql 15 16 import ( 17 "context" 18 "database/sql" 19 "database/sql/driver" 20 "fmt" 21 "net" 22 "net/url" 23 "sync" 24 "testing" 25 "time" 26 27 "github.com/DATA-DOG/go-sqlmock" 28 dmysql "github.com/go-sql-driver/mysql" 29 "github.com/pingcap/errors" 30 "github.com/pingcap/log" 31 "github.com/pingcap/tidb/pkg/ddl" 32 "github.com/pingcap/tidb/pkg/infoschema" 33 "github.com/pingcap/tidb/pkg/parser" 34 "github.com/pingcap/tidb/pkg/parser/ast" 35 "github.com/pingcap/tidb/pkg/parser/charset" 36 "github.com/pingcap/tidb/pkg/parser/mysql" 37 "github.com/pingcap/tiflow/cdc/model" 38 "github.com/pingcap/tiflow/cdc/sink/dmlsink" 39 "github.com/pingcap/tiflow/cdc/sink/metrics" 40 "github.com/pingcap/tiflow/pkg/config" 41 "github.com/pingcap/tiflow/pkg/sink" 42 pmysql "github.com/pingcap/tiflow/pkg/sink/mysql" 43 "github.com/pingcap/tiflow/pkg/sqlmodel" 44 "github.com/stretchr/testify/require" 45 "go.uber.org/zap" 46 "go.uber.org/zap/zaptest/observer" 47 ) 48 49 func init() { 50 serverConfig := config.GetGlobalServerConfig().Clone() 51 serverConfig.TZ = "UTC" 52 config.StoreGlobalServerConfig(serverConfig) 53 } 54 55 func newMySQLBackendWithoutDB(ctx context.Context) *mysqlBackend { 56 cfg := pmysql.NewConfig() 57 cfg.BatchDMLEnable = false 58 return &mysqlBackend{ 59 statistics: metrics.NewStatistics(ctx, 60 model.DefaultChangeFeedID("test"), 61 sink.TxnSink), 62 cfg: cfg, 63 } 64 } 65 66 func newMySQLBackend( 67 ctx context.Context, 68 changefeedID model.ChangeFeedID, 69 sinkURI *url.URL, 70 replicaConfig *config.ReplicaConfig, 71 dbConnFactory pmysql.Factory, 72 ) (*mysqlBackend, error) { 73 ctx1, cancel := context.WithCancel(ctx) 74 statistics := metrics.NewStatistics(ctx1, changefeedID, sink.TxnSink) 75 cancel() // Cancel background goroutines in returned metrics.Statistics. 76 raw := sinkURI.Query() 77 raw.Set("batch-dml-enable", "true") 78 sinkURI.RawQuery = raw.Encode() 79 80 backends, err := NewMySQLBackends(ctx, changefeedID, 81 sinkURI, replicaConfig, dbConnFactory, statistics) 82 if err != nil { 83 return nil, err 84 } 85 return backends[0], nil 86 } 87 88 func newTestMockDB(t *testing.T) (db *sql.DB, mock sqlmock.Sqlmock) { 89 db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) 90 mock.ExpectQuery("select tidb_version()").WillReturnError(&dmysql.MySQLError{ 91 Number: 1305, 92 Message: "FUNCTION test.tidb_version does not exist", 93 }) 94 // mock a different possible error for the second query 95 mock.ExpectQuery("select tidb_version()").WillReturnError(&dmysql.MySQLError{ 96 Number: 1044, 97 Message: "Access denied for user 'cdc'@'%' to database 'information_schema'", 98 }) 99 require.Nil(t, err) 100 return 101 } 102 103 func TestPrepareDML(t *testing.T) { 104 t.Parallel() 105 tableInfo := model.BuildTableInfo("common_1", "uk_without_pk", []*model.Column{ 106 nil, 107 { 108 Name: "a1", 109 Type: mysql.TypeLong, 110 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 111 }, 112 { 113 Name: "a3", 114 Type: mysql.TypeLong, 115 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 116 }, 117 }, [][]int{{1, 2}}) 118 119 testCases := []struct { 120 input []*model.RowChangedEvent 121 expected *preparedDMLs 122 }{ 123 { 124 input: []*model.RowChangedEvent{}, 125 expected: &preparedDMLs{ 126 startTs: []model.Ts{}, 127 sqls: []string{}, 128 values: [][]interface{}{}, 129 }, 130 }, 131 // delete event 132 { 133 input: []*model.RowChangedEvent{ 134 { 135 StartTs: 418658114257813514, 136 CommitTs: 418658114257813515, 137 TableInfo: tableInfo, 138 PreColumns: model.Columns2ColumnDatas([]*model.Column{ 139 nil, 140 { 141 Name: "a1", 142 Value: 1, 143 }, 144 { 145 Name: "a3", 146 Value: 1, 147 }, 148 }, tableInfo), 149 }, 150 }, 151 expected: &preparedDMLs{ 152 startTs: []model.Ts{418658114257813514}, 153 sqls: []string{"DELETE FROM `common_1`.`uk_without_pk` WHERE `a1` = ? AND `a3` = ? LIMIT 1"}, 154 values: [][]interface{}{{1, 1}}, 155 rowCount: 1, 156 approximateSize: 74, 157 }, 158 }, 159 // insert event. 160 { 161 input: []*model.RowChangedEvent{ 162 { 163 StartTs: 418658114257813516, 164 CommitTs: 418658114257813517, 165 TableInfo: tableInfo, 166 Columns: model.Columns2ColumnDatas([]*model.Column{ 167 nil, 168 { 169 Name: "a1", 170 Value: 2, 171 }, 172 { 173 Name: "a3", 174 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag, 175 Value: 2, 176 }, 177 }, tableInfo), 178 }, 179 }, 180 expected: &preparedDMLs{ 181 startTs: []model.Ts{418658114257813516}, 182 sqls: []string{"INSERT INTO `common_1`.`uk_without_pk` (`a1`,`a3`) VALUES (?,?)"}, 183 values: [][]interface{}{{2, 2}}, 184 rowCount: 1, 185 approximateSize: 63, 186 }, 187 }, 188 } 189 190 ctx, cancel := context.WithCancel(context.Background()) 191 defer cancel() 192 ms := newMySQLBackendWithoutDB(ctx) 193 for _, tc := range testCases { 194 ms.events = make([]*dmlsink.TxnCallbackableEvent, 1) 195 ms.events[0] = &dmlsink.TxnCallbackableEvent{ 196 Event: &model.SingleTableTxn{Rows: tc.input}, 197 } 198 ms.rows = len(tc.input) 199 dmls := ms.prepareDMLs() 200 require.Equal(t, tc.expected, dmls) 201 } 202 } 203 204 func TestAdjustSQLMode(t *testing.T) { 205 ctx, cancel := context.WithCancel(context.Background()) 206 defer cancel() 207 208 dbIndex := 0 209 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 210 defer func() { dbIndex++ }() 211 212 if dbIndex == 0 { 213 // test db 214 db, err := pmysql.MockTestDB() 215 require.Nil(t, err) 216 return db, nil 217 } 218 219 // normal db 220 db, mock := newTestMockDB(t) 221 mock.ExpectClose() 222 return db, nil 223 } 224 225 changefeed := "test-changefeed" 226 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1" + 227 "&cache-prep-stmts=false") 228 require.Nil(t, err) 229 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), 230 sinkURI, config.GetDefaultReplicaConfig(), mockGetDBConn) 231 require.Nil(t, err) 232 require.Nil(t, sink.Close()) 233 } 234 235 type mockUnavailableMySQL struct { 236 listener net.Listener 237 quit chan interface{} 238 wg sync.WaitGroup 239 } 240 241 func newMockUnavailableMySQL(addr string, t *testing.T) *mockUnavailableMySQL { 242 s := &mockUnavailableMySQL{ 243 quit: make(chan interface{}), 244 } 245 l, err := net.Listen("tcp", addr) 246 require.Nil(t, err) 247 s.listener = l 248 s.wg.Add(1) 249 go s.serve(t) 250 return s 251 } 252 253 func (s *mockUnavailableMySQL) serve(t *testing.T) { 254 defer s.wg.Done() 255 256 for { 257 _, err := s.listener.Accept() 258 if err != nil { 259 select { 260 case <-s.quit: 261 return 262 default: 263 require.Error(t, err) 264 } 265 } else { 266 s.wg.Add(1) 267 go func() { 268 // don't read from TCP connection, to simulate database service unavailable 269 <-s.quit 270 s.wg.Done() 271 }() 272 } 273 } 274 } 275 276 func (s *mockUnavailableMySQL) Stop() { 277 close(s.quit) 278 s.listener.Close() 279 s.wg.Wait() 280 } 281 282 func TestNewMySQLTimeout(t *testing.T) { 283 addr := "127.0.0.1:33333" 284 mockMySQL := newMockUnavailableMySQL(addr, t) 285 defer mockMySQL.Stop() 286 287 ctx, cancel := context.WithCancel(context.Background()) 288 defer cancel() 289 changefeed := "test-changefeed" 290 sinkURI, err := url.Parse(fmt.Sprintf("mysql://%s/?read-timeout=1s&timeout=1s", addr)) 291 require.Nil(t, err) 292 _, err = newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 293 config.GetDefaultReplicaConfig(), pmysql.CreateMySQLDBConn) 294 require.Equal(t, driver.ErrBadConn, errors.Cause(err)) 295 } 296 297 // Test OnTxnEvent and Flush interfaces. Event callbacks should be called correctly after flush. 298 func TestNewMySQLBackendExecDML(t *testing.T) { 299 dbIndex := 0 300 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 301 defer func() { dbIndex++ }() 302 303 if dbIndex == 0 { 304 // test db 305 db, err := pmysql.MockTestDB() 306 require.Nil(t, err) 307 return db, nil 308 } 309 310 // normal db 311 db, mock := newTestMockDB(t) 312 mock.ExpectBegin() 313 mock.ExpectExec("INSERT INTO `s1`.`t1` (`a`,`b`) VALUES (?,?),(?,?)"). 314 WithArgs(1, "test", 2, "test"). 315 WillReturnResult(sqlmock.NewResult(2, 2)) 316 mock.ExpectCommit() 317 mock.ExpectClose() 318 return db, nil 319 } 320 321 ctx, cancel := context.WithCancel(context.Background()) 322 defer cancel() 323 changefeed := "test-changefeed" 324 // TODO: Need to test txn sink behavior when cache-prep-stmts is true 325 // I did some attempts to write tests when cache-prep-stmts is true, but failed. 326 // The reason is that I can't find a way to prepare a statement in sqlmock connection, 327 // and execute it in another sqlmock connection. 328 sinkURI, err := url.Parse( 329 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&cache-prep-stmts=false") 330 require.Nil(t, err) 331 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 332 config.GetDefaultReplicaConfig(), mockGetDBConn) 333 require.Nil(t, err) 334 335 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 336 { 337 Name: "a", 338 Type: mysql.TypeLong, 339 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 340 }, 341 { 342 Name: "b", 343 Type: mysql.TypeVarchar, 344 Flag: 0, 345 }, 346 }, [][]int{{0}}) 347 rows := []*model.RowChangedEvent{ 348 { 349 StartTs: 1, 350 CommitTs: 2, 351 TableInfo: tableInfo, 352 PhysicalTableID: 1, 353 Columns: model.Columns2ColumnDatas([]*model.Column{ 354 { 355 Name: "a", 356 Value: 1, 357 }, 358 { 359 Name: "b", 360 Value: "test", 361 }, 362 }, tableInfo), 363 }, 364 { 365 StartTs: 5, 366 CommitTs: 6, 367 TableInfo: tableInfo, 368 PhysicalTableID: 1, 369 Columns: model.Columns2ColumnDatas([]*model.Column{ 370 { 371 Name: "a", 372 Value: 2, 373 }, 374 { 375 Name: "b", 376 Value: "test", 377 }, 378 }, tableInfo), 379 }, 380 } 381 382 var flushedTs uint64 = 0 383 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 384 Event: &model.SingleTableTxn{Rows: rows}, 385 Callback: func() { 386 for _, row := range rows { 387 if flushedTs < row.CommitTs { 388 flushedTs = row.CommitTs 389 } 390 } 391 }, 392 }) 393 394 err = sink.Flush(context.Background()) 395 require.Nil(t, err) 396 require.Equal(t, uint64(6), flushedTs) 397 398 require.Nil(t, sink.Close()) 399 } 400 401 func TestExecDMLRollbackErrDatabaseNotExists(t *testing.T) { 402 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 403 { 404 Name: "a", 405 Type: mysql.TypeLong, 406 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 407 }, 408 }, [][]int{{0}}) 409 rows := []*model.RowChangedEvent{ 410 { 411 TableInfo: tableInfo, 412 PhysicalTableID: 1, 413 Columns: model.Columns2ColumnDatas([]*model.Column{ 414 { 415 Name: "a", 416 Value: 1, 417 }, 418 }, tableInfo), 419 }, 420 { 421 TableInfo: tableInfo, 422 PhysicalTableID: 1, 423 Columns: model.Columns2ColumnDatas([]*model.Column{ 424 { 425 Name: "a", 426 Value: 2, 427 }, 428 }, tableInfo), 429 }, 430 } 431 432 errDatabaseNotExists := &dmysql.MySQLError{ 433 Number: uint16(infoschema.ErrDatabaseNotExists.Code()), 434 } 435 436 dbIndex := 0 437 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 438 defer func() { dbIndex++ }() 439 440 if dbIndex == 0 { 441 // test db 442 db, err := pmysql.MockTestDB() 443 require.Nil(t, err) 444 return db, nil 445 } 446 447 // normal db 448 db, mock := newTestMockDB(t) 449 mock.ExpectBegin() 450 mock.ExpectExec("REPLACE INTO `s1`.`t1` (`a`) VALUES (?),(?)"). 451 WithArgs(1, 2). 452 WillReturnError(errDatabaseNotExists) 453 mock.ExpectRollback() 454 mock.ExpectClose() 455 return db, nil 456 } 457 458 ctx, cancel := context.WithCancel(context.Background()) 459 defer cancel() 460 changefeed := "test-changefeed" 461 sinkURI, err := url.Parse( 462 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&cache-prep-stmts=false") 463 require.Nil(t, err) 464 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 465 config.GetDefaultReplicaConfig(), mockGetDBConnErrDatabaseNotExists) 466 require.Nil(t, err) 467 468 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 469 Event: &model.SingleTableTxn{Rows: rows}, 470 }) 471 err = sink.Flush(context.Background()) 472 require.Equal(t, errDatabaseNotExists, errors.Cause(err)) 473 474 require.Nil(t, sink.Close()) 475 } 476 477 func TestExecDMLRollbackErrTableNotExists(t *testing.T) { 478 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 479 { 480 Name: "a", 481 Type: mysql.TypeLong, 482 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 483 Value: 1, 484 }, 485 }, [][]int{{0}}) 486 rows := []*model.RowChangedEvent{ 487 { 488 TableInfo: tableInfo, 489 PhysicalTableID: 1, 490 Columns: model.Columns2ColumnDatas([]*model.Column{ 491 { 492 Name: "a", 493 Value: 1, 494 }, 495 }, tableInfo), 496 }, 497 { 498 TableInfo: tableInfo, 499 PhysicalTableID: 1, 500 Columns: model.Columns2ColumnDatas([]*model.Column{ 501 { 502 Name: "a", 503 Value: 2, 504 }, 505 }, tableInfo), 506 }, 507 } 508 509 errTableNotExists := &dmysql.MySQLError{ 510 Number: uint16(infoschema.ErrTableNotExists.Code()), 511 } 512 513 dbIndex := 0 514 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 515 defer func() { dbIndex++ }() 516 517 if dbIndex == 0 { 518 // test db 519 db, err := pmysql.MockTestDB() 520 require.Nil(t, err) 521 return db, nil 522 } 523 524 // normal db 525 db, mock := newTestMockDB(t) 526 mock.ExpectBegin() 527 mock.ExpectExec("REPLACE INTO `s1`.`t1` (`a`) VALUES (?),(?)"). 528 WithArgs(1, 2). 529 WillReturnError(errTableNotExists) 530 mock.ExpectRollback() 531 mock.ExpectClose() 532 return db, nil 533 } 534 535 ctx, cancel := context.WithCancel(context.Background()) 536 defer cancel() 537 changefeed := "test-changefeed" 538 sinkURI, err := url.Parse( 539 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&cache-prep-stmts=false") 540 require.Nil(t, err) 541 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 542 config.GetDefaultReplicaConfig(), mockGetDBConnErrDatabaseNotExists) 543 require.Nil(t, err) 544 545 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 546 Event: &model.SingleTableTxn{Rows: rows}, 547 }) 548 err = sink.Flush(context.Background()) 549 require.Equal(t, errTableNotExists, errors.Cause(err)) 550 551 require.Nil(t, sink.Close()) 552 } 553 554 func TestExecDMLRollbackErrRetryable(t *testing.T) { 555 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 556 { 557 Name: "a", 558 Type: mysql.TypeLong, 559 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 560 Value: 1, 561 }, 562 }, [][]int{{0}}) 563 rows := []*model.RowChangedEvent{ 564 { 565 TableInfo: tableInfo, 566 PhysicalTableID: 1, 567 Columns: model.Columns2ColumnDatas([]*model.Column{ 568 { 569 Name: "a", 570 Value: 1, 571 }, 572 }, tableInfo), 573 }, 574 { 575 TableInfo: tableInfo, 576 PhysicalTableID: 1, 577 Columns: model.Columns2ColumnDatas([]*model.Column{ 578 { 579 Name: "a", 580 Value: 2, 581 }, 582 }, tableInfo), 583 }, 584 } 585 586 errLockDeadlock := &dmysql.MySQLError{ 587 Number: mysql.ErrLockDeadlock, 588 } 589 590 dbIndex := 0 591 mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 592 defer func() { dbIndex++ }() 593 594 if dbIndex == 0 { 595 // test db 596 db, err := pmysql.MockTestDB() 597 require.Nil(t, err) 598 return db, nil 599 } 600 601 // normal db 602 db, mock := newTestMockDB(t) 603 for i := 0; i < 2; i++ { 604 mock.ExpectBegin() 605 mock.ExpectExec("REPLACE INTO `s1`.`t1` (`a`) VALUES (?),(?)"). 606 WithArgs(1, 2). 607 WillReturnError(errLockDeadlock) 608 mock.ExpectRollback() 609 } 610 mock.ExpectClose() 611 return db, nil 612 } 613 614 ctx, cancel := context.WithCancel(context.Background()) 615 defer cancel() 616 changefeed := "test-changefeed" 617 sinkURI, err := url.Parse( 618 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&cache-prep-stmts=false") 619 require.Nil(t, err) 620 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 621 config.GetDefaultReplicaConfig(), mockGetDBConnErrDatabaseNotExists) 622 require.Nil(t, err) 623 sink.setDMLMaxRetry(2) 624 625 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 626 Event: &model.SingleTableTxn{Rows: rows}, 627 }) 628 err = sink.Flush(context.Background()) 629 require.Equal(t, errLockDeadlock, errors.Cause(err)) 630 631 require.Nil(t, sink.Close()) 632 } 633 634 func TestMysqlSinkNotRetryErrDupEntry(t *testing.T) { 635 errDup := mysql.NewErr(mysql.ErrDupEntry) 636 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 637 { 638 Name: "a", 639 Type: mysql.TypeLong, 640 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 641 }, 642 }, [][]int{{0}}) 643 rows := []*model.RowChangedEvent{ 644 { 645 StartTs: 2, 646 CommitTs: 3, 647 ReplicatingTs: 1, 648 TableInfo: tableInfo, 649 PhysicalTableID: 1, 650 Columns: model.Columns2ColumnDatas([]*model.Column{ 651 { 652 Name: "a", 653 Value: 1, 654 }, 655 }, tableInfo), 656 }, 657 } 658 659 dbIndex := 0 660 mockDBInsertDupEntry := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 661 defer func() { dbIndex++ }() 662 663 if dbIndex == 0 { 664 // test db 665 db, err := pmysql.MockTestDB() 666 require.Nil(t, err) 667 return db, nil 668 } 669 670 // normal db 671 db, mock := newTestMockDB(t) 672 mock.ExpectBegin() 673 mock.ExpectExec("INSERT INTO `s1`.`t1` (`a`) VALUES (?)"). 674 WithArgs(1). 675 WillReturnResult(sqlmock.NewResult(1, 1)) 676 mock.ExpectCommit(). 677 WillReturnError(errDup) 678 mock.ExpectClose() 679 return db, nil 680 } 681 682 ctx, cancel := context.WithCancel(context.Background()) 683 defer cancel() 684 changefeed := "test-changefeed" 685 sinkURI, err := url.Parse( 686 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&safe-mode=false" + 687 "&cache-prep-stmts=false") 688 require.Nil(t, err) 689 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 690 config.GetDefaultReplicaConfig(), mockDBInsertDupEntry) 691 require.Nil(t, err) 692 sink.setDMLMaxRetry(1) 693 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 694 Event: &model.SingleTableTxn{Rows: rows}, 695 }) 696 err = sink.Flush(context.Background()) 697 require.Equal(t, errDup, errors.Cause(err)) 698 699 require.Nil(t, sink.Close()) 700 } 701 702 func TestNewMySQLBackendExecDDL(t *testing.T) { 703 // TODO: fill it. 704 } 705 706 func TestNeedSwitchDB(t *testing.T) { 707 // TODO: fill it. 708 } 709 710 func TestNewMySQLBackend(t *testing.T) { 711 dbIndex := 0 712 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 713 defer func() { dbIndex++ }() 714 715 if dbIndex == 0 { 716 // test db 717 db, err := pmysql.MockTestDB() 718 require.Nil(t, err) 719 return db, nil 720 } 721 722 // normal db 723 db, mock := newTestMockDB(t) 724 mock.ExpectClose() 725 return db, nil 726 } 727 728 ctx, cancel := context.WithCancel(context.Background()) 729 defer cancel() 730 731 changefeed := "test-changefeed" 732 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1" + 733 "&cache-prep-stmts=false") 734 require.Nil(t, err) 735 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 736 config.GetDefaultReplicaConfig(), mockGetDBConn) 737 738 require.Nil(t, err) 739 require.Nil(t, sink.Close()) 740 // Test idempotency of `Close` interface 741 require.Nil(t, sink.Close()) 742 } 743 744 func TestNewMySQLBackendWithIPv6Address(t *testing.T) { 745 dbIndex := 0 746 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 747 require.Contains(t, dsnStr, "root@tcp([::1]:3306)") 748 defer func() { dbIndex++ }() 749 750 if dbIndex == 0 { 751 // test db 752 db, err := pmysql.MockTestDB() 753 require.Nil(t, err) 754 return db, nil 755 } 756 757 // normal db 758 db, mock := newTestMockDB(t) 759 mock.ExpectClose() 760 return db, nil 761 } 762 763 ctx, cancel := context.WithCancel(context.Background()) 764 defer cancel() 765 changefeed := "test-changefeed" 766 // See https://www.ietf.org/rfc/rfc2732.txt, we have to use brackets to wrap IPv6 address. 767 sinkURI, err := url.Parse("mysql://[::1]:3306/?time-zone=UTC&worker-count=1" + 768 "&cache-prep-stmts=false") 769 require.Nil(t, err) 770 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 771 config.GetDefaultReplicaConfig(), mockGetDBConn) 772 require.Nil(t, err) 773 require.Nil(t, sink.Close()) 774 } 775 776 func TestGBKSupported(t *testing.T) { 777 dbIndex := 0 778 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 779 defer func() { dbIndex++ }() 780 781 if dbIndex == 0 { 782 // test db 783 db, err := pmysql.MockTestDB() 784 require.Nil(t, err) 785 return db, nil 786 } 787 788 // normal db 789 db, mock := newTestMockDB(t) 790 mock.ExpectClose() 791 return db, nil 792 } 793 794 zapcore, logs := observer.New(zap.WarnLevel) 795 conf := &log.Config{Level: "warn", File: log.FileLogConfig{}} 796 _, r, _ := log.InitLogger(conf) 797 logger := zap.New(zapcore) 798 restoreFn := log.ReplaceGlobals(logger, r) 799 defer restoreFn() 800 801 ctx := context.Background() 802 changefeed := "test-changefeed" 803 sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1" + 804 "&cache-prep-stmts=false") 805 require.Nil(t, err) 806 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 807 config.GetDefaultReplicaConfig(), mockGetDBConn) 808 require.Nil(t, err) 809 810 // no gbk-related warning log will be output because GBK charset is supported 811 require.Equal(t, logs.FilterMessage("gbk charset is not supported").Len(), 0) 812 813 require.Nil(t, sink.Close()) 814 } 815 816 func TestHolderString(t *testing.T) { 817 t.Parallel() 818 819 testCases := []struct { 820 count int 821 expected string 822 }{ 823 {1, "?"}, 824 {2, "?,?"}, 825 {10, "?,?,?,?,?,?,?,?,?,?"}, 826 } 827 for _, tc := range testCases { 828 s := placeHolder(tc.count) 829 require.Equal(t, tc.expected, s) 830 } 831 // test invalid input 832 require.Panics(t, func() { placeHolder(0) }, "strings.Builder.Grow: negative count") 833 require.Panics(t, func() { placeHolder(-1) }, "strings.Builder.Grow: negative count") 834 } 835 836 func TestMySQLSinkExecDMLError(t *testing.T) { 837 dbIndex := 0 838 mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) { 839 defer func() { dbIndex++ }() 840 841 if dbIndex == 0 { 842 // test db 843 db, err := pmysql.MockTestDB() 844 require.Nil(t, err) 845 return db, nil 846 } 847 848 // normal db 849 db, mock := newTestMockDB(t) 850 mock.ExpectBegin() 851 mock.ExpectExec("INSERT INTO `s1`.`t1` (`a`,`b`) VALUES (?,?),(?,?)").WillDelayFor(1 * time.Second). 852 WillReturnError(&dmysql.MySQLError{Number: mysql.ErrNoSuchTable}) 853 mock.ExpectClose() 854 return db, nil 855 } 856 857 ctx, cancel := context.WithCancel(context.Background()) 858 defer cancel() 859 changefeed := "test-changefeed" 860 sinkURI, err := url.Parse( 861 "mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1&cache-prep-stmts=false") 862 require.Nil(t, err) 863 sink, err := newMySQLBackend(ctx, model.DefaultChangeFeedID(changefeed), sinkURI, 864 config.GetDefaultReplicaConfig(), mockGetDBConn) 865 require.Nil(t, err) 866 867 tableInfo := model.BuildTableInfo("s1", "t1", []*model.Column{ 868 { 869 Name: "a", 870 Type: mysql.TypeLong, 871 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 872 }, 873 { 874 Name: "b", 875 Type: mysql.TypeVarchar, 876 Flag: 0, 877 }, 878 }, [][]int{{0}}) 879 rows := []*model.RowChangedEvent{ 880 { 881 StartTs: 1, 882 CommitTs: 2, 883 TableInfo: tableInfo, 884 PhysicalTableID: 1, 885 Columns: model.Columns2ColumnDatas([]*model.Column{ 886 { 887 Name: "a", 888 Value: 1, 889 }, 890 { 891 Name: "b", 892 Value: "test", 893 }, 894 }, tableInfo), 895 }, 896 { 897 StartTs: 2, 898 CommitTs: 3, 899 TableInfo: tableInfo, 900 PhysicalTableID: 1, 901 Columns: model.Columns2ColumnDatas([]*model.Column{ 902 { 903 Name: "a", 904 Value: 2, 905 }, 906 { 907 Name: "b", 908 Value: "test", 909 }, 910 }, tableInfo), 911 }, 912 } 913 914 _ = sink.OnTxnEvent(&dmlsink.TxnCallbackableEvent{ 915 Event: &model.SingleTableTxn{Rows: rows}, 916 }) 917 err = sink.Flush(context.Background()) 918 require.Regexp(t, ".*ErrMySQLTxnError.*", err) 919 require.Nil(t, sink.Close()) 920 } 921 922 func TestMysqlSinkSafeModeOff(t *testing.T) { 923 t.Parallel() 924 925 tableInfoWithoutPK := model.BuildTableInfo("common_1", "uk_without_pk", []*model.Column{nil, { 926 Name: "a1", 927 Type: mysql.TypeLong, 928 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 929 Value: 1, 930 }, { 931 Name: "a3", 932 Type: mysql.TypeLong, 933 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 934 Value: 1, 935 }}, [][]int{{1, 2}}) 936 937 tableInfoWithPK := model.BuildTableInfo("common_1", "pk", []*model.Column{nil, { 938 Name: "a1", 939 Type: mysql.TypeLong, 940 Flag: model.HandleKeyFlag | model.MultipleKeyFlag | model.PrimaryKeyFlag, 941 }, { 942 Name: "a3", 943 Type: mysql.TypeLong, 944 Flag: model.BinaryFlag | model.HandleKeyFlag | model.MultipleKeyFlag | model.PrimaryKeyFlag, 945 }}, [][]int{{1, 2}}) 946 947 testCases := []struct { 948 name string 949 input []*model.RowChangedEvent 950 expected *preparedDMLs 951 }{ 952 { 953 name: "empty", 954 input: []*model.RowChangedEvent{}, 955 expected: &preparedDMLs{ 956 startTs: []model.Ts{}, 957 sqls: []string{}, 958 values: [][]interface{}{}, 959 }, 960 }, { 961 name: "insert without PK", 962 input: []*model.RowChangedEvent{ 963 { 964 StartTs: 418658114257813514, 965 CommitTs: 418658114257813515, 966 ReplicatingTs: 418658114257813513, 967 TableInfo: tableInfoWithoutPK, 968 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 969 Name: "a1", 970 Value: 1, 971 }, { 972 Name: "a3", 973 Value: 1, 974 }}, tableInfoWithoutPK), 975 }, 976 }, 977 expected: &preparedDMLs{ 978 startTs: []model.Ts{418658114257813514}, 979 sqls: []string{ 980 "INSERT INTO `common_1`.`uk_without_pk` (`a1`,`a3`) VALUES (?,?)", 981 }, 982 values: [][]interface{}{{1, 1}}, 983 rowCount: 1, 984 approximateSize: 63, 985 }, 986 }, { 987 name: "insert with PK", 988 input: []*model.RowChangedEvent{ 989 { 990 StartTs: 418658114257813514, 991 CommitTs: 418658114257813515, 992 ReplicatingTs: 418658114257813513, 993 TableInfo: tableInfoWithPK, 994 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 995 Name: "a1", 996 Value: 1, 997 }, { 998 Name: "a3", 999 Value: 1, 1000 }}, tableInfoWithPK), 1001 }, 1002 }, 1003 expected: &preparedDMLs{ 1004 startTs: []model.Ts{418658114257813514}, 1005 sqls: []string{"INSERT INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)"}, 1006 values: [][]interface{}{{1, 1}}, 1007 rowCount: 1, 1008 approximateSize: 52, 1009 }, 1010 }, { 1011 name: "update without PK", 1012 input: []*model.RowChangedEvent{ 1013 { 1014 StartTs: 418658114257813516, 1015 CommitTs: 418658114257813517, 1016 ReplicatingTs: 418658114257813515, 1017 TableInfo: tableInfoWithoutPK, 1018 PreColumns: model.Columns2ColumnDatas([]*model.Column{nil, { 1019 Name: "a1", 1020 Value: 2, 1021 }, { 1022 Name: "a3", 1023 Value: 2, 1024 }}, tableInfoWithoutPK), 1025 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1026 Name: "a1", 1027 Value: 3, 1028 }, { 1029 Name: "a3", 1030 Value: 3, 1031 }}, tableInfoWithoutPK), 1032 }, 1033 }, 1034 expected: &preparedDMLs{ 1035 startTs: []model.Ts{418658114257813516}, 1036 sqls: []string{ 1037 "UPDATE `common_1`.`uk_without_pk` SET `a1` = ?, `a3` = ? " + 1038 "WHERE `a1` = ? AND `a3` = ? LIMIT 1", 1039 }, 1040 values: [][]interface{}{{3, 3, 2, 2}}, 1041 rowCount: 1, 1042 approximateSize: 92, 1043 }, 1044 }, { 1045 name: "update with PK", 1046 input: []*model.RowChangedEvent{ 1047 { 1048 StartTs: 418658114257813516, 1049 CommitTs: 418658114257813517, 1050 ReplicatingTs: 418658114257813515, 1051 TableInfo: tableInfoWithPK, 1052 PreColumns: model.Columns2ColumnDatas([]*model.Column{nil, { 1053 Name: "a1", 1054 Value: 2, 1055 }, { 1056 Name: "a3", 1057 Value: 2, 1058 }}, tableInfoWithPK), 1059 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1060 Name: "a1", 1061 Value: 3, 1062 }, { 1063 Name: "a3", 1064 Value: 3, 1065 }}, tableInfoWithPK), 1066 }, 1067 }, 1068 expected: &preparedDMLs{ 1069 startTs: []model.Ts{418658114257813516}, 1070 sqls: []string{"UPDATE `common_1`.`pk` SET `a1` = ?, `a3` = ? " + 1071 "WHERE `a1` = ? AND `a3` = ? LIMIT 1"}, 1072 values: [][]interface{}{{3, 3, 2, 2}}, 1073 rowCount: 1, 1074 approximateSize: 81, 1075 }, 1076 }, { 1077 name: "batch insert with PK", 1078 input: []*model.RowChangedEvent{ 1079 { 1080 StartTs: 418658114257813516, 1081 CommitTs: 418658114257813517, 1082 ReplicatingTs: 418658114257813515, 1083 TableInfo: tableInfoWithPK, 1084 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1085 Name: "a1", 1086 Value: 3, 1087 }, { 1088 Name: "a3", 1089 Value: 3, 1090 }}, tableInfoWithPK), 1091 }, 1092 { 1093 StartTs: 418658114257813516, 1094 CommitTs: 418658114257813517, 1095 ReplicatingTs: 418658114257813515, 1096 TableInfo: tableInfoWithPK, 1097 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1098 Name: "a1", 1099 Value: 5, 1100 }, { 1101 Name: "a3", 1102 Value: 5, 1103 }}, tableInfoWithPK), 1104 }, 1105 }, 1106 expected: &preparedDMLs{ 1107 startTs: []model.Ts{418658114257813516}, 1108 sqls: []string{ 1109 "INSERT INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)", 1110 "INSERT INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)", 1111 }, 1112 values: [][]interface{}{{3, 3}, {5, 5}}, 1113 rowCount: 2, 1114 approximateSize: 104, 1115 }, 1116 }, { 1117 name: "safe mode on commit ts < replicating ts", 1118 input: []*model.RowChangedEvent{ 1119 { 1120 StartTs: 418658114257813516, 1121 CommitTs: 418658114257813517, 1122 ReplicatingTs: 418658114257813518, 1123 TableInfo: tableInfoWithPK, 1124 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1125 Name: "a1", 1126 Value: 3, 1127 }, { 1128 Name: "a3", 1129 Value: 3, 1130 }}, tableInfoWithPK), 1131 }, 1132 }, 1133 expected: &preparedDMLs{ 1134 startTs: []model.Ts{418658114257813516}, 1135 sqls: []string{ 1136 "REPLACE INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)", 1137 }, 1138 values: [][]interface{}{{3, 3}}, 1139 rowCount: 1, 1140 approximateSize: 53, 1141 }, 1142 }, { 1143 name: "safe mode on and txn's commit ts < replicating ts", 1144 input: []*model.RowChangedEvent{ 1145 { 1146 StartTs: 418658114257813516, 1147 CommitTs: 418658114257813517, 1148 ReplicatingTs: 418658114257813518, 1149 TableInfo: tableInfoWithPK, 1150 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1151 Name: "a1", 1152 Value: 3, 1153 }, { 1154 Name: "a3", 1155 Value: 3, 1156 }}, tableInfoWithPK), 1157 }, 1158 { 1159 StartTs: 418658114257813516, 1160 CommitTs: 418658114257813517, 1161 ReplicatingTs: 418658114257813518, 1162 TableInfo: tableInfoWithPK, 1163 Columns: model.Columns2ColumnDatas([]*model.Column{nil, { 1164 Name: "a1", 1165 Type: mysql.TypeLong, 1166 Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, 1167 Value: 5, 1168 }, { 1169 Name: "a3", 1170 Type: mysql.TypeLong, 1171 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag, 1172 Value: 5, 1173 }}, tableInfoWithPK), 1174 }, 1175 }, 1176 expected: &preparedDMLs{ 1177 startTs: []model.Ts{418658114257813516}, 1178 sqls: []string{ 1179 "REPLACE INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)", 1180 "REPLACE INTO `common_1`.`pk` (`a1`,`a3`) VALUES (?,?)", 1181 }, 1182 values: [][]interface{}{{3, 3}, {5, 5}}, 1183 rowCount: 2, 1184 approximateSize: 106, 1185 }, 1186 }, 1187 } 1188 ctx, cancel := context.WithCancel(context.Background()) 1189 defer cancel() 1190 ms := newMySQLBackendWithoutDB(ctx) 1191 ms.cfg.SafeMode = false 1192 for _, tc := range testCases { 1193 ms.events = make([]*dmlsink.TxnCallbackableEvent, 1) 1194 ms.events[0] = &dmlsink.TxnCallbackableEvent{ 1195 Event: &model.SingleTableTxn{Rows: tc.input}, 1196 } 1197 ms.rows = len(tc.input) 1198 dmls := ms.prepareDMLs() 1199 require.Equal(t, tc.expected, dmls, tc.name) 1200 } 1201 } 1202 1203 func TestPrepareBatchDMLs(t *testing.T) { 1204 t.Parallel() 1205 tableInfoWithoutPK := model.BuildTableInfo("common_1", "uk_without_pk", []*model.Column{{ 1206 Name: "a1", 1207 Type: mysql.TypeLong, 1208 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 1209 }, { 1210 Name: "a3", 1211 Type: mysql.TypeVarchar, 1212 Charset: charset.CharsetGBK, 1213 Flag: model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 1214 }}, [][]int{{0, 1}}) 1215 testCases := []struct { 1216 isTiDB bool 1217 input []*model.RowChangedEvent 1218 expected *preparedDMLs 1219 }{ 1220 // empty event 1221 { 1222 isTiDB: true, 1223 input: []*model.RowChangedEvent{}, 1224 expected: &preparedDMLs{ 1225 startTs: []model.Ts{}, 1226 sqls: []string{}, 1227 values: [][]interface{}{}, 1228 }, 1229 }, 1230 { // delete event 1231 isTiDB: false, 1232 input: []*model.RowChangedEvent{ 1233 { 1234 StartTs: 418658114257813514, 1235 CommitTs: 418658114257813515, 1236 TableInfo: tableInfoWithoutPK, 1237 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1238 Name: "a1", 1239 Value: 1, 1240 }, { 1241 Name: "a3", 1242 Value: []byte("你好"), 1243 }}, tableInfoWithoutPK), 1244 ApproximateDataSize: 10, 1245 }, 1246 { 1247 StartTs: 418658114257813514, 1248 CommitTs: 418658114257813515, 1249 TableInfo: tableInfoWithoutPK, 1250 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1251 Name: "a1", 1252 Value: 2, 1253 }, { 1254 Name: "a3", 1255 Value: []byte("世界"), 1256 }}, tableInfoWithoutPK), 1257 ApproximateDataSize: 10, 1258 }, 1259 }, 1260 expected: &preparedDMLs{ 1261 startTs: []model.Ts{418658114257813514}, 1262 sqls: []string{"DELETE FROM `common_1`.`uk_without_pk` WHERE (`a1` = ? AND `a3` = ?) OR (`a1` = ? AND `a3` = ?)"}, 1263 values: [][]interface{}{{1, "你好", 2, "世界"}}, 1264 rowCount: 2, 1265 approximateSize: 115, 1266 }, 1267 }, 1268 { // insert event 1269 isTiDB: true, 1270 input: []*model.RowChangedEvent{ 1271 { 1272 StartTs: 418658114257813516, 1273 CommitTs: 418658114257813517, 1274 TableInfo: tableInfoWithoutPK, 1275 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1276 Name: "a1", 1277 Value: 1, 1278 }, { 1279 Name: "a3", 1280 Value: "你好", 1281 }}, tableInfoWithoutPK), 1282 ApproximateDataSize: 10, 1283 }, 1284 { 1285 StartTs: 418658114257813516, 1286 CommitTs: 418658114257813517, 1287 TableInfo: tableInfoWithoutPK, 1288 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1289 Name: "a1", 1290 Value: 2, 1291 }, { 1292 Name: "a3", 1293 Value: "世界", 1294 }}, tableInfoWithoutPK), 1295 ApproximateDataSize: 10, 1296 }, 1297 }, 1298 expected: &preparedDMLs{ 1299 startTs: []model.Ts{418658114257813516}, 1300 sqls: []string{"INSERT INTO `common_1`.`uk_without_pk` (`a1`,`a3`) VALUES (?,?),(?,?)"}, 1301 values: [][]interface{}{{1, "你好", 2, "世界"}}, 1302 rowCount: 2, 1303 approximateSize: 89, 1304 }, 1305 }, 1306 // update event 1307 { 1308 isTiDB: true, 1309 input: []*model.RowChangedEvent{ 1310 { 1311 StartTs: 418658114257813516, 1312 CommitTs: 418658114257813517, 1313 TableInfo: tableInfoWithoutPK, 1314 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1315 Name: "a1", 1316 Value: 1, 1317 }, { 1318 Name: "a3", 1319 Value: []byte("开发"), 1320 }}, tableInfoWithoutPK), 1321 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1322 Name: "a1", 1323 Value: 2, 1324 }, { 1325 Name: "a3", 1326 Value: []byte("测试"), 1327 }}, tableInfoWithoutPK), 1328 ApproximateDataSize: 12, 1329 }, 1330 { 1331 StartTs: 418658114257813516, 1332 CommitTs: 418658114257813517, 1333 TableInfo: tableInfoWithoutPK, 1334 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1335 Name: "a1", 1336 Value: 3, 1337 }, { 1338 Name: "a3", 1339 Value: []byte("纽约"), 1340 }}, tableInfoWithoutPK), 1341 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1342 Name: "a1", 1343 Value: 4, 1344 }, { 1345 Name: "a3", 1346 Value: []byte("北京"), 1347 }}, tableInfoWithoutPK), 1348 ApproximateDataSize: 12, 1349 }, 1350 }, 1351 expected: &preparedDMLs{ 1352 startTs: []model.Ts{418658114257813516}, 1353 sqls: []string{"UPDATE `common_1`.`uk_without_pk` " + 1354 "SET `a1`=CASE WHEN `a1` = ? AND `a3` = ? THEN ? WHEN `a1` = ? AND `a3` = ? THEN ? END, " + 1355 "`a3`=CASE WHEN `a1` = ? AND `a3` = ? THEN ? WHEN `a1` = ? AND `a3` = ? THEN ? END " + 1356 "WHERE (`a1` = ? AND `a3` = ?) OR (`a1` = ? AND `a3` = ?)"}, 1357 values: [][]interface{}{{ 1358 1, "开发", 2, 3, "纽约", 4, 1, "开发", "测试", 3, 1359 "纽约", "北京", 1, "开发", 3, "纽约", 1360 }}, 1361 rowCount: 2, 1362 approximateSize: 283, 1363 }, 1364 }, 1365 // mixed event, and test delete, update, insert are ordered 1366 { 1367 isTiDB: true, 1368 input: []*model.RowChangedEvent{ 1369 { 1370 StartTs: 418658114257813514, 1371 CommitTs: 418658114257813515, 1372 TableInfo: tableInfoWithoutPK, 1373 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1374 Name: "a1", 1375 Value: 2, 1376 }, { 1377 Name: "a3", 1378 Value: []byte("你好"), 1379 }}, tableInfoWithoutPK), 1380 ApproximateDataSize: 10, 1381 }, 1382 { 1383 StartTs: 418658114257813514, 1384 CommitTs: 418658114257813515, 1385 TableInfo: tableInfoWithoutPK, 1386 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1387 Name: "a1", 1388 Value: 1, 1389 }, { 1390 Name: "a3", 1391 Value: []byte("世界"), 1392 }}, tableInfoWithoutPK), 1393 ApproximateDataSize: 10, 1394 }, 1395 { 1396 StartTs: 418658114257813514, 1397 CommitTs: 418658114257813515, 1398 TableInfo: tableInfoWithoutPK, 1399 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1400 Name: "a1", 1401 Value: 2, 1402 }, { 1403 Name: "a3", 1404 Value: "你好", 1405 }}, tableInfoWithoutPK), 1406 ApproximateDataSize: 10, 1407 }, 1408 { 1409 StartTs: 418658114257813516, 1410 CommitTs: 418658114257813517, 1411 TableInfo: tableInfoWithoutPK, 1412 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1413 Name: "a1", 1414 Value: 1, 1415 }, { 1416 Name: "a3", 1417 Value: []byte("开发"), 1418 }}, tableInfoWithoutPK), 1419 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1420 Name: "a1", 1421 Value: 2, 1422 }, { 1423 Name: "a3", 1424 Value: []byte("测试"), 1425 }}, tableInfoWithoutPK), 1426 ApproximateDataSize: 10, 1427 }, 1428 { 1429 StartTs: 418658114257813516, 1430 CommitTs: 418658114257813517, 1431 TableInfo: tableInfoWithoutPK, 1432 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1433 Name: "a1", 1434 Value: 3, 1435 }, { 1436 Name: "a3", 1437 Value: []byte("纽约"), 1438 }}, tableInfoWithoutPK), 1439 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1440 Name: "a1", 1441 Value: 4, 1442 }, { 1443 Name: "a3", 1444 Value: []byte("北京"), 1445 }}, tableInfoWithoutPK), 1446 ApproximateDataSize: 10, 1447 }, 1448 }, 1449 expected: &preparedDMLs{ 1450 startTs: []model.Ts{418658114257813514}, 1451 sqls: []string{ 1452 "DELETE FROM `common_1`.`uk_without_pk` WHERE (`a1` = ? AND `a3` = ?) OR (`a1` = ? AND `a3` = ?)", 1453 "UPDATE `common_1`.`uk_without_pk` " + 1454 "SET `a1`=CASE WHEN `a1` = ? AND `a3` = ? THEN ? WHEN `a1` = ? AND `a3` = ? THEN ? END, " + 1455 "`a3`=CASE WHEN `a1` = ? AND `a3` = ? THEN ? WHEN `a1` = ? AND `a3` = ? THEN ? END " + 1456 "WHERE (`a1` = ? AND `a3` = ?) OR (`a1` = ? AND `a3` = ?)", 1457 "INSERT INTO `common_1`.`uk_without_pk` (`a1`,`a3`) VALUES (?,?)", 1458 }, 1459 values: [][]interface{}{ 1460 {1, "世界", 2, "你好"}, 1461 { 1462 1, "开发", 2, 3, "纽约", 4, 1, "开发", "测试", 3, 1463 "纽约", "北京", 1, "开发", 3, "纽约", 1464 }, 1465 {2, "你好"}, 1466 }, 1467 rowCount: 5, 1468 approximateSize: 467, 1469 }, 1470 }, 1471 // update event and downstream is mysql and without pk 1472 { 1473 isTiDB: false, 1474 input: []*model.RowChangedEvent{ 1475 { 1476 StartTs: 418658114257813516, 1477 CommitTs: 418658114257813517, 1478 TableInfo: tableInfoWithoutPK, 1479 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1480 Name: "a1", 1481 Value: 1, 1482 }, { 1483 Name: "a3", 1484 Value: []byte("开发"), 1485 }}, tableInfoWithoutPK), 1486 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1487 Name: "a1", 1488 Value: 2, 1489 }, { 1490 Name: "a3", 1491 Value: []byte("测试"), 1492 }}, tableInfoWithoutPK), 1493 ApproximateDataSize: 10, 1494 }, 1495 { 1496 StartTs: 418658114257813516, 1497 CommitTs: 418658114257813517, 1498 TableInfo: tableInfoWithoutPK, 1499 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1500 Name: "a1", 1501 Value: 3, 1502 }, { 1503 Name: "a3", 1504 Value: []byte("纽约"), 1505 }}, tableInfoWithoutPK), 1506 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1507 Name: "a1", 1508 Value: 4, 1509 }, { 1510 Name: "a3", 1511 Value: []byte("北京"), 1512 }}, tableInfoWithoutPK), 1513 ApproximateDataSize: 10, 1514 }, 1515 }, 1516 expected: &preparedDMLs{ 1517 startTs: []model.Ts{418658114257813516}, 1518 sqls: []string{ 1519 "UPDATE `common_1`.`uk_without_pk` SET `a1` = ?, " + 1520 "`a3` = ? WHERE `a1` = ? AND `a3` = ? LIMIT 1", 1521 "UPDATE `common_1`.`uk_without_pk` SET `a1` = ?, " + 1522 "`a3` = ? WHERE `a1` = ? AND `a3` = ? LIMIT 1", 1523 }, 1524 values: [][]interface{}{{2, "测试", 1, "开发"}, {4, "北京", 3, "纽约"}}, 1525 rowCount: 2, 1526 approximateSize: 204, 1527 }, 1528 }, 1529 } 1530 1531 ctx, cancel := context.WithCancel(context.Background()) 1532 defer cancel() 1533 ms := newMySQLBackendWithoutDB(ctx) 1534 ms.cfg.BatchDMLEnable = true 1535 ms.cfg.SafeMode = false 1536 for _, tc := range testCases { 1537 ms.cfg.IsTiDB = tc.isTiDB 1538 ms.events = make([]*dmlsink.TxnCallbackableEvent, 1) 1539 ms.events[0] = &dmlsink.TxnCallbackableEvent{ 1540 Event: &model.SingleTableTxn{Rows: tc.input}, 1541 } 1542 ms.rows = len(tc.input) 1543 dmls := ms.prepareDMLs() 1544 require.Equal(t, tc.expected, dmls) 1545 } 1546 } 1547 1548 func TestGroupRowsByType(t *testing.T) { 1549 ctx := context.Background() 1550 ms := newMySQLBackendWithoutDB(ctx) 1551 tableInfoWithoutPK := model.BuildTableInfo("common_1", "uk_without_pk", []*model.Column{{ 1552 Name: "a1", 1553 Type: mysql.TypeLong, 1554 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 1555 }, { 1556 Name: "a3", 1557 Type: mysql.TypeLong, 1558 Flag: model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag | model.UniqueKeyFlag, 1559 }}, [][]int{{0, 1}}) 1560 testCases := []struct { 1561 name string 1562 input []*model.RowChangedEvent 1563 maxTxnRow int 1564 }{ 1565 { 1566 name: "delete", 1567 input: []*model.RowChangedEvent{ 1568 { 1569 StartTs: 418658114257813514, 1570 CommitTs: 418658114257813515, 1571 TableInfo: tableInfoWithoutPK, 1572 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1573 Name: "a1", 1574 Value: 1, 1575 }, { 1576 Name: "a3", 1577 Value: 1, 1578 }}, tableInfoWithoutPK), 1579 }, 1580 { 1581 StartTs: 418658114257813514, 1582 CommitTs: 418658114257813515, 1583 TableInfo: tableInfoWithoutPK, 1584 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1585 Name: "a1", 1586 Value: 2, 1587 }, { 1588 Name: "a3", 1589 Value: 2, 1590 }}, tableInfoWithoutPK), 1591 }, 1592 { 1593 StartTs: 418658114257813514, 1594 CommitTs: 418658114257813515, 1595 TableInfo: tableInfoWithoutPK, 1596 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1597 Name: "a1", 1598 Value: 2, 1599 }, { 1600 Name: "a3", 1601 Value: 2, 1602 }}, tableInfoWithoutPK), 1603 }, 1604 { 1605 StartTs: 418658114257813514, 1606 CommitTs: 418658114257813515, 1607 TableInfo: tableInfoWithoutPK, 1608 PreColumns: model.Columns2ColumnDatas([]*model.Column{{ 1609 Name: "a1", 1610 Value: 2, 1611 }, { 1612 Name: "a3", 1613 Value: 2, 1614 }}, tableInfoWithoutPK), 1615 }, 1616 }, 1617 maxTxnRow: 2, 1618 }, 1619 { 1620 name: "insert", 1621 input: []*model.RowChangedEvent{ 1622 { 1623 StartTs: 418658114257813516, 1624 CommitTs: 418658114257813517, 1625 TableInfo: tableInfoWithoutPK, 1626 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1627 Name: "a1", 1628 Value: 1, 1629 }, { 1630 Name: "a3", 1631 Value: 1, 1632 }}, tableInfoWithoutPK), 1633 }, 1634 { 1635 StartTs: 418658114257813516, 1636 CommitTs: 418658114257813517, 1637 TableInfo: tableInfoWithoutPK, 1638 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1639 Name: "a1", 1640 Value: 2, 1641 }, { 1642 Name: "a3", 1643 Value: 2, 1644 }}, tableInfoWithoutPK), 1645 }, 1646 { 1647 StartTs: 418658114257813516, 1648 CommitTs: 418658114257813517, 1649 TableInfo: tableInfoWithoutPK, 1650 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1651 Name: "a1", 1652 Value: 2, 1653 }, { 1654 Name: "a3", 1655 Value: 2, 1656 }}, tableInfoWithoutPK), 1657 }, 1658 { 1659 StartTs: 418658114257813516, 1660 CommitTs: 418658114257813517, 1661 TableInfo: tableInfoWithoutPK, 1662 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1663 Name: "a1", 1664 Value: 2, 1665 }, { 1666 Name: "a3", 1667 Value: 2, 1668 }}, tableInfoWithoutPK), 1669 }, 1670 1671 { 1672 StartTs: 418658114257813516, 1673 CommitTs: 418658114257813517, 1674 TableInfo: tableInfoWithoutPK, 1675 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1676 Name: "a1", 1677 Value: 2, 1678 }, { 1679 Name: "a3", 1680 Value: 2, 1681 }}, tableInfoWithoutPK), 1682 }, 1683 1684 { 1685 StartTs: 418658114257813516, 1686 CommitTs: 418658114257813517, 1687 TableInfo: tableInfoWithoutPK, 1688 Columns: model.Columns2ColumnDatas([]*model.Column{{ 1689 Name: "a1", 1690 Value: 2, 1691 }, { 1692 Name: "a3", 1693 Value: 2, 1694 }}, tableInfoWithoutPK), 1695 }, 1696 }, 1697 maxTxnRow: 4, 1698 }, 1699 } 1700 for _, tc := range testCases { 1701 t.Run(tc.name, func(t *testing.T) { 1702 event := &dmlsink.TxnCallbackableEvent{ 1703 Event: &model.SingleTableTxn{ 1704 TableInfo: testCases[0].input[0].TableInfo, 1705 Rows: testCases[0].input, 1706 }, 1707 } 1708 ms.cfg.MaxTxnRow = tc.maxTxnRow 1709 inserts, updates, deletes := ms.groupRowsByType(event, event.Event.TableInfo) 1710 for _, rows := range inserts { 1711 require.LessOrEqual(t, len(rows), tc.maxTxnRow) 1712 } 1713 for _, rows := range updates { 1714 require.LessOrEqual(t, len(rows), tc.maxTxnRow) 1715 } 1716 for _, rows := range deletes { 1717 require.LessOrEqual(t, len(rows), tc.maxTxnRow) 1718 } 1719 }) 1720 } 1721 } 1722 1723 func TestBackendGenUpdateSQL(t *testing.T) { 1724 ctx := context.Background() 1725 ms := newMySQLBackendWithoutDB(ctx) 1726 table := &model.TableName{Schema: "db", Table: "tb1"} 1727 1728 createSQL := "CREATE TABLE tb1 (id INT PRIMARY KEY, name varchar(20))" 1729 stmt, err := parser.New().ParseOneStmt(createSQL, "", "") 1730 require.NoError(t, err) 1731 ti, err := ddl.BuildTableInfoFromAST(stmt.(*ast.CreateTableStmt)) 1732 require.NoError(t, err) 1733 1734 row1 := sqlmodel.NewRowChange(table, table, []any{1, "a"}, []any{1, "aa"}, ti, ti, nil) 1735 row1.SetApproximateDataSize(6) 1736 row2 := sqlmodel.NewRowChange(table, table, []any{2, "b"}, []any{2, "bb"}, ti, ti, nil) 1737 row2.SetApproximateDataSize(6) 1738 1739 testCases := []struct { 1740 rows []*sqlmodel.RowChange 1741 maxMultiUpdateRowSize int 1742 expectedSQLs []string 1743 expectedValues [][]interface{} 1744 }{ 1745 { 1746 []*sqlmodel.RowChange{row1, row2}, 1747 ms.cfg.MaxMultiUpdateRowCount, 1748 []string{ 1749 "UPDATE `db`.`tb1` SET " + 1750 "`id`=CASE WHEN `id` = ? THEN ? WHEN `id` = ? THEN ? END, " + 1751 "`name`=CASE WHEN `id` = ? THEN ? WHEN `id` = ? THEN ? END " + 1752 "WHERE (`id` = ?) OR (`id` = ?)", 1753 }, 1754 [][]interface{}{ 1755 {1, 1, 2, 2, 1, "aa", 2, "bb", 1, 2}, 1756 }, 1757 }, 1758 { 1759 []*sqlmodel.RowChange{row1, row2}, 1760 0, 1761 []string{ 1762 "UPDATE `db`.`tb1` SET `id` = ?, `name` = ? WHERE `id` = ? LIMIT 1", 1763 "UPDATE `db`.`tb1` SET `id` = ?, `name` = ? WHERE `id` = ? LIMIT 1", 1764 }, 1765 [][]interface{}{ 1766 {1, "aa", 1}, 1767 {2, "bb", 2}, 1768 }, 1769 }, 1770 } 1771 for _, tc := range testCases { 1772 ms.cfg.MaxMultiUpdateRowSize = tc.maxMultiUpdateRowSize 1773 sqls, values := ms.genUpdateSQL(tc.rows...) 1774 require.Equal(t, tc.expectedSQLs, sqls) 1775 require.Equal(t, tc.expectedValues, values) 1776 } 1777 }