github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/sqlmodel/row_change_test.go (about) 1 // Copyright 2022 PingCAP, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package sqlmodel 15 16 import ( 17 "testing" 18 19 "github.com/pingcap/tidb/pkg/ddl" 20 "github.com/pingcap/tidb/pkg/parser" 21 "github.com/pingcap/tidb/pkg/parser/ast" 22 "github.com/pingcap/tidb/pkg/parser/charset" 23 timodel "github.com/pingcap/tidb/pkg/parser/model" 24 _ "github.com/pingcap/tidb/pkg/planner/core" // init expression.EvalSimpleAst related function 25 timock "github.com/pingcap/tidb/pkg/util/mock" 26 cdcmodel "github.com/pingcap/tiflow/cdc/model" 27 "github.com/pingcap/tiflow/dm/pkg/log" 28 "github.com/pingcap/tiflow/dm/pkg/utils" 29 "github.com/stretchr/testify/require" 30 "github.com/stretchr/testify/suite" 31 ) 32 33 func mockTableInfo(t *testing.T, sql string) *timodel.TableInfo { 34 p := parser.New() 35 se := timock.NewContext() 36 node, err := p.ParseOneStmt(sql, "", "") 37 require.NoError(t, err) 38 dbChs, dbColl := charset.GetDefaultCharsetAndCollate() 39 ti, err := ddl.BuildTableInfoWithStmt(se, node.(*ast.CreateTableStmt), dbChs, dbColl, nil) 40 require.NoError(t, err) 41 return ti 42 } 43 44 type dpanicSuite struct { 45 suite.Suite 46 } 47 48 func (s *dpanicSuite) SetupSuite() { 49 err := log.InitLogger(&log.Config{Level: "debug"}) 50 s.NoError(err) 51 } 52 53 func TestDpanicSuite(t *testing.T) { 54 suite.Run(t, new(dpanicSuite)) 55 } 56 57 func TestNewRowChange(t *testing.T) { 58 t.Parallel() 59 60 source := &cdcmodel.TableName{Schema: "db", Table: "tbl"} 61 target := &cdcmodel.TableName{Schema: "db", Table: "tbl_routed"} 62 sourceTI := mockTableInfo(t, "CREATE TABLE tbl (id INT PRIMARY KEY, name INT)") 63 targetTI := mockTableInfo(t, "CREATE TABLE tbl_routed (id INT PRIMARY KEY, name INT)") 64 tiSession := utils.NewSessionCtx(map[string]string{ 65 "time_zone": "+08:00", 66 }) 67 68 expected := &RowChange{ 69 sourceTable: source, 70 targetTable: target, 71 preValues: []interface{}{1, 2}, 72 postValues: []interface{}{1, 3}, 73 sourceTableInfo: sourceTI, 74 targetTableInfo: targetTI, 75 tiSessionCtx: tiSession, 76 tp: RowChangeUpdate, 77 whereHandle: nil, 78 } 79 80 actual := NewRowChange(source, target, []interface{}{1, 2}, []interface{}{1, 3}, sourceTI, targetTI, tiSession) 81 require.Equal(t, expected, actual) 82 83 actual.lazyInitWhereHandle() 84 require.NotNil(t, actual.whereHandle) 85 86 // test some arguments of NewRowChange can be nil 87 88 expected.targetTable = expected.sourceTable 89 expected.targetTableInfo = expected.sourceTableInfo 90 expected.tiSessionCtx = utils.ZeroSessionCtx 91 expected.whereHandle = nil 92 actual = NewRowChange(source, nil, []interface{}{1, 2}, []interface{}{1, 3}, sourceTI, nil, nil) 93 require.Equal(t, expected, actual) 94 } 95 96 func (s *dpanicSuite) TestRowChangeType() { 97 change := &RowChange{preValues: []interface{}{1}} 98 change.calculateType() 99 s.Equal(RowChangeDelete, change.tp) 100 change = &RowChange{preValues: []interface{}{1}, postValues: []interface{}{2}} 101 change.calculateType() 102 s.Equal(RowChangeUpdate, change.tp) 103 change = &RowChange{postValues: []interface{}{1}} 104 change.calculateType() 105 s.Equal(RowChangeInsert, change.tp) 106 107 s.Panics(func() { 108 change = &RowChange{} 109 change.calculateType() 110 }) 111 } 112 113 func (s *dpanicSuite) TestGenDelete() { 114 source := &cdcmodel.TableName{Schema: "db", Table: "tb1"} 115 target := &cdcmodel.TableName{Schema: "db", Table: "tb2"} 116 117 cases := []struct { 118 sourceCreateSQL string 119 targetCreateSQL string 120 preValues []interface{} 121 122 expectedSQL string 123 expectedArgs []interface{} 124 }{ 125 { 126 "CREATE TABLE tb1 (id INT PRIMARY KEY, name INT)", 127 "CREATE TABLE tb2 (id INT PRIMARY KEY, name INT, extra VARCHAR(20))", 128 []interface{}{1, 2}, 129 130 "DELETE FROM `db`.`tb2` WHERE `id` = ? LIMIT 1", 131 []interface{}{1}, 132 }, 133 { 134 "CREATE TABLE tb1 (c INT, c2 INT UNIQUE)", 135 "CREATE TABLE tb2 (c INT, c2 INT UNIQUE)", 136 []interface{}{1, 2}, 137 138 "DELETE FROM `db`.`tb2` WHERE `c2` = ? LIMIT 1", 139 []interface{}{2}, 140 }, 141 // next 2 cases test NULL value 142 { 143 "CREATE TABLE tb1 (c INT, c2 INT UNIQUE)", 144 "CREATE TABLE tb2 (c INT, c2 INT UNIQUE)", 145 []interface{}{1, nil}, 146 147 "DELETE FROM `db`.`tb2` WHERE `c` = ? AND `c2` IS ? LIMIT 1", 148 []interface{}{1, nil}, 149 }, 150 { 151 "CREATE TABLE tb1 (c INT, c2 INT)", 152 "CREATE TABLE tb2 (c INT, c2 INT)", 153 []interface{}{1, nil}, 154 155 "DELETE FROM `db`.`tb2` WHERE `c` = ? AND `c2` IS ? LIMIT 1", 156 []interface{}{1, nil}, 157 }, 158 // next 2 cases test using downstream table to generate WHERE 159 { 160 "CREATE TABLE tb1 (id INT PRIMARY KEY, user_id INT NOT NULL UNIQUE)", 161 "CREATE TABLE tb2 (new_id INT PRIMARY KEY, id INT, user_id INT NOT NULL UNIQUE)", 162 []interface{}{1, 2}, 163 164 "DELETE FROM `db`.`tb2` WHERE `user_id` = ? LIMIT 1", 165 []interface{}{2}, 166 }, 167 { 168 "CREATE TABLE tb1 (id INT PRIMARY KEY, c2 INT)", 169 "CREATE TABLE tb2 (new_id INT PRIMARY KEY, id INT, c2 INT)", 170 []interface{}{1, 2}, 171 172 "DELETE FROM `db`.`tb2` WHERE `id` = ? AND `c2` = ? LIMIT 1", 173 []interface{}{1, 2}, 174 }, 175 } 176 177 for _, c := range cases { 178 sourceTI := mockTableInfo(s.T(), c.sourceCreateSQL) 179 targetTI := mockTableInfo(s.T(), c.targetCreateSQL) 180 change := NewRowChange(source, target, c.preValues, nil, sourceTI, targetTI, nil) 181 sql, args := change.GenSQL(DMLDelete) 182 s.Equal(c.expectedSQL, sql) 183 s.Equal(c.expectedArgs, args) 184 } 185 186 // a RowChangeUpdate can still generate DELETE SQL 187 sourceTI := mockTableInfo(s.T(), "CREATE TABLE tb1 (id INT PRIMARY KEY, name INT)") 188 change := NewRowChange(source, nil, []interface{}{1, 2}, []interface{}{3, 4}, sourceTI, nil, nil) 189 sql, args := change.GenSQL(DMLDelete) 190 s.Equal("DELETE FROM `db`.`tb1` WHERE `id` = ? LIMIT 1", sql) 191 s.Equal([]interface{}{1}, args) 192 193 change = NewRowChange(source, nil, nil, []interface{}{3, 4}, sourceTI, nil, nil) 194 s.Panics(func() { 195 change.GenSQL(DMLDelete) 196 }) 197 } 198 199 func (s *dpanicSuite) TestGenUpdate() { 200 source := &cdcmodel.TableName{Schema: "db", Table: "tb1"} 201 target := &cdcmodel.TableName{Schema: "db", Table: "tb2"} 202 203 cases := []struct { 204 sourceCreateSQL string 205 targetCreateSQL string 206 preValues []interface{} 207 postValues []interface{} 208 209 expectedSQL string 210 expectedArgs []interface{} 211 }{ 212 { 213 "CREATE TABLE tb1 (id INT PRIMARY KEY, name INT)", 214 "CREATE TABLE tb2 (id INT PRIMARY KEY, name INT, extra VARCHAR(20))", 215 []interface{}{1, 2}, 216 []interface{}{3, 4}, 217 218 "UPDATE `db`.`tb2` SET `id` = ?, `name` = ? WHERE `id` = ? LIMIT 1", 219 []interface{}{3, 4, 1}, 220 }, 221 { 222 "CREATE TABLE tb1 (id INT UNIQUE, name INT)", 223 "CREATE TABLE tb2 (id INT UNIQUE, name INT)", 224 []interface{}{nil, 2}, 225 []interface{}{3, 4}, 226 227 "UPDATE `db`.`tb2` SET `id` = ?, `name` = ? WHERE `id` IS ? AND `name` = ? LIMIT 1", 228 []interface{}{3, 4, nil, 2}, 229 }, 230 { 231 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT)", 232 "CREATE TABLE tb2 (c INT, c2 INT)", 233 []interface{}{1, 2}, 234 []interface{}{3, 4}, 235 236 "UPDATE `db`.`tb2` SET `c` = ?, `c2` = ? WHERE `c` = ? AND `c2` = ? LIMIT 1", 237 []interface{}{3, 4, 1, 2}, 238 }, 239 // next 2 cases test generated column 240 { 241 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT AS (c+1))", 242 "CREATE TABLE tb2 (c INT PRIMARY KEY, c2 INT AS (c+1))", 243 []interface{}{1, 2}, 244 []interface{}{3, 4}, 245 246 "UPDATE `db`.`tb2` SET `c` = ? WHERE `c` = ? LIMIT 1", 247 []interface{}{3, 1}, 248 }, 249 { 250 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT AS (c+1))", 251 "CREATE TABLE tb2 (c INT PRIMARY KEY, c2 INT)", 252 []interface{}{1, 2}, 253 []interface{}{3, 4}, 254 255 "UPDATE `db`.`tb2` SET `c` = ?, `c2` = ? WHERE `c` = ? LIMIT 1", 256 []interface{}{3, 4, 1}, 257 }, 258 } 259 260 for _, c := range cases { 261 sourceTI := mockTableInfo(s.T(), c.sourceCreateSQL) 262 targetTI := mockTableInfo(s.T(), c.targetCreateSQL) 263 change := NewRowChange(source, target, c.preValues, c.postValues, sourceTI, targetTI, nil) 264 sql, args := change.GenSQL(DMLUpdate) 265 s.Equal(c.expectedSQL, sql) 266 s.Equal(c.expectedArgs, args) 267 } 268 269 sourceTI := mockTableInfo(s.T(), "CREATE TABLE tb1 (id INT PRIMARY KEY, name INT)") 270 change := NewRowChange(source, nil, nil, []interface{}{3, 4}, sourceTI, nil, nil) 271 s.Panics(func() { 272 change.GenSQL(DMLUpdate) 273 }) 274 } 275 276 func (s *dpanicSuite) TestExpressionIndex() { 277 source := &cdcmodel.TableName{Schema: "db", Table: "tb1"} 278 sql := `CREATE TABLE tb1 ( 279 id INT PRIMARY KEY, 280 j JSON, 281 UNIQUE KEY j_index ((cast(json_extract(j,'$[*]') as signed array)), id) 282 )` 283 ti := mockTableInfo(s.T(), sql) 284 change := NewRowChange(source, nil, nil, []interface{}{1, `[1,2,3]`}, ti, nil, nil) 285 sql, args := change.GenSQL(DMLInsert) 286 s.Equal("INSERT INTO `db`.`tb1` (`id`,`j`) VALUES (?,?)", sql) 287 s.Equal([]interface{}{1, `[1,2,3]`}, args) 288 require.Equal(s.T(), 2, change.ColumnCount()) 289 keys := change.CausalityKeys() 290 // TODO: need change it after future fix 291 require.Equal(s.T(), []string{"1.id.db.tb1"}, keys) 292 293 change2 := NewRowChange(source, nil, []interface{}{1, `[1,2,3]`}, []interface{}{1, `[1,2,3,4]`}, ti, nil, nil) 294 sql, args = change2.GenSQL(DMLUpdate) 295 s.Equal("UPDATE `db`.`tb1` SET `id` = ?, `j` = ? WHERE `id` = ? LIMIT 1", sql) 296 s.Equal([]interface{}{1, `[1,2,3,4]`, 1}, args) 297 298 change2.Reduce(change) 299 sql, args = change2.GenSQL(DMLInsert) 300 s.Equal("INSERT INTO `db`.`tb1` (`id`,`j`) VALUES (?,?)", sql) 301 s.Equal([]interface{}{1, `[1,2,3,4]`}, args) 302 } 303 304 func TestGenInsert(t *testing.T) { 305 t.Parallel() 306 307 source := &cdcmodel.TableName{Schema: "db", Table: "tb1"} 308 target := &cdcmodel.TableName{Schema: "db", Table: "tb2"} 309 310 cases := []struct { 311 sourceCreateSQL string 312 targetCreateSQL string 313 postValues []interface{} 314 315 expectedInsertSQL string 316 expectedReplaceSQL string 317 expectedInsertOnDupSQL string 318 expectedArgs []interface{} 319 }{ 320 { 321 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT)", 322 "CREATE TABLE tb2 (c INT PRIMARY KEY, c2 INT, extra VARCHAR(20))", 323 []interface{}{1, 2}, 324 325 "INSERT INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?)", 326 "REPLACE INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?)", 327 "INSERT INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?) ON DUPLICATE KEY UPDATE `c`=VALUES(`c`),`c2`=VALUES(`c2`)", 328 []interface{}{1, 2}, 329 }, 330 // next 2 cases test generated column 331 { 332 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT AS (c+1))", 333 "CREATE TABLE tb2 (c INT PRIMARY KEY, c2 INT AS (c+1))", 334 []interface{}{1, 2}, 335 336 "INSERT INTO `db`.`tb2` (`c`) VALUES (?)", 337 "REPLACE INTO `db`.`tb2` (`c`) VALUES (?)", 338 "INSERT INTO `db`.`tb2` (`c`) VALUES (?) ON DUPLICATE KEY UPDATE `c`=VALUES(`c`)", 339 []interface{}{1}, 340 }, 341 { 342 "CREATE TABLE tb1 (c INT PRIMARY KEY, c2 INT AS (c+1))", 343 "CREATE TABLE tb2 (c INT PRIMARY KEY, c2 INT)", 344 []interface{}{1, 2}, 345 346 "INSERT INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?)", 347 "REPLACE INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?)", 348 "INSERT INTO `db`.`tb2` (`c`,`c2`) VALUES (?,?) ON DUPLICATE KEY UPDATE `c`=VALUES(`c`),`c2`=VALUES(`c2`)", 349 []interface{}{1, 2}, 350 }, 351 } 352 353 for _, c := range cases { 354 sourceTI := mockTableInfo(t, c.sourceCreateSQL) 355 targetTI := mockTableInfo(t, c.targetCreateSQL) 356 change := NewRowChange(source, target, nil, c.postValues, sourceTI, targetTI, nil) 357 sql, args := change.GenSQL(DMLInsert) 358 require.Equal(t, c.expectedInsertSQL, sql) 359 require.Equal(t, c.expectedArgs, args) 360 sql, args = change.GenSQL(DMLReplace) 361 require.Equal(t, c.expectedReplaceSQL, sql) 362 require.Equal(t, c.expectedArgs, args) 363 sql, args = change.GenSQL(DMLInsertOnDuplicateUpdate) 364 require.Equal(t, c.expectedInsertOnDupSQL, sql) 365 require.Equal(t, c.expectedArgs, args) 366 } 367 }