github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/syncer/ddl_test.go (about)

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