github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/checker/table_structure_test.go (about)

     1  // Copyright 2021 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 checker
    15  
    16  import (
    17  	"context"
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/DATA-DOG/go-sqlmock"
    22  	"github.com/go-sql-driver/mysql"
    23  	"github.com/pingcap/tidb/pkg/util/filter"
    24  	"github.com/pingcap/tiflow/dm/pkg/conn"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  var errNoSuchTable = &mysql.MySQLError{Number: 1146, Message: "Table 'xxx' doesn't exist"}
    29  
    30  func TestShardingTablesChecker(t *testing.T) {
    31  	db, mock, err := sqlmock.New()
    32  	require.NoError(t, err)
    33  	ctx := context.Background()
    34  
    35  	// 1. test a success check
    36  	mock = initShardingMock(mock)
    37  	createTableRow2 := sqlmock.NewRows([]string{"Table", "Create Table"}).
    38  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
    39    "c" int(11) NOT NULL,
    40    PRIMARY KEY ("c")
    41  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
    42  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
    43  
    44  	checker := NewShardingTablesChecker("test-name",
    45  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
    46  		map[string][]filter.Table{"test-source": {
    47  			{Schema: "test-db", Name: "test-table-1"},
    48  			{Schema: "test-db", Name: "test-table-2"},
    49  		}},
    50  		false,
    51  		1)
    52  	result := checker.Check(ctx)
    53  	require.Equal(t, StateSuccess, result.State)
    54  	require.NoError(t, mock.ExpectationsWereMet())
    55  
    56  	// 2. check different column number
    57  	checker = NewShardingTablesChecker("test-name",
    58  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
    59  		map[string][]filter.Table{"test-source": {
    60  			{Schema: "test-db", Name: "test-table-1"},
    61  			{Schema: "test-db", Name: "test-table-2"},
    62  		}},
    63  		false,
    64  		1)
    65  	mock = initShardingMock(mock)
    66  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
    67  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
    68    "c" int(11) NOT NULL,
    69    "d" int(11) NOT NULL,
    70    PRIMARY KEY ("c")
    71  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
    72  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
    73  
    74  	result = checker.Check(ctx)
    75  	require.Equal(t, StateFailure, result.State)
    76  	require.Len(t, result.Errors, 1)
    77  	require.NoError(t, mock.ExpectationsWereMet())
    78  
    79  	// 3. check different column def
    80  	checker = NewShardingTablesChecker("test-name",
    81  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
    82  		map[string][]filter.Table{"test-source": {
    83  			{Schema: "test-db", Name: "test-table-1"},
    84  			{Schema: "test-db", Name: "test-table-2"},
    85  		}},
    86  		false,
    87  		1)
    88  	mock = initShardingMock(mock)
    89  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
    90  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
    91    "c" varchar(20) NOT NULL,
    92    PRIMARY KEY ("c")
    93  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
    94  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
    95  
    96  	result = checker.Check(ctx)
    97  	require.Equal(t, StateFailure, result.State)
    98  	require.Len(t, result.Errors, 1)
    99  	require.NoError(t, mock.ExpectationsWereMet())
   100  
   101  	// 4. test tiflow#5759
   102  	checker = NewShardingTablesChecker("test-name",
   103  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   104  		map[string][]filter.Table{"test-source": {
   105  			{Schema: "test-db", Name: "test-table-1"},
   106  			{Schema: "test-db", Name: "test-table-2"},
   107  			{Schema: "test-db", Name: "test-table-3"},
   108  			{Schema: "test-db", Name: "test-table-4"},
   109  		}},
   110  		false,
   111  		1)
   112  	mock = initShardingMock(mock)
   113  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
   114  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
   115    "c" varchar(20) NOT NULL,
   116    PRIMARY KEY ("c")
   117  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   118  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   119  	createTableRow3 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   120  		AddRow("test-table-3", `CREATE TABLE "test-table-3" (
   121    "c" varchar(20) NOT NULL,
   122    "c2" INT,
   123    PRIMARY KEY ("c")
   124  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   125  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-3`").WillReturnRows(createTableRow3)
   126  	createTableRow4 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   127  		AddRow("test-table-4", `CREATE TABLE "test-table-4" (
   128    "c" varchar(20) NOT NULL,
   129    "c2" INT,
   130    "c3" INT,
   131    PRIMARY KEY ("c")
   132  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   133  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-4`").WillReturnRows(createTableRow4)
   134  
   135  	// in tiflow#5759, this function will enter deadlock
   136  	result = checker.Check(ctx)
   137  	require.Equal(t, StateFailure, result.State)
   138  	require.Len(t, result.Errors, 3)
   139  }
   140  
   141  func TestTablesChecker(t *testing.T) {
   142  	db, mock, err := sqlmock.New()
   143  	require.NoError(t, err)
   144  	downDB, downMock, err := sqlmock.New()
   145  	require.NoError(t, err)
   146  	ctx := context.Background()
   147  
   148  	commonMock := func() {
   149  		maxConnectionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   150  			AddRow("max_connections", "2")
   151  		mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnectionsRow)
   152  		sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   153  			AddRow("sql_mode", "ANSI_QUOTES")
   154  		mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   155  		sqlModeRow2 := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   156  			AddRow("sql_mode", "ANSI_QUOTES")
   157  		downMock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow2)
   158  	}
   159  
   160  	// 1. test a success check
   161  
   162  	commonMock()
   163  	createTableRowUp := sqlmock.NewRows([]string{"Table", "Create Table"}).
   164  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   165  		  "c" int(11) NOT NULL,
   166  		  PRIMARY KEY ("c")
   167  		) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   168  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   169  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnError(errNoSuchTable)
   170  
   171  	checker := NewTablesChecker(
   172  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   173  		conn.NewBaseDBForTest(downDB),
   174  		map[string]map[filter.Table][]filter.Table{
   175  			"test-source": {
   176  				{Schema: "test-db", Name: "test-table-1"}: {
   177  					{Schema: "test-db", Name: "test-table-1"},
   178  				},
   179  			},
   180  		},
   181  		nil,
   182  		1)
   183  	result := checker.Check(ctx)
   184  	require.Equal(t, StateSuccess, result.State)
   185  	require.NoError(t, mock.ExpectationsWereMet())
   186  	require.NoError(t, downMock.ExpectationsWereMet())
   187  
   188  	// 2. check many errors
   189  
   190  	commonMock()
   191  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   192  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   193    "c" int(11) NOT NULL,
   194    CONSTRAINT "fk" FOREIGN KEY ("c") REFERENCES "t" ("c")
   195  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   196  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   197  	createTableRow2 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   198  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   199    "c" int(11) NOT NULL,
   200    CONSTRAINT "fk" FOREIGN KEY ("c") REFERENCES "t" ("c")
   201  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`)
   202  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow2)
   203  
   204  	checker = NewTablesChecker(
   205  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   206  		conn.NewBaseDBForTest(downDB),
   207  		map[string]map[filter.Table][]filter.Table{
   208  			"test-source": {
   209  				{Schema: "test-db", Name: "test-table-1"}: {
   210  					{Schema: "test-db", Name: "test-table-1"},
   211  				},
   212  			},
   213  		},
   214  		nil,
   215  		1)
   216  	result = checker.Check(ctx)
   217  	require.Equal(t, StateWarning, result.State)
   218  	require.Len(t, result.Errors, 2)
   219  	require.NoError(t, mock.ExpectationsWereMet())
   220  	require.NoError(t, downMock.ExpectationsWereMet())
   221  	require.False(t, strings.HasSuffix(result.Instruction, "; "))
   222  
   223  	// 3. test #5759
   224  
   225  	commonMock()
   226  	createTableRow1 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   227  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   228    "c" int(11) NOT NULL
   229  ) ENGINE=InnoDB`)
   230  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
   231  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
   232    "c" int(11) NOT NULL
   233  ) ENGINE=InnoDB`)
   234  	createTableRow3 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   235  		AddRow("test-table-3", `CREATE TABLE "test-table-3" (
   236    "c" int(11) NOT NULL
   237  ) ENGINE=InnoDB`)
   238  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow1)
   239  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table`").WillReturnError(errNoSuchTable)
   240  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   241  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-3`").WillReturnRows(createTableRow3)
   242  
   243  	checker = NewTablesChecker(
   244  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   245  		conn.NewBaseDBForTest(downDB),
   246  		map[string]map[filter.Table][]filter.Table{
   247  			"test-source": {
   248  				{Schema: "test-db", Name: "test-table"}: {
   249  					{Schema: "test-db", Name: "test-table-1"},
   250  					{Schema: "test-db", Name: "test-table-2"},
   251  					{Schema: "test-db", Name: "test-table-3"},
   252  				},
   253  			},
   254  		},
   255  		nil,
   256  		1)
   257  	result = checker.Check(ctx)
   258  	require.Equal(t, StateWarning, result.State)
   259  	require.Len(t, result.Errors, 3)
   260  	require.NoError(t, mock.ExpectationsWereMet())
   261  	require.NoError(t, downMock.ExpectationsWereMet())
   262  
   263  	// 4. check warning from mismatching of upstream/downstream
   264  	commonMock()
   265  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   266  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   267  		  "c" int(11) NOT NULL,
   268  		  "d" int(11) NOT NULL,
   269  		  "e" int(11) NOT NULL,
   270  		  PRIMARY KEY ("c"),
   271  		  UNIQUE KEY "idx_d" ("d")
   272  		) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin`)
   273  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   274  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
   275  		AddRow("test-table", `CREATE TABLE "test-table" (
   276    		  "c" int(11) NOT NULL,
   277    		  "d" int(11) NOT NULL,
   278    		  "f" int(11) DEFAULT NULL,
   279    		  "g" int(11) NOT NULL,
   280  		  PRIMARY KEY ("c")
   281  		) ENGINE=InnoDB DEFAULT CHARSET=gbk COLLATE=gbk_chinese_ci`)
   282  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table`").WillReturnRows(createTableRow2)
   283  
   284  	checker = NewTablesChecker(
   285  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   286  		conn.NewBaseDBForTest(downDB),
   287  		map[string]map[filter.Table][]filter.Table{
   288  			"test-source": {
   289  				{Schema: "test-db", Name: "test-table"}: {
   290  					{Schema: "test-db", Name: "test-table-1"},
   291  				},
   292  			},
   293  		},
   294  		nil,
   295  		1)
   296  	result = checker.Check(ctx)
   297  	require.Equal(t, StateWarning, result.State)
   298  	require.Len(t, result.Errors, 5)
   299  	require.Equal(t,
   300  		"table `test-db`.`test-table-1` charset is not same, upstream: (test-table-1 latin1), downstream: (test-table gbk)",
   301  		result.Errors[0].ShortErr)
   302  	require.Equal(t,
   303  		"table `test-db`.`test-table-1` collation is not same, upstream: (test-table-1 latin1_bin), downstream: (test-table gbk_chinese_ci)",
   304  		result.Errors[1].ShortErr)
   305  	require.Equal(t,
   306  		"table `test-db`.`test-table-1` upstream has more PK or NOT NULL UK than downstream, index name: idx_d, columns: [d]",
   307  		result.Errors[2].ShortErr)
   308  	require.Equal(t,
   309  		"table `test-db`.`test-table-1` upstream has more columns than downstream, columns: [e]",
   310  		result.Errors[3].ShortErr)
   311  	require.Equal(t,
   312  		"table `test-db`.`test-table-1` downstream has more columns than upstream that require values to insert records, table name: test-table, columns: [g]",
   313  		result.Errors[4].ShortErr)
   314  	require.NoError(t, mock.ExpectationsWereMet())
   315  	require.NoError(t, downMock.ExpectationsWereMet())
   316  	require.False(t, strings.HasSuffix(result.Instruction, "; "))
   317  
   318  	// 5. check extended columns
   319  	commonMock()
   320  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   321  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   322  		  "c" int(11) NOT NULL,
   323  		  "ext1" int(11) NOT NULL,
   324  		  PRIMARY KEY ("c")
   325  		) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin`)
   326  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   327  	createTableRowDown := sqlmock.NewRows([]string{"Table", "Create Table"}).
   328  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   329  		  "c" int(11) NOT NULL,
   330  		  "ext3" int(11) NOT NULL,
   331  		  PRIMARY KEY ("c")
   332  		) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin`)
   333  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowDown)
   334  
   335  	checker = NewTablesChecker(
   336  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   337  		conn.NewBaseDBForTest(downDB),
   338  		map[string]map[filter.Table][]filter.Table{
   339  			"test-source": {
   340  				{Schema: "test-db", Name: "test-table-1"}: {
   341  					{Schema: "test-db", Name: "test-table-1"},
   342  				},
   343  			},
   344  		},
   345  		map[filter.Table][]string{
   346  			{Schema: "test-db", Name: "test-table-1"}: {"ext1", "ext2", "ext3"},
   347  		},
   348  		1)
   349  	result = checker.Check(ctx)
   350  	require.Equal(t, StateFailure, result.State)
   351  	require.Len(t, result.Errors, 2)
   352  	require.False(t, strings.HasSuffix(result.Instruction, "; "))
   353  	require.Equal(t,
   354  		"table `test-db`.`test-table-1` upstream table must not contain extended column [ext1]",
   355  		result.Errors[0].ShortErr)
   356  	require.Equal(t,
   357  		"table `test-db`.`test-table-1` downstream table must contain extended columns [ext1 ext2]",
   358  		result.Errors[1].ShortErr)
   359  	require.Contains(t, result.Instruction, "DM automatically fills the values of extended columns. You need to remove these columns or change configuration.")
   360  	require.NoError(t, mock.ExpectationsWereMet())
   361  	require.NoError(t, downMock.ExpectationsWereMet())
   362  }
   363  
   364  func TestCombineInstruction(t *testing.T) {
   365  	db, mock, err := sqlmock.New()
   366  	require.NoError(t, err)
   367  	downDB, downMock, err := sqlmock.New()
   368  	require.NoError(t, err)
   369  	ctx := context.Background()
   370  	commonMock := func() {
   371  		maxConnectionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   372  			AddRow("max_connections", "2")
   373  		mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnectionsRow)
   374  		sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   375  			AddRow("sql_mode", "ANSI_QUOTES")
   376  		mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   377  		sqlModeRow2 := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   378  			AddRow("sql_mode", "ANSI_QUOTES")
   379  		downMock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow2)
   380  	}
   381  
   382  	// 1. table with foreign key & no primary key
   383  	commonMock()
   384  	createTableRowUp := sqlmock.NewRows([]string{"Table", "Create Table"}).
   385  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   386  		  "c" int(11) NOT NULL,
   387  			"b" int(11) NOT NULL,
   388  			FOREIGN KEY ("b") REFERENCES "test-table-2" ("c")
   389  		) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   390  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   391  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnError(errNoSuchTable)
   392  	checker := NewTablesChecker(
   393  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   394  		conn.NewBaseDBForTest(downDB),
   395  		map[string]map[filter.Table][]filter.Table{
   396  			"test-source": {
   397  				{Schema: "test-db", Name: "test-table-1"}: {
   398  					{Schema: "test-db", Name: "test-table-1"},
   399  				},
   400  			},
   401  		},
   402  		nil,
   403  		1)
   404  	result := checker.Check(ctx)
   405  	require.Equal(t, StateWarning, result.State)
   406  	require.Contains(t, result.Instruction, "TiDB does not support foreign key constraints. See the document: https://docs.pingcap.com/tidb/stable/mysql-compatibility#unsupported-features")
   407  	require.Contains(t, result.Instruction, "You need to set primary/unique keys for the table. Otherwise replication efficiency might become very low and exactly-once replication cannot be guaranteed.")
   408  	require.Contains(t, result.Errors[0].ShortErr, "is parsed but ignored by TiDB.")
   409  	require.Contains(t, result.Errors[1].ShortErr, "primary/unique key does not exist")
   410  	require.NoError(t, mock.ExpectationsWereMet())
   411  	require.NoError(t, downMock.ExpectationsWereMet())
   412  
   413  	// 2. mismatched index columns
   414  	commonMock()
   415  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   416  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   417  	    "c" int(11) NOT NULL,
   418  			"b" int(11) NOT NULL,
   419  			"d" int(11) NOT NULL,
   420  			PRIMARY KEY("c", "b", "d")
   421  		) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   422  	createTableDown := sqlmock.NewRows([]string{"Table", "Create Table"}).
   423  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   424  		"c" int(11) NOT NULL,
   425  		"b" int(11) NOT NULL,
   426  		"d" int(11) NOT NULL,
   427  		PRIMARY KEY("c")
   428  	) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   429  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   430  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableDown)
   431  	checker = NewTablesChecker(
   432  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   433  		conn.NewBaseDBForTest(downDB),
   434  		map[string]map[filter.Table][]filter.Table{
   435  			"test-source": {
   436  				{Schema: "test-db", Name: "test-table-1"}: {
   437  					{Schema: "test-db", Name: "test-table-1"},
   438  				},
   439  			},
   440  		},
   441  		nil,
   442  		1)
   443  	result = checker.Check(ctx)
   444  	require.Equal(t, StateWarning, result.State)
   445  	require.Equal(t, result.State, StateWarning)
   446  	require.Contains(t, result.Errors[0].ShortErr, "upstream has more PK or NOT NULL UK than downstream")
   447  	require.Contains(t, result.Instruction, "Ensure that you use the same index columns for both upstream and downstream databases. Otherwise the migration job might fail or data inconsistency might occur.")
   448  	require.NoError(t, mock.ExpectationsWereMet())
   449  	require.NoError(t, downMock.ExpectationsWereMet())
   450  
   451  	// 3. charset not same or collation not same
   452  	commonMock()
   453  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   454  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   455  	    "c" int(11) NOT NULL,
   456  			"b" int(11) NOT NULL,
   457  			"d" int(11) NOT NULL,
   458  			PRIMARY KEY("c")
   459  		) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_bin`)
   460  	createTableDown = sqlmock.NewRows([]string{"Table", "Create Table"}).
   461  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   462  		"c" int(11) NOT NULL,
   463  		"b" int(11) NOT NULL,
   464  		"d" int(11) NOT NULL,
   465  		PRIMARY KEY("c")
   466  	) ENGINE=InnoDB DEFAULT CHARSET=binary COLLATE=binary`)
   467  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   468  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableDown)
   469  	checker = NewTablesChecker(
   470  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   471  		conn.NewBaseDBForTest(downDB),
   472  		map[string]map[filter.Table][]filter.Table{
   473  			"test-source": {
   474  				{Schema: "test-db", Name: "test-table-1"}: {
   475  					{Schema: "test-db", Name: "test-table-1"},
   476  				},
   477  			},
   478  		},
   479  		nil,
   480  		1)
   481  	result = checker.Check(ctx)
   482  	require.Equal(t, result.State, StateWarning)
   483  	require.Contains(t, result.Errors[0].ShortErr, "charset is not same")
   484  	require.Contains(t, result.Errors[1].ShortErr, "collation is not same")
   485  	require.Contains(t, result.Instruction, "Ensure that you use the same charsets for both upstream and downstream databases. Different charsets might cause data inconsistency.")
   486  	require.Contains(t, result.Instruction, "Ensure that you use the same collations for both upstream and downstream databases. Otherwise the query results from the two databases might be inconsistent.")
   487  	require.NoError(t, mock.ExpectationsWereMet())
   488  	require.NoError(t, downMock.ExpectationsWereMet())
   489  
   490  	// 4. different column number
   491  	commonMock()
   492  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   493  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   494  	    "c" int(11) NOT NULL,
   495  			"b" int(11) NOT NULL,
   496  			"d" int(11) NOT NULL,
   497  			PRIMARY KEY("c")
   498  		) ENGINE=InnoDB`)
   499  	createTableDown = sqlmock.NewRows([]string{"Table", "Create Table"}).
   500  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   501  		"c" int(11) NOT NULL,
   502  		"b" int(11) NOT NULL,
   503  		PRIMARY KEY("c")
   504  	) ENGINE=InnoDB`)
   505  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   506  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableDown)
   507  	checker = NewTablesChecker(
   508  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   509  		conn.NewBaseDBForTest(downDB),
   510  		map[string]map[filter.Table][]filter.Table{
   511  			"test-source": {
   512  				{Schema: "test-db", Name: "test-table-1"}: {
   513  					{Schema: "test-db", Name: "test-table-1"},
   514  				},
   515  			},
   516  		},
   517  		nil,
   518  		1)
   519  	result = checker.Check(ctx)
   520  	require.Equal(t, StateWarning, result.State)
   521  	require.Contains(t, result.Errors[0].ShortErr, "upstream has more columns than downstream")
   522  	require.Contains(t, result.Instruction, "Ensure that the column numbers are the same between upstream and downstream databases. Otherwise the migration job may fail.")
   523  	require.NoError(t, mock.ExpectationsWereMet())
   524  	require.NoError(t, downMock.ExpectationsWereMet())
   525  
   526  	// 5. upstream has extended column & downstream doesn't have extended column
   527  	commonMock()
   528  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   529  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   530        "c" int(11) NOT NULL,
   531  			"b" int(11) NOT NULL,
   532  			"d" int(11) NOT NULL,
   533  			PRIMARY KEY("c")
   534  		) ENGINE=InnoDB`)
   535  	createTableDown = sqlmock.NewRows([]string{"Table", "Create Table"}).
   536  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   537  		"c" int(11) NOT NULL,
   538  		"b" int(11) NOT NULL,
   539  		"e" int(11) NOT NULL,
   540  		PRIMARY KEY("c")
   541  	) ENGINE=InnoDB`)
   542  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   543  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableDown)
   544  	checker = NewTablesChecker(
   545  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   546  		conn.NewBaseDBForTest(downDB),
   547  		map[string]map[filter.Table][]filter.Table{
   548  			"test-source": {
   549  				{Schema: "test-db", Name: "test-table-1"}: {
   550  					{Schema: "test-db", Name: "test-table-1"},
   551  				},
   552  			},
   553  		},
   554  		map[filter.Table][]string{
   555  			{Schema: "test-db", Name: "test-table-1"}: {"d"},
   556  		},
   557  		1) // extended column "d"
   558  	result = checker.Check(ctx)
   559  	require.Equal(t, StateFailure, result.State)
   560  	require.Contains(t, result.Errors[0].ShortErr, "upstream table must not contain extended column")
   561  	require.Contains(t, result.Errors[1].ShortErr, "downstream table must contain extended columns")
   562  	require.Contains(t, result.Instruction, "DM automatically fills the values of extended columns. You need to remove these columns or change configuration.")
   563  	require.Contains(t, result.Instruction, "You need to manually add extended columns to the downstream table.")
   564  	require.NoError(t, mock.ExpectationsWereMet())
   565  	require.NoError(t, downMock.ExpectationsWereMet())
   566  
   567  	// 6. no downstream table with extended column
   568  	commonMock()
   569  	createTableRowUp = sqlmock.NewRows([]string{"Table", "Create Table"}).
   570  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   571        "c" int(11) NOT NULL,
   572  			"b" int(11) NOT NULL,
   573  			PRIMARY KEY("c")
   574  		) ENGINE=InnoDB`)
   575  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRowUp)
   576  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnError(errNoSuchTable)
   577  	checker = NewTablesChecker(
   578  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   579  		conn.NewBaseDBForTest(downDB),
   580  		map[string]map[filter.Table][]filter.Table{
   581  			"test-source": {
   582  				{Schema: "test-db", Name: "test-table-1"}: {
   583  					{Schema: "test-db", Name: "test-table-1"},
   584  				},
   585  			},
   586  		},
   587  		map[filter.Table][]string{
   588  			{Schema: "test-db", Name: "test-table-1"}: {"d"},
   589  		},
   590  		1) // extended column "d"
   591  	result = checker.Check(ctx)
   592  	require.Equal(t, StateFailure, result.State)
   593  	require.Contains(t, result.Errors[0].ShortErr, "does not exist in downstream table")
   594  	require.Contains(t, result.Instruction, "You need to create a table with extended columns before replication.")
   595  	require.NoError(t, mock.ExpectationsWereMet())
   596  	require.NoError(t, downMock.ExpectationsWereMet())
   597  }
   598  
   599  func TestOptimisticShardingTablesChecker(t *testing.T) {
   600  	db, mock, err := sqlmock.New()
   601  	require.NoError(t, err)
   602  	ctx := context.Background()
   603  
   604  	cases := []struct {
   605  		createTable1SQL string
   606  		createTable2SQL string
   607  		expectState     State
   608  		errLen          int
   609  	}{
   610  		// optimistic check different column number
   611  		{
   612  			createTable1SQL: `CREATE TABLE "test-table-1" (
   613  				"c" int(11) NOT NULL,
   614  				PRIMARY KEY ("c")
   615  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   616  			createTable2SQL: `CREATE TABLE "test-table-2" (
   617  				"c" int(11) NOT NULL,
   618  				"d" int(11) NOT NULL,
   619  				PRIMARY KEY ("c")
   620  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   621  			expectState: StateSuccess,
   622  		},
   623  		// optimistic check auto_increment conflict
   624  		{
   625  			createTable1SQL: `CREATE TABLE "test-table-1" (
   626  				"c" int(11) NOT NULL AUTO_INCREMENT,
   627  				PRIMARY KEY ("c")
   628  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   629  			createTable2SQL: `CREATE TABLE "test-table-2" (
   630  				"c" int(11) NOT NULL AUTO_INCREMENT,
   631  				"d" int(11) NOT NULL,
   632  				PRIMARY KEY ("c")
   633  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   634  			expectState: StateWarning,
   635  			errLen:      2, // 2 auto_increment warning
   636  		},
   637  		{
   638  			createTable1SQL: `CREATE TABLE "test-table-1" (
   639  				"c" int(11) NOT NULL AUTO_INCREMENT,
   640  				PRIMARY KEY ("c")
   641  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   642  			createTable2SQL: `CREATE TABLE "test-table-2" (
   643  				"c" int(11) NOT NULL,
   644  				"d" int(11) NOT NULL,
   645  				PRIMARY KEY ("c")
   646  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   647  			expectState: StateWarning,
   648  			errLen:      1, // 1 auto_increment warning
   649  		},
   650  		// must set auto_increment with key(failure)
   651  		{
   652  			createTable1SQL: `CREATE TABLE "test-table-1" (
   653  				"c" int(11) NOT NULL AUTO_INCREMENT
   654  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   655  			createTable2SQL: `CREATE TABLE "test-table-2" (
   656  				"c" int(11) NOT NULL,
   657  				"d" int(11) NOT NULL
   658  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   659  			expectState: StateFailure,
   660  			errLen:      2, // 1 auto_increment warning
   661  		},
   662  		{
   663  			createTable1SQL: `CREATE TABLE "test-table-1" (
   664  				"c" int(11) NOT NULL AUTO_INCREMENT,
   665  				PRIMARY KEY ("c")
   666  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   667  			createTable2SQL: `CREATE TABLE "test-table-2" (
   668  				"c" int(11) NOT NULL,
   669  				"d" int(11) NOT NULL
   670  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   671  			expectState: StateFailure,
   672  			errLen:      2, // 1 auto_increment warning
   673  		},
   674  		// different auto_increment
   675  		{
   676  			createTable1SQL: `CREATE TABLE "test-table-1" (
   677  				"c" int(11) NOT NULL AUTO_INCREMENT,
   678  				PRIMARY KEY ("c")
   679  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   680  			createTable2SQL: `CREATE TABLE "test-table-2" (
   681  				"c" int(11) NOT NULL,
   682  				"d" int(11) NOT NULL AUTO_INCREMENT,
   683  				PRIMARY KEY ("d")
   684  				) ENGINE=InnoDB DEFAULT CHARSET=latin1`,
   685  			expectState: StateFailure,
   686  			errLen:      3, // 2 auto_increment warning
   687  		},
   688  	}
   689  
   690  	for _, cs := range cases {
   691  		maxConnecionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("max_connections", "2")
   692  		mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnecionsRow)
   693  		sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("sql_mode", "ANSI_QUOTES")
   694  		mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   695  		createTableRow1 := sqlmock.NewRows([]string{"Table", "Create Table"}).AddRow("test-table-1", cs.createTable1SQL)
   696  		mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow1)
   697  		createTableRow2 := sqlmock.NewRows([]string{"Table", "Create Table"}).AddRow("test-table-2", cs.createTable2SQL)
   698  		mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   699  		checker := NewOptimisticShardingTablesChecker(
   700  			"test-name",
   701  			map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   702  			map[string][]filter.Table{"test-source": {
   703  				{Schema: "test-db", Name: "test-table-1"},
   704  				{Schema: "test-db", Name: "test-table-2"},
   705  			}},
   706  			0)
   707  		result := checker.Check(ctx)
   708  		require.Equal(t, cs.expectState, result.State)
   709  		require.Len(t, result.Errors, cs.errLen)
   710  		require.NoError(t, mock.ExpectationsWereMet())
   711  	}
   712  }
   713  
   714  func TestUnknownCharsetCollation(t *testing.T) {
   715  	db, mock, err := sqlmock.New()
   716  	require.NoError(t, err)
   717  	downDB, downMock, err := sqlmock.New()
   718  	require.NoError(t, err)
   719  	ctx := context.Background()
   720  
   721  	// 1. test TablesChecker
   722  
   723  	maxConnectionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   724  		AddRow("max_connections", "2")
   725  	mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnectionsRow)
   726  	sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   727  		AddRow("sql_mode", "ANSI_QUOTES")
   728  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   729  	createTableRow := sqlmock.NewRows([]string{"Table", "Create Table"}).
   730  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   731  		  "c" int(11) NOT NULL,
   732  		  PRIMARY KEY ("c")
   733  		) ENGINE=InnoDB DEFAULT CHARSET=utf32`)
   734  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   735  	sqlModeRow2 := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   736  		AddRow("sql_mode", "ANSI_QUOTES")
   737  	downMock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow2)
   738  
   739  	checker := NewTablesChecker(
   740  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   741  		conn.NewBaseDBForTest(downDB),
   742  		map[string]map[filter.Table][]filter.Table{
   743  			"test-source": {
   744  				{Schema: "test-db", Name: "test-table-1"}: {
   745  					{Schema: "test-db", Name: "test-table-1"},
   746  				},
   747  			},
   748  		},
   749  		nil,
   750  		1)
   751  	result := checker.Check(ctx)
   752  	require.Equal(t, StateWarning, result.State)
   753  	require.Len(t, result.Errors, 1)
   754  	require.Contains(t, result.Errors[0].ShortErr, "Unknown character set: 'utf32'")
   755  	require.NoError(t, mock.ExpectationsWereMet())
   756  	require.NoError(t, downMock.ExpectationsWereMet())
   757  
   758  	// 2. test ShardingTablesChecker
   759  	// 2.1 the first table has unknown charset
   760  
   761  	sqlModeRow = sqlmock.NewRows([]string{"Variable_name", "Value"}).
   762  		AddRow("sql_mode", "ANSI_QUOTES")
   763  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   764  	createTableRow = sqlmock.NewRows([]string{"Table", "Create Table"}).
   765  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   766  "c" int(11) NOT NULL,
   767  PRIMARY KEY ("c")
   768  ) ENGINE=InnoDB DEFAULT CHARSET=utf16`)
   769  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   770  
   771  	checker = NewShardingTablesChecker("test-name",
   772  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   773  		map[string][]filter.Table{"test-source": {
   774  			{Schema: "test-db", Name: "test-table-1"},
   775  			{Schema: "test-db", Name: "test-table-2"},
   776  		}},
   777  		false,
   778  		1)
   779  	result = checker.Check(ctx)
   780  	require.Equal(t, StateWarning, result.State)
   781  	require.Len(t, result.Errors, 1)
   782  	require.Contains(t, result.Errors[0].ShortErr, "Unknown character set: 'utf16'")
   783  	require.NoError(t, mock.ExpectationsWereMet())
   784  
   785  	// 2.2 not the first table has unknown charset
   786  
   787  	mock = initShardingMock(mock)
   788  	createTableRow2 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   789  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
   790    "c" int(11) NOT NULL,
   791    PRIMARY KEY ("c")
   792  ) ENGINE=InnoDB DEFAULT CHARSET=utf16`)
   793  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   794  
   795  	checker = NewShardingTablesChecker("test-name",
   796  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   797  		map[string][]filter.Table{"test-source": {
   798  			{Schema: "test-db", Name: "test-table-1"},
   799  			{Schema: "test-db", Name: "test-table-2"},
   800  		}},
   801  		false,
   802  		1)
   803  	result = checker.Check(ctx)
   804  	require.Equal(t, StateWarning, result.State)
   805  	require.Len(t, result.Errors, 1)
   806  	require.Contains(t, result.Errors[0].ShortErr, "Unknown character set: 'utf16'")
   807  	require.NoError(t, mock.ExpectationsWereMet())
   808  
   809  	// 2.3 not the first table has unknown collation
   810  
   811  	mock = initShardingMock(mock)
   812  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
   813  		AddRow("test-table-2", `CREATE TABLE "test-table-2" (
   814    "c" int(11) NOT NULL,
   815    PRIMARY KEY ("c")
   816  ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_german1_ci`)
   817  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   818  
   819  	checker = NewShardingTablesChecker("test-name",
   820  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   821  		map[string][]filter.Table{"test-source": {
   822  			{Schema: "test-db", Name: "test-table-1"},
   823  			{Schema: "test-db", Name: "test-table-2"},
   824  		}},
   825  		false,
   826  		1)
   827  	result = checker.Check(ctx)
   828  	// unknown collation will not raise error during parsing
   829  	require.Equal(t, StateSuccess, result.State)
   830  	require.NoError(t, mock.ExpectationsWereMet())
   831  
   832  	// 3. test OptimisticShardingTablesChecker
   833  
   834  	maxConnecionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("max_connections", "2")
   835  	mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnecionsRow)
   836  	sqlModeRow = sqlmock.NewRows([]string{"Variable_name", "Value"}).AddRow("sql_mode", "ANSI_QUOTES")
   837  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   838  	createTableRow = sqlmock.NewRows([]string{"Table", "Create Table"}).AddRow("test-table-1", `
   839  CREATE TABLE "test-table-1" (
   840    "c" int(11) NOT NULL,
   841    PRIMARY KEY ("c")
   842  ) ENGINE=InnoDB DEFAULT CHARSET=utf16`)
   843  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   844  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).AddRow("test-table-2", `
   845  CREATE TABLE "test-table-2" (
   846    "c" int(11) NOT NULL,
   847    PRIMARY KEY ("c")
   848  ) ENGINE=InnoDB DEFAULT CHARSET=utf16`)
   849  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-2`").WillReturnRows(createTableRow2)
   850  	checker = NewOptimisticShardingTablesChecker(
   851  		"test-name",
   852  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   853  		map[string][]filter.Table{"test-source": {
   854  			{Schema: "test-db", Name: "test-table-1"},
   855  			{Schema: "test-db", Name: "test-table-2"},
   856  		}},
   857  		0)
   858  	result = checker.Check(ctx)
   859  	require.Equal(t, StateWarning, result.State)
   860  	require.Len(t, result.Errors, 2)
   861  	require.Contains(t, result.Errors[0].ShortErr, "Unknown character set: 'utf16'")
   862  	require.Contains(t, result.Errors[1].ShortErr, "Unknown character set: 'utf16'")
   863  	require.NoError(t, mock.ExpectationsWereMet())
   864  }
   865  
   866  func initShardingMock(mock sqlmock.Sqlmock) sqlmock.Sqlmock {
   867  	sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   868  		AddRow("sql_mode", "ANSI_QUOTES")
   869  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   870  	createTableRow := sqlmock.NewRows([]string{"Table", "Create Table"}).
   871  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   872  "c" int(11) NOT NULL,
   873  PRIMARY KEY ("c")
   874  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   875  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   876  
   877  	maxConnecionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   878  		AddRow("max_connections", "2")
   879  	mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnecionsRow)
   880  	sqlModeRow = sqlmock.NewRows([]string{"Variable_name", "Value"}).
   881  		AddRow("sql_mode", "ANSI_QUOTES")
   882  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   883  	createTableRow = sqlmock.NewRows([]string{"Table", "Create Table"}).
   884  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   885  "c" int(11) NOT NULL,
   886  PRIMARY KEY ("c")
   887  ) ENGINE=InnoDB DEFAULT CHARSET=latin1`)
   888  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   889  	return mock
   890  }
   891  
   892  func TestExpressionUK(t *testing.T) {
   893  	db, mock, err := sqlmock.New()
   894  	require.NoError(t, err)
   895  	downDB, downMock, err := sqlmock.New()
   896  	require.NoError(t, err)
   897  	ctx := context.Background()
   898  
   899  	// test same table structure
   900  
   901  	maxConnectionsRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   902  		AddRow("max_connections", "2")
   903  	mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnectionsRow)
   904  	sqlModeRow := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   905  		AddRow("sql_mode", "ANSI_QUOTES")
   906  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   907  	createTableRow := sqlmock.NewRows([]string{"Table", "Create Table"}).
   908  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   909  		  "c" int(11) NOT NULL,
   910  		  "c2" int(11) NOT NULL,
   911  		  PRIMARY KEY ("c"),
   912  		  UNIQUE KEY "uk" (("c2"+1), "c")
   913  		) ENGINE=InnoDB`)
   914  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   915  	sqlModeRow2 := sqlmock.NewRows([]string{"Variable_name", "Value"}).
   916  		AddRow("sql_mode", "ANSI_QUOTES")
   917  	downMock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow2)
   918  	createTableRow2 := sqlmock.NewRows([]string{"Table", "Create Table"}).
   919  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   920  		  "c" int(11) NOT NULL,
   921  		  "c2" int(11) NOT NULL,
   922  		  PRIMARY KEY ("c"),
   923  		  UNIQUE KEY "uk" (("c2"+1), "c")
   924  		) ENGINE=InnoDB`)
   925  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow2)
   926  
   927  	checker := NewTablesChecker(
   928  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   929  		conn.NewBaseDBForTest(downDB),
   930  		map[string]map[filter.Table][]filter.Table{
   931  			"test-source": {
   932  				{Schema: "test-db", Name: "test-table-1"}: {
   933  					{Schema: "test-db", Name: "test-table-1"},
   934  				},
   935  			},
   936  		},
   937  		nil,
   938  		1)
   939  	result := checker.Check(ctx)
   940  	require.Equal(t, StateSuccess, result.State)
   941  	require.NoError(t, mock.ExpectationsWereMet())
   942  	require.NoError(t, downMock.ExpectationsWereMet())
   943  
   944  	// test different table structure
   945  
   946  	maxConnectionsRow = sqlmock.NewRows([]string{"Variable_name", "Value"}).
   947  		AddRow("max_connections", "2")
   948  	mock.ExpectQuery("SHOW VARIABLES LIKE 'max_connections'").WillReturnRows(maxConnectionsRow)
   949  	sqlModeRow = sqlmock.NewRows([]string{"Variable_name", "Value"}).
   950  		AddRow("sql_mode", "ANSI_QUOTES")
   951  	mock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow)
   952  	createTableRow = sqlmock.NewRows([]string{"Table", "Create Table"}).
   953  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   954  		  "c" int(11) NOT NULL,
   955  		  "c2" int(11) NOT NULL,
   956  		  PRIMARY KEY ("c"),
   957  		  UNIQUE KEY "uk" (("c2"+1), "c")
   958  		) ENGINE=InnoDB`)
   959  	mock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow)
   960  	sqlModeRow2 = sqlmock.NewRows([]string{"Variable_name", "Value"}).
   961  		AddRow("sql_mode", "ANSI_QUOTES")
   962  	downMock.ExpectQuery("SHOW VARIABLES LIKE 'sql_mode'").WillReturnRows(sqlModeRow2)
   963  	createTableRow2 = sqlmock.NewRows([]string{"Table", "Create Table"}).
   964  		AddRow("test-table-1", `CREATE TABLE "test-table-1" (
   965  		  "c" int(11) NOT NULL,
   966  		  "c2" int(11) NOT NULL,
   967  		  PRIMARY KEY ("c"),
   968  		  UNIQUE KEY "uk" (("c2"+3), "c")
   969  		) ENGINE=InnoDB`)
   970  	downMock.ExpectQuery("SHOW CREATE TABLE `test-db`.`test-table-1`").WillReturnRows(createTableRow2)
   971  
   972  	checker = NewTablesChecker(
   973  		map[string]*conn.BaseDB{"test-source": conn.NewBaseDBForTest(db)},
   974  		conn.NewBaseDBForTest(downDB),
   975  		map[string]map[filter.Table][]filter.Table{
   976  			"test-source": {
   977  				{Schema: "test-db", Name: "test-table-1"}: {
   978  					{Schema: "test-db", Name: "test-table-1"},
   979  				},
   980  			},
   981  		},
   982  		nil,
   983  		1)
   984  	result = checker.Check(ctx)
   985  	require.Equal(t, StateWarning, result.State)
   986  	require.Len(t, result.Errors, 2)
   987  	// maybe [`c2`+1 c] or [c `c2`+1]
   988  	require.Contains(t, result.Errors[0].ShortErr, "upstream has more PK or NOT NULL UK than downstream")
   989  	require.Contains(t, result.Errors[0].ShortErr, "`c2`+1")
   990  	require.Contains(t, result.Errors[1].ShortErr, "downstream has more PK or NOT NULL UK than upstream")
   991  	require.Contains(t, result.Errors[1].ShortErr, "`c2`+3")
   992  	require.NoError(t, mock.ExpectationsWereMet())
   993  	require.NoError(t, downMock.ExpectationsWereMet())
   994  }