github.com/pingcap/ticdc@v0.0.0-20220526033649-485a10ef2652/cdc/sink/mysql_test.go (about)

     1  // Copyright 2020 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 sink
    15  
    16  import (
    17  	"context"
    18  	"database/sql"
    19  	"database/sql/driver"
    20  	"fmt"
    21  	"net"
    22  	"net/url"
    23  	"sort"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/DATA-DOG/go-sqlmock"
    30  	"github.com/davecgh/go-spew/spew"
    31  	dmysql "github.com/go-sql-driver/mysql"
    32  	"github.com/pingcap/check"
    33  	"github.com/pingcap/errors"
    34  	timodel "github.com/pingcap/parser/model"
    35  	"github.com/pingcap/parser/mysql"
    36  	"github.com/pingcap/ticdc/cdc/model"
    37  	"github.com/pingcap/ticdc/cdc/sink/common"
    38  	"github.com/pingcap/ticdc/pkg/config"
    39  	"github.com/pingcap/ticdc/pkg/cyclic/mark"
    40  	cerror "github.com/pingcap/ticdc/pkg/errors"
    41  	"github.com/pingcap/ticdc/pkg/filter"
    42  	"github.com/pingcap/ticdc/pkg/notify"
    43  	"github.com/pingcap/ticdc/pkg/retry"
    44  	"github.com/pingcap/ticdc/pkg/util/testleak"
    45  	"github.com/pingcap/tidb/infoschema"
    46  	"golang.org/x/sync/errgroup"
    47  )
    48  
    49  type MySQLSinkSuite struct{}
    50  
    51  func Test(t *testing.T) { check.TestingT(t) }
    52  
    53  var _ = check.Suite(&MySQLSinkSuite{})
    54  
    55  func newMySQLSink4Test(ctx context.Context, c *check.C) *mysqlSink {
    56  	f, err := filter.NewFilter(config.GetDefaultReplicaConfig())
    57  	c.Assert(err, check.IsNil)
    58  	params := defaultParams.Clone()
    59  	params.batchReplaceEnabled = false
    60  	return &mysqlSink{
    61  		txnCache:   common.NewUnresolvedTxnCache(),
    62  		filter:     f,
    63  		statistics: NewStatistics(ctx, "test", make(map[string]string)),
    64  		params:     params,
    65  	}
    66  }
    67  
    68  func (s MySQLSinkSuite) TestMysqlSinkWorker(c *check.C) {
    69  	defer testleak.AfterTest(c)()
    70  	testCases := []struct {
    71  		txns                     []*model.SingleTableTxn
    72  		expectedOutputRows       [][]*model.RowChangedEvent
    73  		exportedOutputReplicaIDs []uint64
    74  		maxTxnRow                int
    75  	}{
    76  		{
    77  			txns:      []*model.SingleTableTxn{},
    78  			maxTxnRow: 4,
    79  		}, {
    80  			txns: []*model.SingleTableTxn{
    81  				{
    82  					CommitTs:  1,
    83  					Rows:      []*model.RowChangedEvent{{CommitTs: 1}},
    84  					ReplicaID: 1,
    85  				},
    86  			},
    87  			expectedOutputRows:       [][]*model.RowChangedEvent{{{CommitTs: 1}}},
    88  			exportedOutputReplicaIDs: []uint64{1},
    89  			maxTxnRow:                2,
    90  		}, {
    91  			txns: []*model.SingleTableTxn{
    92  				{
    93  					CommitTs:  1,
    94  					Rows:      []*model.RowChangedEvent{{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 1}},
    95  					ReplicaID: 1,
    96  				},
    97  			},
    98  			expectedOutputRows: [][]*model.RowChangedEvent{
    99  				{{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 1}},
   100  			},
   101  			exportedOutputReplicaIDs: []uint64{1},
   102  			maxTxnRow:                2,
   103  		}, {
   104  			txns: []*model.SingleTableTxn{
   105  				{
   106  					CommitTs:  1,
   107  					Rows:      []*model.RowChangedEvent{{CommitTs: 1}, {CommitTs: 1}},
   108  					ReplicaID: 1,
   109  				},
   110  				{
   111  					CommitTs:  2,
   112  					Rows:      []*model.RowChangedEvent{{CommitTs: 2}},
   113  					ReplicaID: 1,
   114  				},
   115  				{
   116  					CommitTs:  3,
   117  					Rows:      []*model.RowChangedEvent{{CommitTs: 3}, {CommitTs: 3}},
   118  					ReplicaID: 1,
   119  				},
   120  			},
   121  			expectedOutputRows: [][]*model.RowChangedEvent{
   122  				{{CommitTs: 1}, {CommitTs: 1}, {CommitTs: 2}},
   123  				{{CommitTs: 3}, {CommitTs: 3}},
   124  			},
   125  			exportedOutputReplicaIDs: []uint64{1, 1},
   126  			maxTxnRow:                4,
   127  		}, {
   128  			txns: []*model.SingleTableTxn{
   129  				{
   130  					CommitTs:  1,
   131  					Rows:      []*model.RowChangedEvent{{CommitTs: 1}},
   132  					ReplicaID: 1,
   133  				},
   134  				{
   135  					CommitTs:  2,
   136  					Rows:      []*model.RowChangedEvent{{CommitTs: 2}},
   137  					ReplicaID: 2,
   138  				},
   139  				{
   140  					CommitTs:  3,
   141  					Rows:      []*model.RowChangedEvent{{CommitTs: 3}},
   142  					ReplicaID: 3,
   143  				},
   144  			},
   145  			expectedOutputRows: [][]*model.RowChangedEvent{
   146  				{{CommitTs: 1}},
   147  				{{CommitTs: 2}},
   148  				{{CommitTs: 3}},
   149  			},
   150  			exportedOutputReplicaIDs: []uint64{1, 2, 3},
   151  			maxTxnRow:                4,
   152  		}, {
   153  			txns: []*model.SingleTableTxn{
   154  				{
   155  					CommitTs:  1,
   156  					Rows:      []*model.RowChangedEvent{{CommitTs: 1}},
   157  					ReplicaID: 1,
   158  				},
   159  				{
   160  					CommitTs:  2,
   161  					Rows:      []*model.RowChangedEvent{{CommitTs: 2}, {CommitTs: 2}, {CommitTs: 2}},
   162  					ReplicaID: 1,
   163  				},
   164  				{
   165  					CommitTs:  3,
   166  					Rows:      []*model.RowChangedEvent{{CommitTs: 3}},
   167  					ReplicaID: 1,
   168  				},
   169  				{
   170  					CommitTs:  4,
   171  					Rows:      []*model.RowChangedEvent{{CommitTs: 4}},
   172  					ReplicaID: 1,
   173  				},
   174  			},
   175  			expectedOutputRows: [][]*model.RowChangedEvent{
   176  				{{CommitTs: 1}},
   177  				{{CommitTs: 2}, {CommitTs: 2}, {CommitTs: 2}},
   178  				{{CommitTs: 3}, {CommitTs: 4}},
   179  			},
   180  			exportedOutputReplicaIDs: []uint64{1, 1, 1},
   181  			maxTxnRow:                2,
   182  		},
   183  	}
   184  	ctx := context.Background()
   185  
   186  	notifier := new(notify.Notifier)
   187  	for i, tc := range testCases {
   188  		cctx, cancel := context.WithCancel(ctx)
   189  		var outputRows [][]*model.RowChangedEvent
   190  		var outputReplicaIDs []uint64
   191  		receiver, err := notifier.NewReceiver(-1)
   192  		c.Assert(err, check.IsNil)
   193  		w := newMySQLSinkWorker(tc.maxTxnRow, 1,
   194  			bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"),
   195  			receiver,
   196  			func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error {
   197  				outputRows = append(outputRows, events)
   198  				outputReplicaIDs = append(outputReplicaIDs, replicaID)
   199  				return nil
   200  			})
   201  		errg, cctx := errgroup.WithContext(cctx)
   202  		errg.Go(func() error {
   203  			return w.run(cctx)
   204  		})
   205  		for _, txn := range tc.txns {
   206  			w.appendTxn(cctx, txn)
   207  		}
   208  		var wg sync.WaitGroup
   209  		w.appendFinishTxn(&wg)
   210  		// ensure all txns are fetched from txn channel in sink worker
   211  		time.Sleep(time.Millisecond * 100)
   212  		notifier.Notify()
   213  		wg.Wait()
   214  		cancel()
   215  		c.Assert(errors.Cause(errg.Wait()), check.Equals, context.Canceled)
   216  		c.Assert(outputRows, check.DeepEquals, tc.expectedOutputRows,
   217  			check.Commentf("case %v, %s, %s", i, spew.Sdump(outputRows), spew.Sdump(tc.expectedOutputRows)))
   218  		c.Assert(outputReplicaIDs, check.DeepEquals, tc.exportedOutputReplicaIDs,
   219  			check.Commentf("case %v, %s, %s", i, spew.Sdump(outputReplicaIDs), spew.Sdump(tc.exportedOutputReplicaIDs)))
   220  	}
   221  }
   222  
   223  func (s MySQLSinkSuite) TestMySQLSinkWorkerExitWithError(c *check.C) {
   224  	defer testleak.AfterTest(c)()
   225  	txns1 := []*model.SingleTableTxn{
   226  		{
   227  			CommitTs: 1,
   228  			Rows:     []*model.RowChangedEvent{{CommitTs: 1}},
   229  		},
   230  		{
   231  			CommitTs: 2,
   232  			Rows:     []*model.RowChangedEvent{{CommitTs: 2}},
   233  		},
   234  		{
   235  			CommitTs: 3,
   236  			Rows:     []*model.RowChangedEvent{{CommitTs: 3}},
   237  		},
   238  		{
   239  			CommitTs: 4,
   240  			Rows:     []*model.RowChangedEvent{{CommitTs: 4}},
   241  		},
   242  	}
   243  	txns2 := []*model.SingleTableTxn{
   244  		{
   245  			CommitTs: 5,
   246  			Rows:     []*model.RowChangedEvent{{CommitTs: 5}},
   247  		},
   248  		{
   249  			CommitTs: 6,
   250  			Rows:     []*model.RowChangedEvent{{CommitTs: 6}},
   251  		},
   252  	}
   253  	maxTxnRow := 1
   254  	ctx := context.Background()
   255  
   256  	errExecFailed := errors.New("sink worker exec failed")
   257  	notifier := new(notify.Notifier)
   258  	cctx, cancel := context.WithCancel(ctx)
   259  	receiver, err := notifier.NewReceiver(-1)
   260  	c.Assert(err, check.IsNil)
   261  	w := newMySQLSinkWorker(maxTxnRow, 1, /*bucket*/
   262  		bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"),
   263  		receiver,
   264  		func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error {
   265  			return errExecFailed
   266  		})
   267  	errg, cctx := errgroup.WithContext(cctx)
   268  	errg.Go(func() error {
   269  		return w.run(cctx)
   270  	})
   271  	// txn in txns1 will be sent to worker txnCh
   272  	for _, txn := range txns1 {
   273  		w.appendTxn(cctx, txn)
   274  	}
   275  
   276  	// simulate notify sink worker to flush existing txns
   277  	var wg sync.WaitGroup
   278  	w.appendFinishTxn(&wg)
   279  	time.Sleep(time.Millisecond * 100)
   280  	// txn in txn2 will be blocked since the worker has exited
   281  	for _, txn := range txns2 {
   282  		w.appendTxn(cctx, txn)
   283  	}
   284  	notifier.Notify()
   285  
   286  	// simulate sink shutdown and send closed singal to sink worker
   287  	w.closedCh <- struct{}{}
   288  	w.cleanup()
   289  
   290  	// the flush notification wait group should be done
   291  	wg.Wait()
   292  
   293  	cancel()
   294  	c.Assert(errg.Wait(), check.Equals, errExecFailed)
   295  }
   296  
   297  func (s MySQLSinkSuite) TestMySQLSinkWorkerExitCleanup(c *check.C) {
   298  	defer testleak.AfterTest(c)()
   299  	txns1 := []*model.SingleTableTxn{
   300  		{
   301  			CommitTs: 1,
   302  			Rows:     []*model.RowChangedEvent{{CommitTs: 1}},
   303  		},
   304  		{
   305  			CommitTs: 2,
   306  			Rows:     []*model.RowChangedEvent{{CommitTs: 2}},
   307  		},
   308  	}
   309  	txns2 := []*model.SingleTableTxn{
   310  		{
   311  			CommitTs: 5,
   312  			Rows:     []*model.RowChangedEvent{{CommitTs: 5}},
   313  		},
   314  	}
   315  
   316  	maxTxnRow := 1
   317  	ctx := context.Background()
   318  
   319  	errExecFailed := errors.New("sink worker exec failed")
   320  	notifier := new(notify.Notifier)
   321  	cctx, cancel := context.WithCancel(ctx)
   322  	receiver, err := notifier.NewReceiver(-1)
   323  	c.Assert(err, check.IsNil)
   324  	w := newMySQLSinkWorker(maxTxnRow, 1, /*bucket*/
   325  		bucketSizeCounter.WithLabelValues("capture", "changefeed", "1"),
   326  		receiver,
   327  		func(ctx context.Context, events []*model.RowChangedEvent, replicaID uint64, bucket int) error {
   328  			return errExecFailed
   329  		})
   330  	errg, cctx := errgroup.WithContext(cctx)
   331  	errg.Go(func() error {
   332  		err := w.run(cctx)
   333  		return err
   334  	})
   335  	for _, txn := range txns1 {
   336  		w.appendTxn(cctx, txn)
   337  	}
   338  
   339  	// sleep to let txns flushed by tick
   340  	time.Sleep(time.Millisecond * 100)
   341  
   342  	// simulate more txns are sent to txnCh after the sink worker run has exited
   343  	for _, txn := range txns2 {
   344  		w.appendTxn(cctx, txn)
   345  	}
   346  	var wg sync.WaitGroup
   347  	w.appendFinishTxn(&wg)
   348  	notifier.Notify()
   349  
   350  	// simulate sink shutdown and send closed singal to sink worker
   351  	w.closedCh <- struct{}{}
   352  	w.cleanup()
   353  
   354  	// the flush notification wait group should be done
   355  	wg.Wait()
   356  
   357  	cancel()
   358  	c.Assert(errg.Wait(), check.Equals, errExecFailed)
   359  }
   360  
   361  func (s MySQLSinkSuite) TestPrepareDML(c *check.C) {
   362  	defer testleak.AfterTest(c)()
   363  	testCases := []struct {
   364  		input    []*model.RowChangedEvent
   365  		expected *preparedDMLs
   366  	}{{
   367  		input:    []*model.RowChangedEvent{},
   368  		expected: &preparedDMLs{sqls: []string{}, values: [][]interface{}{}},
   369  	}, {
   370  		input: []*model.RowChangedEvent{
   371  			{
   372  				StartTs:  418658114257813514,
   373  				CommitTs: 418658114257813515,
   374  				Table:    &model.TableName{Schema: "common_1", Table: "uk_without_pk"},
   375  				PreColumns: []*model.Column{nil, {
   376  					Name:  "a1",
   377  					Type:  mysql.TypeLong,
   378  					Flag:  model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag,
   379  					Value: 1,
   380  				}, {
   381  					Name:  "a3",
   382  					Type:  mysql.TypeLong,
   383  					Flag:  model.BinaryFlag | model.MultipleKeyFlag | model.HandleKeyFlag,
   384  					Value: 1,
   385  				}},
   386  				IndexColumns: [][]int{{1, 2}},
   387  			},
   388  		},
   389  		expected: &preparedDMLs{
   390  			sqls:     []string{"DELETE FROM `common_1`.`uk_without_pk` WHERE `a1` = ? AND `a3` = ? LIMIT 1;"},
   391  			values:   [][]interface{}{{1, 1}},
   392  			rowCount: 1,
   393  		},
   394  	}}
   395  	ctx, cancel := context.WithCancel(context.Background())
   396  	defer cancel()
   397  	ms := newMySQLSink4Test(ctx, c)
   398  	for i, tc := range testCases {
   399  		dmls := ms.prepareDMLs(tc.input, 0, 0)
   400  		c.Assert(dmls, check.DeepEquals, tc.expected, check.Commentf("%d", i))
   401  	}
   402  }
   403  
   404  func (s MySQLSinkSuite) TestPrepareUpdate(c *check.C) {
   405  	defer testleak.AfterTest(c)()
   406  	testCases := []struct {
   407  		quoteTable   string
   408  		preCols      []*model.Column
   409  		cols         []*model.Column
   410  		expectedSQL  string
   411  		expectedArgs []interface{}
   412  	}{
   413  		{
   414  			quoteTable:   "`test`.`t1`",
   415  			preCols:      []*model.Column{},
   416  			cols:         []*model.Column{},
   417  			expectedSQL:  "",
   418  			expectedArgs: nil,
   419  		},
   420  		{
   421  			quoteTable: "`test`.`t1`",
   422  			preCols: []*model.Column{
   423  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
   424  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
   425  			},
   426  			cols: []*model.Column{
   427  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
   428  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test2"},
   429  			},
   430  			expectedSQL:  "UPDATE `test`.`t1` SET `a`=?,`b`=? WHERE `a`=? LIMIT 1;",
   431  			expectedArgs: []interface{}{1, "test2", 1},
   432  		},
   433  		{
   434  			quoteTable: "`test`.`t1`",
   435  			preCols: []*model.Column{
   436  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1},
   437  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"},
   438  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   439  			},
   440  			cols: []*model.Column{
   441  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 2},
   442  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test2"},
   443  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   444  			},
   445  			expectedSQL:  "UPDATE `test`.`t1` SET `a`=?,`b`=? WHERE `a`=? AND `b`=? LIMIT 1;",
   446  			expectedArgs: []interface{}{2, "test2", 1, "test"},
   447  		},
   448  	}
   449  	for _, tc := range testCases {
   450  		query, args := prepareUpdate(tc.quoteTable, tc.preCols, tc.cols, false)
   451  		c.Assert(query, check.Equals, tc.expectedSQL)
   452  		c.Assert(args, check.DeepEquals, tc.expectedArgs)
   453  	}
   454  }
   455  
   456  func (s MySQLSinkSuite) TestPrepareDelete(c *check.C) {
   457  	defer testleak.AfterTest(c)()
   458  	testCases := []struct {
   459  		quoteTable   string
   460  		preCols      []*model.Column
   461  		expectedSQL  string
   462  		expectedArgs []interface{}
   463  	}{
   464  		{
   465  			quoteTable:   "`test`.`t1`",
   466  			preCols:      []*model.Column{},
   467  			expectedSQL:  "",
   468  			expectedArgs: nil,
   469  		},
   470  		{
   471  			quoteTable: "`test`.`t1`",
   472  			preCols: []*model.Column{
   473  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
   474  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
   475  			},
   476  			expectedSQL:  "DELETE FROM `test`.`t1` WHERE `a` = ? LIMIT 1;",
   477  			expectedArgs: []interface{}{1},
   478  		},
   479  		{
   480  			quoteTable: "`test`.`t1`",
   481  			preCols: []*model.Column{
   482  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1},
   483  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"},
   484  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   485  			},
   486  			expectedSQL:  "DELETE FROM `test`.`t1` WHERE `a` = ? AND `b` = ? LIMIT 1;",
   487  			expectedArgs: []interface{}{1, "test"},
   488  		},
   489  	}
   490  	for _, tc := range testCases {
   491  		query, args := prepareDelete(tc.quoteTable, tc.preCols, false)
   492  		c.Assert(query, check.Equals, tc.expectedSQL)
   493  		c.Assert(args, check.DeepEquals, tc.expectedArgs)
   494  	}
   495  }
   496  
   497  func (s MySQLSinkSuite) TestWhereSlice(c *check.C) {
   498  	defer testleak.AfterTest(c)()
   499  	testCases := []struct {
   500  		cols             []*model.Column
   501  		forceReplicate   bool
   502  		expectedColNames []string
   503  		expectedArgs     []interface{}
   504  	}{
   505  		{
   506  			cols:             []*model.Column{},
   507  			forceReplicate:   false,
   508  			expectedColNames: nil,
   509  			expectedArgs:     nil,
   510  		},
   511  		{
   512  			cols: []*model.Column{
   513  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
   514  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
   515  			},
   516  			forceReplicate:   false,
   517  			expectedColNames: []string{"a"},
   518  			expectedArgs:     []interface{}{1},
   519  		},
   520  		{
   521  			cols: []*model.Column{
   522  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1},
   523  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"},
   524  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   525  			},
   526  			forceReplicate:   false,
   527  			expectedColNames: []string{"a", "b"},
   528  			expectedArgs:     []interface{}{1, "test"},
   529  		},
   530  		{
   531  			cols:             []*model.Column{},
   532  			forceReplicate:   true,
   533  			expectedColNames: []string{},
   534  			expectedArgs:     []interface{}{},
   535  		},
   536  		{
   537  			cols: []*model.Column{
   538  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
   539  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
   540  			},
   541  			forceReplicate:   true,
   542  			expectedColNames: []string{"a"},
   543  			expectedArgs:     []interface{}{1},
   544  		},
   545  		{
   546  			cols: []*model.Column{
   547  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: 1},
   548  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag | model.HandleKeyFlag, Value: "test"},
   549  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   550  			},
   551  			forceReplicate:   true,
   552  			expectedColNames: []string{"a", "b"},
   553  			expectedArgs:     []interface{}{1, "test"},
   554  		},
   555  		{
   556  			cols: []*model.Column{
   557  				{Name: "a", Type: mysql.TypeLong, Flag: model.UniqueKeyFlag, Value: 1},
   558  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
   559  			},
   560  			forceReplicate:   true,
   561  			expectedColNames: []string{"a", "b"},
   562  			expectedArgs:     []interface{}{1, "test"},
   563  		},
   564  		{
   565  			cols: []*model.Column{
   566  				{Name: "a", Type: mysql.TypeLong, Flag: model.MultipleKeyFlag, Value: 1},
   567  				{Name: "b", Type: mysql.TypeVarString, Flag: model.MultipleKeyFlag, Value: "test"},
   568  				{Name: "c", Type: mysql.TypeLong, Flag: model.GeneratedColumnFlag, Value: 100},
   569  			},
   570  			forceReplicate:   true,
   571  			expectedColNames: []string{"a", "b", "c"},
   572  			expectedArgs:     []interface{}{1, "test", 100},
   573  		},
   574  	}
   575  	for _, tc := range testCases {
   576  		colNames, args := whereSlice(tc.cols, tc.forceReplicate)
   577  		c.Assert(colNames, check.DeepEquals, tc.expectedColNames)
   578  		c.Assert(args, check.DeepEquals, tc.expectedArgs)
   579  	}
   580  }
   581  
   582  func (s MySQLSinkSuite) TestMapReplace(c *check.C) {
   583  	defer testleak.AfterTest(c)()
   584  	testCases := []struct {
   585  		quoteTable    string
   586  		cols          []*model.Column
   587  		expectedQuery string
   588  		expectedArgs  []interface{}
   589  	}{
   590  		{
   591  			quoteTable: "`test`.`t1`",
   592  			cols: []*model.Column{
   593  				{Name: "a", Type: mysql.TypeLong, Value: 1},
   594  				{Name: "b", Type: mysql.TypeVarchar, Value: "varchar"},
   595  				{Name: "c", Type: mysql.TypeLong, Value: 1, Flag: model.GeneratedColumnFlag},
   596  				{Name: "d", Type: mysql.TypeTiny, Value: uint8(255)},
   597  			},
   598  			expectedQuery: "REPLACE INTO `test`.`t1`(`a`,`b`,`d`) VALUES ",
   599  			expectedArgs:  []interface{}{1, "varchar", uint8(255)},
   600  		},
   601  		{
   602  			quoteTable: "`test`.`t1`",
   603  			cols: []*model.Column{
   604  				{Name: "a", Type: mysql.TypeLong, Value: 1},
   605  				{Name: "b", Type: mysql.TypeVarchar, Value: "varchar"},
   606  				{Name: "c", Type: mysql.TypeLong, Value: 1},
   607  				{Name: "d", Type: mysql.TypeTiny, Value: uint8(255)},
   608  			},
   609  			expectedQuery: "REPLACE INTO `test`.`t1`(`a`,`b`,`c`,`d`) VALUES ",
   610  			expectedArgs:  []interface{}{1, "varchar", 1, uint8(255)},
   611  		},
   612  	}
   613  	for _, tc := range testCases {
   614  		// multiple times to verify the stability of column sequence in query string
   615  		for i := 0; i < 10; i++ {
   616  			query, args := prepareReplace(tc.quoteTable, tc.cols, false, false)
   617  			c.Assert(query, check.Equals, tc.expectedQuery)
   618  			c.Assert(args, check.DeepEquals, tc.expectedArgs)
   619  		}
   620  	}
   621  }
   622  
   623  type sqlArgs [][]interface{}
   624  
   625  func (a sqlArgs) Len() int           { return len(a) }
   626  func (a sqlArgs) Less(i, j int) bool { return fmt.Sprintf("%s", a[i]) < fmt.Sprintf("%s", a[j]) }
   627  func (a sqlArgs) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   628  
   629  func (s MySQLSinkSuite) TestReduceReplace(c *check.C) {
   630  	defer testleak.AfterTest(c)()
   631  	testCases := []struct {
   632  		replaces   map[string][][]interface{}
   633  		batchSize  int
   634  		sort       bool
   635  		expectSQLs []string
   636  		expectArgs [][]interface{}
   637  	}{
   638  		{
   639  			replaces: map[string][][]interface{}{
   640  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": {
   641  					[]interface{}{1, "1"},
   642  					[]interface{}{2, "2"},
   643  					[]interface{}{3, "3"},
   644  				},
   645  			},
   646  			batchSize: 1,
   647  			sort:      false,
   648  			expectSQLs: []string{
   649  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)",
   650  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)",
   651  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?)",
   652  			},
   653  			expectArgs: [][]interface{}{
   654  				{1, "1"},
   655  				{2, "2"},
   656  				{3, "3"},
   657  			},
   658  		},
   659  		{
   660  			replaces: map[string][][]interface{}{
   661  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": {
   662  					[]interface{}{1, "1"},
   663  					[]interface{}{2, "2"},
   664  					[]interface{}{3, "3"},
   665  					[]interface{}{4, "3"},
   666  					[]interface{}{5, "5"},
   667  				},
   668  			},
   669  			batchSize: 3,
   670  			sort:      false,
   671  			expectSQLs: []string{
   672  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)",
   673  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?)",
   674  			},
   675  			expectArgs: [][]interface{}{
   676  				{1, "1", 2, "2", 3, "3"},
   677  				{4, "3", 5, "5"},
   678  			},
   679  		},
   680  		{
   681  			replaces: map[string][][]interface{}{
   682  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": {
   683  					[]interface{}{1, "1"},
   684  					[]interface{}{2, "2"},
   685  					[]interface{}{3, "3"},
   686  					[]interface{}{4, "3"},
   687  					[]interface{}{5, "5"},
   688  				},
   689  			},
   690  			batchSize: 10,
   691  			sort:      false,
   692  			expectSQLs: []string{
   693  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?),(?,?),(?,?)",
   694  			},
   695  			expectArgs: [][]interface{}{
   696  				{1, "1", 2, "2", 3, "3", 4, "3", 5, "5"},
   697  			},
   698  		},
   699  		{
   700  			replaces: map[string][][]interface{}{
   701  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES ": {
   702  					[]interface{}{1, "1"},
   703  					[]interface{}{2, "2"},
   704  					[]interface{}{3, "3"},
   705  					[]interface{}{4, "3"},
   706  					[]interface{}{5, "5"},
   707  					[]interface{}{6, "6"},
   708  				},
   709  				"REPLACE INTO `test`.`t2`(`a`,`b`) VALUES ": {
   710  					[]interface{}{7, ""},
   711  					[]interface{}{8, ""},
   712  					[]interface{}{9, ""},
   713  				},
   714  			},
   715  			batchSize: 3,
   716  			sort:      true,
   717  			expectSQLs: []string{
   718  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)",
   719  				"REPLACE INTO `test`.`t1`(`a`,`b`) VALUES (?,?),(?,?),(?,?)",
   720  				"REPLACE INTO `test`.`t2`(`a`,`b`) VALUES (?,?),(?,?),(?,?)",
   721  			},
   722  			expectArgs: [][]interface{}{
   723  				{1, "1", 2, "2", 3, "3"},
   724  				{4, "3", 5, "5", 6, "6"},
   725  				{7, "", 8, "", 9, ""},
   726  			},
   727  		},
   728  	}
   729  	for _, tc := range testCases {
   730  		sqls, args := reduceReplace(tc.replaces, tc.batchSize)
   731  		if tc.sort {
   732  			sort.Strings(sqls)
   733  			sort.Sort(sqlArgs(args))
   734  		}
   735  		c.Assert(sqls, check.DeepEquals, tc.expectSQLs)
   736  		c.Assert(args, check.DeepEquals, tc.expectArgs)
   737  	}
   738  }
   739  
   740  func (s MySQLSinkSuite) TestSinkParamsClone(c *check.C) {
   741  	defer testleak.AfterTest(c)()
   742  	param1 := defaultParams.Clone()
   743  	param2 := param1.Clone()
   744  	param2.changefeedID = "123"
   745  	param2.batchReplaceEnabled = false
   746  	param2.maxTxnRow = 1
   747  	c.Assert(param1, check.DeepEquals, &sinkParams{
   748  		workerCount:         defaultWorkerCount,
   749  		maxTxnRow:           defaultMaxTxnRow,
   750  		tidbTxnMode:         defaultTiDBTxnMode,
   751  		batchReplaceEnabled: defaultBatchReplaceEnabled,
   752  		batchReplaceSize:    defaultBatchReplaceSize,
   753  		readTimeout:         defaultReadTimeout,
   754  		writeTimeout:        defaultWriteTimeout,
   755  		dialTimeout:         defaultDialTimeout,
   756  		safeMode:            defaultSafeMode,
   757  	})
   758  	c.Assert(param2, check.DeepEquals, &sinkParams{
   759  		changefeedID:        "123",
   760  		workerCount:         defaultWorkerCount,
   761  		maxTxnRow:           1,
   762  		tidbTxnMode:         defaultTiDBTxnMode,
   763  		batchReplaceEnabled: false,
   764  		batchReplaceSize:    defaultBatchReplaceSize,
   765  		readTimeout:         defaultReadTimeout,
   766  		writeTimeout:        defaultWriteTimeout,
   767  		dialTimeout:         defaultDialTimeout,
   768  		safeMode:            defaultSafeMode,
   769  	})
   770  }
   771  
   772  func (s MySQLSinkSuite) TestConfigureSinkURI(c *check.C) {
   773  	defer testleak.AfterTest(c)()
   774  
   775  	testDefaultParams := func() {
   776  		db, err := mockTestDB()
   777  		c.Assert(err, check.IsNil)
   778  		defer db.Close()
   779  
   780  		dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/")
   781  		c.Assert(err, check.IsNil)
   782  		params := defaultParams.Clone()
   783  		dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db)
   784  		c.Assert(err, check.IsNil)
   785  		expectedParams := []string{
   786  			"tidb_txn_mode=optimistic",
   787  			"readTimeout=2m",
   788  			"writeTimeout=2m",
   789  			"allow_auto_random_explicit_insert=1",
   790  		}
   791  		for _, param := range expectedParams {
   792  			c.Assert(strings.Contains(dsnStr, param), check.IsTrue)
   793  		}
   794  		c.Assert(strings.Contains(dsnStr, "time_zone"), check.IsFalse)
   795  	}
   796  
   797  	testTimezoneParam := func() {
   798  		db, err := mockTestDB()
   799  		c.Assert(err, check.IsNil)
   800  		defer db.Close()
   801  
   802  		dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/")
   803  		c.Assert(err, check.IsNil)
   804  		params := defaultParams.Clone()
   805  		params.timezone = `"UTC"`
   806  		dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db)
   807  		c.Assert(err, check.IsNil)
   808  		c.Assert(strings.Contains(dsnStr, "time_zone=%22UTC%22"), check.IsTrue)
   809  	}
   810  
   811  	testTimeoutParams := func() {
   812  		db, err := mockTestDB()
   813  		c.Assert(err, check.IsNil)
   814  		defer db.Close()
   815  
   816  		dsn, err := dmysql.ParseDSN("root:123456@tcp(127.0.0.1:4000)/")
   817  		c.Assert(err, check.IsNil)
   818  		uri, err := url.Parse("mysql://127.0.0.1:3306/?read-timeout=4m&write-timeout=5m&timeout=3m")
   819  		c.Assert(err, check.IsNil)
   820  		params, err := parseSinkURI(context.TODO(), uri, map[string]string{})
   821  		c.Assert(err, check.IsNil)
   822  		dsnStr, err := configureSinkURI(context.TODO(), dsn, params, db)
   823  		c.Assert(err, check.IsNil)
   824  		expectedParams := []string{
   825  			"readTimeout=4m",
   826  			"writeTimeout=5m",
   827  			"timeout=3m",
   828  		}
   829  		for _, param := range expectedParams {
   830  			c.Assert(strings.Contains(dsnStr, param), check.IsTrue)
   831  		}
   832  	}
   833  
   834  	testDefaultParams()
   835  	testTimezoneParam()
   836  	testTimeoutParams()
   837  }
   838  
   839  func (s MySQLSinkSuite) TestParseSinkURI(c *check.C) {
   840  	defer testleak.AfterTest(c)()
   841  	expected := defaultParams.Clone()
   842  	expected.workerCount = 64
   843  	expected.maxTxnRow = 20
   844  	expected.batchReplaceEnabled = true
   845  	expected.batchReplaceSize = 50
   846  	expected.safeMode = true
   847  	expected.timezone = `"UTC"`
   848  	expected.changefeedID = "cf-id"
   849  	expected.captureAddr = "127.0.0.1:8300"
   850  	expected.tidbTxnMode = "pessimistic"
   851  	uriStr := "mysql://127.0.0.1:3306/?worker-count=64&max-txn-row=20" +
   852  		"&batch-replace-enable=true&batch-replace-size=50&safe-mode=true" +
   853  		"&tidb-txn-mode=pessimistic"
   854  	opts := map[string]string{
   855  		OptChangefeedID: expected.changefeedID,
   856  		OptCaptureAddr:  expected.captureAddr,
   857  	}
   858  	uri, err := url.Parse(uriStr)
   859  	c.Assert(err, check.IsNil)
   860  	params, err := parseSinkURI(context.TODO(), uri, opts)
   861  	c.Assert(err, check.IsNil)
   862  	c.Assert(params, check.DeepEquals, expected)
   863  }
   864  
   865  func (s MySQLSinkSuite) TestParseSinkURITimezone(c *check.C) {
   866  	defer testleak.AfterTest(c)()
   867  	uris := []string{
   868  		"mysql://127.0.0.1:3306/?time-zone=Asia/Shanghai&worker-count=32",
   869  		"mysql://127.0.0.1:3306/?time-zone=&worker-count=32",
   870  		"mysql://127.0.0.1:3306/?worker-count=32",
   871  	}
   872  	expected := []string{
   873  		"\"Asia/Shanghai\"",
   874  		"",
   875  		"\"UTC\"",
   876  	}
   877  	ctx := context.TODO()
   878  	opts := map[string]string{}
   879  	for i, uriStr := range uris {
   880  		uri, err := url.Parse(uriStr)
   881  		c.Assert(err, check.IsNil)
   882  		params, err := parseSinkURI(ctx, uri, opts)
   883  		c.Assert(err, check.IsNil)
   884  		c.Assert(params.timezone, check.Equals, expected[i])
   885  	}
   886  }
   887  
   888  func (s MySQLSinkSuite) TestParseSinkURIBadQueryString(c *check.C) {
   889  	defer testleak.AfterTest(c)()
   890  	uris := []string{
   891  		"",
   892  		"postgre://127.0.0.1:3306",
   893  		"mysql://127.0.0.1:3306/?worker-count=not-number",
   894  		"mysql://127.0.0.1:3306/?max-txn-row=not-number",
   895  		"mysql://127.0.0.1:3306/?ssl-ca=only-ca-exists",
   896  		"mysql://127.0.0.1:3306/?batch-replace-enable=not-bool",
   897  		"mysql://127.0.0.1:3306/?batch-replace-enable=true&batch-replace-size=not-number",
   898  		"mysql://127.0.0.1:3306/?safe-mode=not-bool",
   899  	}
   900  	ctx := context.TODO()
   901  	opts := map[string]string{OptChangefeedID: "changefeed-01"}
   902  	var uri *url.URL
   903  	var err error
   904  	for _, uriStr := range uris {
   905  		if uriStr != "" {
   906  			uri, err = url.Parse(uriStr)
   907  			c.Assert(err, check.IsNil)
   908  		} else {
   909  			uri = nil
   910  		}
   911  		_, err = parseSinkURI(ctx, uri, opts)
   912  		c.Assert(err, check.NotNil)
   913  	}
   914  }
   915  
   916  func (s MySQLSinkSuite) TestCheckTiDBVariable(c *check.C) {
   917  	defer testleak.AfterTest(c)()
   918  	db, mock, err := sqlmock.New()
   919  	c.Assert(err, check.IsNil)
   920  	defer db.Close() //nolint:errcheck
   921  	columns := []string{"Variable_name", "Value"}
   922  
   923  	mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows(
   924  		sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"),
   925  	)
   926  	val, err := checkTiDBVariable(context.TODO(), db, "allow_auto_random_explicit_insert", "1")
   927  	c.Assert(err, check.IsNil)
   928  	c.Assert(val, check.Equals, "1")
   929  
   930  	mock.ExpectQuery("show session variables like 'no_exist_variable';").WillReturnError(sql.ErrNoRows)
   931  	val, err = checkTiDBVariable(context.TODO(), db, "no_exist_variable", "0")
   932  	c.Assert(err, check.IsNil)
   933  	c.Assert(val, check.Equals, "")
   934  
   935  	mock.ExpectQuery("show session variables like 'version';").WillReturnError(sql.ErrConnDone)
   936  	_, err = checkTiDBVariable(context.TODO(), db, "version", "5.7.25-TiDB-v4.0.0")
   937  	c.Assert(err, check.ErrorMatches, ".*"+sql.ErrConnDone.Error())
   938  }
   939  
   940  func mockTestDB() (*sql.DB, error) {
   941  	// mock for test db, which is used querying TiDB session variable
   942  	db, mock, err := sqlmock.New()
   943  	if err != nil {
   944  		return nil, err
   945  	}
   946  	columns := []string{"Variable_name", "Value"}
   947  	mock.ExpectQuery("show session variables like 'allow_auto_random_explicit_insert';").WillReturnRows(
   948  		sqlmock.NewRows(columns).AddRow("allow_auto_random_explicit_insert", "0"),
   949  	)
   950  	mock.ExpectQuery("show session variables like 'tidb_txn_mode';").WillReturnRows(
   951  		sqlmock.NewRows(columns).AddRow("tidb_txn_mode", "pessimistic"),
   952  	)
   953  	mock.ExpectClose()
   954  	return db, nil
   955  }
   956  
   957  func (s MySQLSinkSuite) TestAdjustSQLMode(c *check.C) {
   958  	defer testleak.AfterTest(c)()
   959  
   960  	ctx, cancel := context.WithCancel(context.Background())
   961  	defer cancel()
   962  
   963  	dbIndex := 0
   964  	mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
   965  		defer func() {
   966  			dbIndex++
   967  		}()
   968  		if dbIndex == 0 {
   969  			// test db
   970  			db, err := mockTestDB()
   971  			c.Assert(err, check.IsNil)
   972  			return db, nil
   973  		}
   974  		// normal db
   975  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
   976  		c.Assert(err, check.IsNil)
   977  		mock.ExpectQuery("SELECT @@SESSION.sql_mode;").
   978  			WillReturnRows(sqlmock.NewRows([]string{"@@SESSION.sql_mode"}).
   979  				AddRow("ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE"))
   980  		mock.ExpectExec("SET sql_mode = 'ONLY_FULL_GROUP_BY,NO_ZERO_IN_DATE,NO_ZERO_DATE';").
   981  			WillReturnResult(sqlmock.NewResult(0, 0))
   982  		mock.ExpectClose()
   983  		return db, nil
   984  	}
   985  	backupGetDBConn := getDBConnImpl
   986  	getDBConnImpl = mockGetDBConn
   987  	defer func() {
   988  		getDBConnImpl = backupGetDBConn
   989  	}()
   990  
   991  	changefeed := "test-changefeed"
   992  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4")
   993  	c.Assert(err, check.IsNil)
   994  	rc := config.GetDefaultReplicaConfig()
   995  	rc.Cyclic = &config.CyclicConfig{
   996  		Enable:          true,
   997  		ReplicaID:       1,
   998  		FilterReplicaID: []uint64{2},
   999  	}
  1000  	f, err := filter.NewFilter(rc)
  1001  	c.Assert(err, check.IsNil)
  1002  	cyclicConfig, err := rc.Cyclic.Marshal()
  1003  	c.Assert(err, check.IsNil)
  1004  	opts := map[string]string{
  1005  		mark.OptCyclicConfig: cyclicConfig,
  1006  	}
  1007  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, opts)
  1008  	c.Assert(err, check.IsNil)
  1009  
  1010  	err = sink.Close(ctx)
  1011  	c.Assert(err, check.IsNil)
  1012  }
  1013  
  1014  type mockUnavailableMySQL struct {
  1015  	listener net.Listener
  1016  	quit     chan interface{}
  1017  	wg       sync.WaitGroup
  1018  }
  1019  
  1020  func newMockUnavailableMySQL(addr string, c *check.C) *mockUnavailableMySQL {
  1021  	s := &mockUnavailableMySQL{
  1022  		quit: make(chan interface{}),
  1023  	}
  1024  	l, err := net.Listen("tcp", addr)
  1025  	c.Assert(err, check.IsNil)
  1026  	s.listener = l
  1027  	s.wg.Add(1)
  1028  	go s.serve(c)
  1029  	return s
  1030  }
  1031  
  1032  func (s *mockUnavailableMySQL) serve(c *check.C) {
  1033  	defer s.wg.Done()
  1034  
  1035  	for {
  1036  		_, err := s.listener.Accept()
  1037  		if err != nil {
  1038  			select {
  1039  			case <-s.quit:
  1040  				return
  1041  			default:
  1042  				c.Error(err)
  1043  			}
  1044  		} else {
  1045  			s.wg.Add(1)
  1046  			go func() {
  1047  				// don't read from TCP connection, to simulate database service unavailable
  1048  				<-s.quit
  1049  				s.wg.Done()
  1050  			}()
  1051  		}
  1052  	}
  1053  }
  1054  
  1055  func (s *mockUnavailableMySQL) Stop() {
  1056  	close(s.quit)
  1057  	s.listener.Close()
  1058  	s.wg.Wait()
  1059  }
  1060  
  1061  func (s MySQLSinkSuite) TestNewMySQLTimeout(c *check.C) {
  1062  	defer testleak.AfterTest(c)()
  1063  
  1064  	addr := "127.0.0.1:33333"
  1065  	mockMySQL := newMockUnavailableMySQL(addr, c)
  1066  	defer mockMySQL.Stop()
  1067  
  1068  	ctx, cancel := context.WithCancel(context.Background())
  1069  	defer cancel()
  1070  	changefeed := "test-changefeed"
  1071  	sinkURI, err := url.Parse(fmt.Sprintf("mysql://%s/?read-timeout=2s&timeout=2s", addr))
  1072  	c.Assert(err, check.IsNil)
  1073  	rc := config.GetDefaultReplicaConfig()
  1074  	f, err := filter.NewFilter(rc)
  1075  	c.Assert(err, check.IsNil)
  1076  	_, err = newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1077  	c.Assert(errors.Cause(err), check.Equals, driver.ErrBadConn)
  1078  }
  1079  
  1080  func (s MySQLSinkSuite) TestNewMySQLSinkExecDML(c *check.C) {
  1081  	defer testleak.AfterTest(c)()
  1082  
  1083  	dbIndex := 0
  1084  	mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1085  		defer func() {
  1086  			dbIndex++
  1087  		}()
  1088  		if dbIndex == 0 {
  1089  			// test db
  1090  			db, err := mockTestDB()
  1091  			c.Assert(err, check.IsNil)
  1092  			return db, nil
  1093  		}
  1094  		// normal db
  1095  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1096  		c.Assert(err, check.IsNil)
  1097  		mock.ExpectBegin()
  1098  		mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`,`b`) VALUES (?,?),(?,?)").
  1099  			WithArgs(1, "test", 2, "test").
  1100  			WillReturnResult(sqlmock.NewResult(2, 2))
  1101  		mock.ExpectCommit()
  1102  		mock.ExpectBegin()
  1103  		mock.ExpectExec("REPLACE INTO `s1`.`t2`(`a`,`b`) VALUES (?,?),(?,?)").
  1104  			WithArgs(1, "test", 2, "test").
  1105  			WillReturnResult(sqlmock.NewResult(2, 2))
  1106  		mock.ExpectCommit()
  1107  		mock.ExpectClose()
  1108  		return db, nil
  1109  	}
  1110  	backupGetDBConn := getDBConnImpl
  1111  	getDBConnImpl = mockGetDBConn
  1112  	defer func() {
  1113  		getDBConnImpl = backupGetDBConn
  1114  	}()
  1115  
  1116  	ctx, cancel := context.WithCancel(context.Background())
  1117  	defer cancel()
  1118  	changefeed := "test-changefeed"
  1119  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4")
  1120  	c.Assert(err, check.IsNil)
  1121  	rc := config.GetDefaultReplicaConfig()
  1122  	f, err := filter.NewFilter(rc)
  1123  	c.Assert(err, check.IsNil)
  1124  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1125  	c.Assert(err, check.IsNil)
  1126  
  1127  	rows := []*model.RowChangedEvent{
  1128  		{
  1129  			StartTs:  1,
  1130  			CommitTs: 2,
  1131  			Table:    &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1132  			Columns: []*model.Column{
  1133  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
  1134  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
  1135  			},
  1136  		},
  1137  		{
  1138  			StartTs:  1,
  1139  			CommitTs: 2,
  1140  			Table:    &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1141  			Columns: []*model.Column{
  1142  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2},
  1143  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
  1144  			},
  1145  		},
  1146  		{
  1147  			StartTs:  5,
  1148  			CommitTs: 6,
  1149  			Table:    &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1150  			Columns: []*model.Column{
  1151  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 3},
  1152  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
  1153  			},
  1154  		},
  1155  		{
  1156  			StartTs:  3,
  1157  			CommitTs: 4,
  1158  			Table:    &model.TableName{Schema: "s1", Table: "t2", TableID: 2},
  1159  			Columns: []*model.Column{
  1160  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
  1161  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
  1162  			},
  1163  		},
  1164  		{
  1165  			StartTs:  3,
  1166  			CommitTs: 4,
  1167  			Table:    &model.TableName{Schema: "s1", Table: "t2", TableID: 2},
  1168  			Columns: []*model.Column{
  1169  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2},
  1170  				{Name: "b", Type: mysql.TypeVarchar, Flag: 0, Value: "test"},
  1171  			},
  1172  		},
  1173  	}
  1174  
  1175  	err = sink.EmitRowChangedEvents(ctx, rows...)
  1176  	c.Assert(err, check.IsNil)
  1177  
  1178  	err = retry.Do(context.Background(), func() error {
  1179  		ts, err := sink.FlushRowChangedEvents(ctx, uint64(2))
  1180  		c.Assert(err, check.IsNil)
  1181  		if ts < uint64(2) {
  1182  			return errors.Errorf("checkpoint ts %d less than resolved ts %d", ts, 2)
  1183  		}
  1184  		return nil
  1185  	}, retry.WithBackoffBaseDelay(20), retry.WithMaxTries(10), retry.WithIsRetryableErr(cerror.IsRetryableError))
  1186  
  1187  	c.Assert(err, check.IsNil)
  1188  
  1189  	err = retry.Do(context.Background(), func() error {
  1190  		ts, err := sink.FlushRowChangedEvents(ctx, uint64(4))
  1191  		c.Assert(err, check.IsNil)
  1192  		if ts < uint64(4) {
  1193  			return errors.Errorf("checkpoint ts %d less than resolved ts %d", ts, 4)
  1194  		}
  1195  		return nil
  1196  	}, retry.WithBackoffBaseDelay(20), retry.WithMaxTries(10), retry.WithIsRetryableErr(cerror.IsRetryableError))
  1197  	c.Assert(err, check.IsNil)
  1198  
  1199  	err = sink.Barrier(ctx)
  1200  	c.Assert(err, check.IsNil)
  1201  
  1202  	err = sink.Close(ctx)
  1203  	c.Assert(err, check.IsNil)
  1204  }
  1205  
  1206  func (s MySQLSinkSuite) TestExecDMLRollbackErrDatabaseNotExists(c *check.C) {
  1207  	defer testleak.AfterTest(c)()
  1208  
  1209  	rows := []*model.RowChangedEvent{
  1210  		{
  1211  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1212  			Columns: []*model.Column{
  1213  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
  1214  			},
  1215  		},
  1216  		{
  1217  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1218  			Columns: []*model.Column{
  1219  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2},
  1220  			},
  1221  		},
  1222  	}
  1223  
  1224  	errDatabaseNotExists := &dmysql.MySQLError{
  1225  		Number: uint16(infoschema.ErrDatabaseNotExists.Code()),
  1226  	}
  1227  
  1228  	dbIndex := 0
  1229  	mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1230  		defer func() {
  1231  			dbIndex++
  1232  		}()
  1233  		if dbIndex == 0 {
  1234  			// test db
  1235  			db, err := mockTestDB()
  1236  			c.Assert(err, check.IsNil)
  1237  			return db, nil
  1238  		}
  1239  		// normal db
  1240  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1241  		c.Assert(err, check.IsNil)
  1242  		mock.ExpectBegin()
  1243  		mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)").
  1244  			WithArgs(1, 2).
  1245  			WillReturnError(errDatabaseNotExists)
  1246  		mock.ExpectRollback()
  1247  		mock.ExpectClose()
  1248  		return db, nil
  1249  	}
  1250  	backupGetDBConn := getDBConnImpl
  1251  	getDBConnImpl = mockGetDBConnErrDatabaseNotExists
  1252  	defer func() {
  1253  		getDBConnImpl = backupGetDBConn
  1254  	}()
  1255  
  1256  	ctx, cancel := context.WithCancel(context.Background())
  1257  	defer cancel()
  1258  	changefeed := "test-changefeed"
  1259  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1")
  1260  	c.Assert(err, check.IsNil)
  1261  	rc := config.GetDefaultReplicaConfig()
  1262  	f, err := filter.NewFilter(rc)
  1263  	c.Assert(err, check.IsNil)
  1264  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1265  	c.Assert(err, check.IsNil)
  1266  
  1267  	err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */)
  1268  	c.Assert(errors.Cause(err), check.Equals, errDatabaseNotExists)
  1269  
  1270  	err = sink.Close(ctx)
  1271  	c.Assert(err, check.IsNil)
  1272  }
  1273  
  1274  func (s MySQLSinkSuite) TestExecDMLRollbackErrTableNotExists(c *check.C) {
  1275  	defer testleak.AfterTest(c)()
  1276  
  1277  	rows := []*model.RowChangedEvent{
  1278  		{
  1279  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1280  			Columns: []*model.Column{
  1281  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
  1282  			},
  1283  		},
  1284  		{
  1285  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1286  			Columns: []*model.Column{
  1287  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2},
  1288  			},
  1289  		},
  1290  	}
  1291  
  1292  	errTableNotExists := &dmysql.MySQLError{
  1293  		Number: uint16(infoschema.ErrTableNotExists.Code()),
  1294  	}
  1295  
  1296  	dbIndex := 0
  1297  	mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1298  		defer func() {
  1299  			dbIndex++
  1300  		}()
  1301  		if dbIndex == 0 {
  1302  			// test db
  1303  			db, err := mockTestDB()
  1304  			c.Assert(err, check.IsNil)
  1305  			return db, nil
  1306  		}
  1307  		// normal db
  1308  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1309  		c.Assert(err, check.IsNil)
  1310  		mock.ExpectBegin()
  1311  		mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)").
  1312  			WithArgs(1, 2).
  1313  			WillReturnError(errTableNotExists)
  1314  		mock.ExpectRollback()
  1315  		mock.ExpectClose()
  1316  		return db, nil
  1317  	}
  1318  	backupGetDBConn := getDBConnImpl
  1319  	getDBConnImpl = mockGetDBConnErrDatabaseNotExists
  1320  	defer func() {
  1321  		getDBConnImpl = backupGetDBConn
  1322  	}()
  1323  
  1324  	ctx, cancel := context.WithCancel(context.Background())
  1325  	defer cancel()
  1326  	changefeed := "test-changefeed"
  1327  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1")
  1328  	c.Assert(err, check.IsNil)
  1329  	rc := config.GetDefaultReplicaConfig()
  1330  	f, err := filter.NewFilter(rc)
  1331  	c.Assert(err, check.IsNil)
  1332  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1333  	c.Assert(err, check.IsNil)
  1334  
  1335  	err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */)
  1336  	c.Assert(errors.Cause(err), check.Equals, errTableNotExists)
  1337  
  1338  	err = sink.Close(ctx)
  1339  	c.Assert(err, check.IsNil)
  1340  }
  1341  
  1342  func (s MySQLSinkSuite) TestExecDMLRollbackErrRetryable(c *check.C) {
  1343  	defer testleak.AfterTest(c)()
  1344  
  1345  	rows := []*model.RowChangedEvent{
  1346  		{
  1347  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1348  			Columns: []*model.Column{
  1349  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 1},
  1350  			},
  1351  		},
  1352  		{
  1353  			Table: &model.TableName{Schema: "s1", Table: "t1", TableID: 1},
  1354  			Columns: []*model.Column{
  1355  				{Name: "a", Type: mysql.TypeLong, Flag: model.HandleKeyFlag | model.PrimaryKeyFlag, Value: 2},
  1356  			},
  1357  		},
  1358  	}
  1359  
  1360  	errLockDeadlock := &dmysql.MySQLError{
  1361  		Number: mysql.ErrLockDeadlock,
  1362  	}
  1363  
  1364  	dbIndex := 0
  1365  	mockGetDBConnErrDatabaseNotExists := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1366  		defer func() {
  1367  			dbIndex++
  1368  		}()
  1369  		if dbIndex == 0 {
  1370  			// test db
  1371  			db, err := mockTestDB()
  1372  			c.Assert(err, check.IsNil)
  1373  			return db, nil
  1374  		}
  1375  		// normal db
  1376  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1377  		c.Assert(err, check.IsNil)
  1378  		for i := 0; i < defaultDMLMaxRetryTime; i++ {
  1379  			mock.ExpectBegin()
  1380  			mock.ExpectExec("REPLACE INTO `s1`.`t1`(`a`) VALUES (?),(?)").
  1381  				WithArgs(1, 2).
  1382  				WillReturnError(errLockDeadlock)
  1383  			mock.ExpectRollback()
  1384  		}
  1385  		mock.ExpectClose()
  1386  		return db, nil
  1387  	}
  1388  	backupGetDBConn := getDBConnImpl
  1389  	getDBConnImpl = mockGetDBConnErrDatabaseNotExists
  1390  	defer func() {
  1391  		getDBConnImpl = backupGetDBConn
  1392  	}()
  1393  
  1394  	ctx, cancel := context.WithCancel(context.Background())
  1395  	defer cancel()
  1396  	changefeed := "test-changefeed"
  1397  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=1")
  1398  	c.Assert(err, check.IsNil)
  1399  	rc := config.GetDefaultReplicaConfig()
  1400  	f, err := filter.NewFilter(rc)
  1401  	c.Assert(err, check.IsNil)
  1402  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1403  	c.Assert(err, check.IsNil)
  1404  
  1405  	err = sink.(*mysqlSink).execDMLs(ctx, rows, 1 /* replicaID */, 1 /* bucket */)
  1406  	c.Assert(errors.Cause(err), check.Equals, errLockDeadlock)
  1407  
  1408  	err = sink.Close(ctx)
  1409  	c.Assert(err, check.IsNil)
  1410  }
  1411  
  1412  func (s MySQLSinkSuite) TestNewMySQLSinkExecDDL(c *check.C) {
  1413  	defer testleak.AfterTest(c)()
  1414  
  1415  	dbIndex := 0
  1416  	mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1417  		defer func() {
  1418  			dbIndex++
  1419  		}()
  1420  		if dbIndex == 0 {
  1421  			// test db
  1422  			db, err := mockTestDB()
  1423  			c.Assert(err, check.IsNil)
  1424  			return db, nil
  1425  		}
  1426  		// normal db
  1427  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1428  		c.Assert(err, check.IsNil)
  1429  		mock.ExpectBegin()
  1430  		mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1))
  1431  		mock.ExpectExec("ALTER TABLE test.t1 ADD COLUMN a int").WillReturnResult(sqlmock.NewResult(1, 1))
  1432  		mock.ExpectCommit()
  1433  		mock.ExpectBegin()
  1434  		mock.ExpectExec("USE `test`;").WillReturnResult(sqlmock.NewResult(1, 1))
  1435  		mock.ExpectExec("ALTER TABLE test.t1 ADD COLUMN a int").
  1436  			WillReturnError(&dmysql.MySQLError{
  1437  				Number: uint16(infoschema.ErrColumnExists.Code()),
  1438  			})
  1439  		mock.ExpectRollback()
  1440  		mock.ExpectClose()
  1441  		return db, nil
  1442  	}
  1443  	backupGetDBConn := getDBConnImpl
  1444  	getDBConnImpl = mockGetDBConn
  1445  	defer func() {
  1446  		getDBConnImpl = backupGetDBConn
  1447  	}()
  1448  
  1449  	ctx, cancel := context.WithCancel(context.Background())
  1450  	defer cancel()
  1451  	changefeed := "test-changefeed"
  1452  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4")
  1453  	c.Assert(err, check.IsNil)
  1454  	rc := config.GetDefaultReplicaConfig()
  1455  	rc.Filter = &config.FilterConfig{
  1456  		Rules: []string{"test.t1"},
  1457  	}
  1458  	f, err := filter.NewFilter(rc)
  1459  	c.Assert(err, check.IsNil)
  1460  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1461  	c.Assert(err, check.IsNil)
  1462  
  1463  	ddl1 := &model.DDLEvent{
  1464  		StartTs:  1000,
  1465  		CommitTs: 1010,
  1466  		TableInfo: &model.SimpleTableInfo{
  1467  			Schema: "test",
  1468  			Table:  "t1",
  1469  		},
  1470  		Type:  timodel.ActionAddColumn,
  1471  		Query: "ALTER TABLE test.t1 ADD COLUMN a int",
  1472  	}
  1473  	ddl2 := &model.DDLEvent{
  1474  		StartTs:  1020,
  1475  		CommitTs: 1030,
  1476  		TableInfo: &model.SimpleTableInfo{
  1477  			Schema: "test",
  1478  			Table:  "t2",
  1479  		},
  1480  		Type:  timodel.ActionAddColumn,
  1481  		Query: "ALTER TABLE test.t1 ADD COLUMN a int",
  1482  	}
  1483  
  1484  	err = sink.EmitDDLEvent(ctx, ddl1)
  1485  	c.Assert(err, check.IsNil)
  1486  	err = sink.EmitDDLEvent(ctx, ddl2)
  1487  	c.Assert(cerror.ErrDDLEventIgnored.Equal(err), check.IsTrue)
  1488  	// DDL execute failed, but error can be ignored
  1489  	err = sink.EmitDDLEvent(ctx, ddl1)
  1490  	c.Assert(err, check.IsNil)
  1491  
  1492  	err = sink.Close(ctx)
  1493  	c.Assert(err, check.IsNil)
  1494  }
  1495  
  1496  func (s MySQLSinkSuite) TestNewMySQLSink(c *check.C) {
  1497  	defer testleak.AfterTest(c)()
  1498  
  1499  	dbIndex := 0
  1500  	mockGetDBConn := func(ctx context.Context, dsnStr string) (*sql.DB, error) {
  1501  		defer func() {
  1502  			dbIndex++
  1503  		}()
  1504  		if dbIndex == 0 {
  1505  			// test db
  1506  			db, err := mockTestDB()
  1507  			c.Assert(err, check.IsNil)
  1508  			return db, nil
  1509  		}
  1510  		// normal db
  1511  		db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
  1512  		mock.ExpectClose()
  1513  		c.Assert(err, check.IsNil)
  1514  		return db, nil
  1515  	}
  1516  	backupGetDBConn := getDBConnImpl
  1517  	getDBConnImpl = mockGetDBConn
  1518  	defer func() {
  1519  		getDBConnImpl = backupGetDBConn
  1520  	}()
  1521  
  1522  	ctx, cancel := context.WithCancel(context.Background())
  1523  	defer cancel()
  1524  	changefeed := "test-changefeed"
  1525  	sinkURI, err := url.Parse("mysql://127.0.0.1:4000/?time-zone=UTC&worker-count=4")
  1526  	c.Assert(err, check.IsNil)
  1527  	rc := config.GetDefaultReplicaConfig()
  1528  	f, err := filter.NewFilter(rc)
  1529  	c.Assert(err, check.IsNil)
  1530  	sink, err := newMySQLSink(ctx, changefeed, sinkURI, f, rc, map[string]string{})
  1531  	c.Assert(err, check.IsNil)
  1532  	err = sink.Close(ctx)
  1533  	c.Assert(err, check.IsNil)
  1534  }