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  }