github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/engine/pkg/meta/internal/sqlkv/sql_impl_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 sqlkv 15 16 import ( 17 "context" 18 "database/sql" 19 "database/sql/driver" 20 "fmt" 21 "reflect" 22 "regexp" 23 "testing" 24 "time" 25 26 "github.com/DATA-DOG/go-sqlmock" 27 "github.com/VividCortex/mysqlerr" 28 "github.com/go-sql-driver/mysql" 29 "github.com/pingcap/log" 30 sqlkvModel "github.com/pingcap/tiflow/engine/pkg/meta/internal/sqlkv/model" 31 metaModel "github.com/pingcap/tiflow/engine/pkg/meta/model" 32 "github.com/pingcap/tiflow/pkg/errors" 33 "github.com/stretchr/testify/require" 34 "go.uber.org/zap/zapcore" 35 ) 36 37 const ( 38 fakeJob = "fakeJob" 39 fakeTable = "fakeTable" 40 defaultTestStoreType = metaModel.StoreTypeMySQL 41 ) 42 43 type tCase struct { 44 caseName string // case name 45 fn string // function name 46 inputs []interface{} // function args 47 48 output interface{} // function output 49 err error // function error 50 51 mockExpectResFn func(mock sqlmock.Sqlmock) // sqlmock expectation 52 } 53 54 func mockGetDBConn(t *testing.T, table string) (*sql.DB, sqlmock.Sqlmock) { 55 db, mock, err := sqlmock.New() 56 require.Nil(t, err) 57 // common execution for orm 58 mock.ExpectQuery("SELECT VERSION()"). 59 WillReturnRows(sqlmock.NewRows([]string{"VERSION()"}). 60 AddRow("5.7.35-log")) 61 mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf("CREATE TABLE `%s` (`seq_id` bigint unsigned AUTO_INCREMENT,"+ 62 "`created_at` datetime(3) NULL,`updated_at` datetime(3) NULL,`meta_key` varbinary(2048) not null,`meta_value` longblob,"+ 63 "`job_id` varchar(64) not null,PRIMARY KEY (`seq_id`),UNIQUE INDEX `uidx_jk` (`job_id`,`meta_key`))", table))). 64 WillReturnResult(sqlmock.NewResult(1, 1)) 65 return db, mock 66 } 67 68 type anyTime struct{} 69 70 func (a anyTime) Match(v driver.Value) bool { 71 _, ok := v.(time.Time) 72 return ok 73 } 74 75 func TestNewSQLImpl(t *testing.T) { 76 t.Parallel() 77 78 sqlDB, mock := mockGetDBConn(t, fakeTable) 79 80 defer sqlDB.Close() //nolint:staticcheck 81 defer mock.ExpectClose() 82 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, fakeTable, fakeJob) 83 defer cli.Close() //nolint:staticcheck 84 require.Nil(t, err) 85 require.NotNil(t, cli) 86 } 87 88 func TestPut(t *testing.T) { 89 t.Parallel() 90 91 sqlDB, mock := mockGetDBConn(t, fakeTable) 92 defer sqlDB.Close() //nolint:staticcheck 93 defer mock.ExpectClose() 94 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, fakeTable, fakeJob) 95 defer cli.Close() //nolint:staticcheck 96 require.Nil(t, err) 97 require.NotNil(t, cli) 98 var anyT anyTime 99 100 testCases := []tCase{ 101 { 102 fn: "Put", 103 inputs: []interface{}{ 104 "key0", 105 "value0", 106 }, 107 output: &metaModel.PutResponse{ 108 Header: &metaModel.ResponseHeader{}, 109 }, 110 mockExpectResFn: func(mock sqlmock.Sqlmock) { 111 mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `fakeTable` (`created_at`,`updated_at`,"+ 112 "`meta_key`,`meta_value`,`job_id`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `updated_at`=?,"+ 113 "`meta_key`=VALUES(`meta_key`),`meta_value`=VALUES(`meta_value`),`job_id`=VALUES(`job_id`)")). 114 WithArgs(anyT, anyT, []byte("key0"), []byte("value0"), fakeJob, anyT). 115 WillReturnResult(sqlmock.NewResult(1, 1)) 116 }, 117 }, 118 } 119 120 for _, tc := range testCases { 121 testInner(t, mock, cli, tc) 122 } 123 } 124 125 func TestGet(t *testing.T) { 126 t.Parallel() 127 128 sqlDB, mock := mockGetDBConn(t, fakeTable) 129 defer sqlDB.Close() //nolint:staticcheck 130 defer mock.ExpectClose() 131 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, fakeTable, fakeJob) 132 require.Nil(t, err) 133 require.NotNil(t, cli) 134 135 testCases := []tCase{ 136 { 137 caseName: "RecordNotFoundErrReturnEmptyResp", 138 fn: "Get", 139 inputs: []interface{}{ 140 "key0", 141 }, 142 output: &metaModel.GetResponse{ 143 Header: &metaModel.ResponseHeader{}, 144 Kvs: []*metaModel.KeyValue{}, 145 }, 146 mockExpectResFn: func(mock sqlmock.Sqlmock) { 147 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE job_id = ? AND "+ 148 "meta_key = ? ORDER BY `fakeTable`.`seq_id` LIMIT 1")). 149 WithArgs(fakeJob, []byte("key0")). 150 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"})) 151 }, 152 }, 153 { 154 caseName: "NormalGet", 155 fn: "Get", 156 inputs: []interface{}{ 157 "key0", 158 }, 159 output: &metaModel.GetResponse{ 160 Header: &metaModel.ResponseHeader{}, 161 Kvs: []*metaModel.KeyValue{ 162 { 163 Key: []byte("key0"), 164 Value: []byte("value0"), 165 }, 166 }, 167 }, 168 mockExpectResFn: func(mock sqlmock.Sqlmock) { 169 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE job_id = ? AND "+ 170 "meta_key = ? ORDER BY `fakeTable`.`seq_id` LIMIT 1")). 171 WithArgs(fakeJob, []byte("key0")). 172 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}).AddRow("key0", "value0")) 173 }, 174 }, 175 { 176 caseName: "RangeGet", 177 fn: "Get", 178 inputs: []interface{}{ 179 "key0", 180 metaModel.WithRange("key999"), 181 }, 182 output: &metaModel.GetResponse{ 183 Header: &metaModel.ResponseHeader{}, 184 Kvs: []*metaModel.KeyValue{ 185 { 186 Key: []byte("key0"), 187 Value: []byte("value0"), 188 }, 189 { 190 Key: []byte("key1"), 191 Value: []byte("value1"), 192 }, 193 }, 194 }, 195 mockExpectResFn: func(mock sqlmock.Sqlmock) { 196 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE "+ 197 "job_id = ? AND (meta_key >= ? AND meta_key < ?)")). 198 WithArgs(fakeJob, []byte("key0"), []byte("key999")). 199 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}). 200 AddRow("key0", "value0").AddRow("key1", "value1")) 201 }, 202 }, 203 { 204 caseName: "FromKeyGet", 205 fn: "Get", 206 inputs: []interface{}{ 207 "key0", 208 metaModel.WithFromKey(), 209 }, 210 output: &metaModel.GetResponse{ 211 Header: &metaModel.ResponseHeader{}, 212 Kvs: []*metaModel.KeyValue{ 213 { 214 Key: []byte("key0"), 215 Value: []byte("value0"), 216 }, 217 }, 218 }, 219 mockExpectResFn: func(mock sqlmock.Sqlmock) { 220 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE job_id = ? AND meta_key >= ?")). 221 WithArgs(fakeJob, []byte("key0")). 222 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}). 223 AddRow("key0", "value0")) 224 }, 225 }, 226 { 227 caseName: "PrefixGet", 228 fn: "Get", 229 inputs: []interface{}{ 230 "key0", 231 metaModel.WithPrefix(), 232 }, 233 output: &metaModel.GetResponse{ 234 Header: &metaModel.ResponseHeader{}, 235 Kvs: []*metaModel.KeyValue{ 236 { 237 Key: []byte("key0"), 238 Value: []byte("value0"), 239 }, 240 }, 241 }, 242 mockExpectResFn: func(mock sqlmock.Sqlmock) { 243 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE job_id = ? AND meta_key like ?")). 244 WithArgs(fakeJob, []byte("key0%")). 245 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}). 246 AddRow("key0", "value0")) 247 }, 248 }, 249 } 250 251 for _, tc := range testCases { 252 testInner(t, mock, cli, tc) 253 } 254 } 255 256 func TestDelete(t *testing.T) { 257 t.Parallel() 258 259 sqlDB, mock := mockGetDBConn(t, fakeTable) 260 defer sqlDB.Close() //nolint:staticcheck 261 defer mock.ExpectClose() 262 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, fakeTable, fakeJob) 263 require.Nil(t, err) 264 require.NotNil(t, cli) 265 266 testCases := []tCase{ 267 { 268 fn: "Delete", 269 inputs: []interface{}{ 270 "key0", 271 }, 272 output: &metaModel.DeleteResponse{ 273 Header: &metaModel.ResponseHeader{}, 274 }, 275 mockExpectResFn: func(mock sqlmock.Sqlmock) { 276 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `fakeTable` WHERE job_id = ? AND meta_key = ?")). 277 WithArgs(fakeJob, []byte("key0")). 278 WillReturnResult(sqlmock.NewResult(1, 1)) 279 }, 280 }, 281 { 282 fn: "Delete", 283 inputs: []interface{}{ 284 "key0", 285 metaModel.WithRange("key999"), 286 }, 287 output: &metaModel.DeleteResponse{ 288 Header: &metaModel.ResponseHeader{}, 289 }, 290 mockExpectResFn: func(mock sqlmock.Sqlmock) { 291 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `fakeTable` "+ 292 "WHERE job_id = ? AND (meta_key >= ? AND meta_key < ?)")). 293 WithArgs(fakeJob, []byte("key0"), []byte("key999")). 294 WillReturnResult(sqlmock.NewResult(1, 1)) 295 }, 296 }, 297 { 298 fn: "Delete", 299 inputs: []interface{}{ 300 "key0", 301 metaModel.WithFromKey(), 302 }, 303 output: &metaModel.DeleteResponse{ 304 Header: &metaModel.ResponseHeader{}, 305 }, 306 mockExpectResFn: func(mock sqlmock.Sqlmock) { 307 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `fakeTable` WHERE job_id = ? AND meta_key >= ?")). 308 WithArgs(fakeJob, []byte("key0")). 309 WillReturnResult(sqlmock.NewResult(1, 1)) 310 }, 311 }, 312 { 313 fn: "Delete", 314 inputs: []interface{}{ 315 "key0", 316 metaModel.WithPrefix(), 317 }, 318 output: &metaModel.DeleteResponse{ 319 Header: &metaModel.ResponseHeader{}, 320 }, 321 mockExpectResFn: func(mock sqlmock.Sqlmock) { 322 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `fakeTable` WHERE job_id = ? AND meta_key like ?")). 323 WithArgs(fakeJob, []byte("key0%")). 324 WillReturnResult(sqlmock.NewResult(1, 1)) 325 }, 326 }, 327 } 328 329 for _, tc := range testCases { 330 testInner(t, mock, cli, tc) 331 } 332 } 333 334 func TestTxn(t *testing.T) { 335 t.Parallel() 336 337 sqlDB, mock := mockGetDBConn(t, fakeTable) 338 defer sqlDB.Close() //nolint:staticcheck 339 defer mock.ExpectClose() 340 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, fakeTable, fakeJob) 341 require.Nil(t, err) 342 require.NotNil(t, cli) 343 var anyT anyTime 344 345 txn := cli.Txn(context.Background()) 346 txn.Do(metaModel.OpGet("key0", metaModel.WithRange("key999"))) 347 txn.Do(metaModel.OpPut("key1", "value1")) 348 txn.Do(metaModel.OpDelete("key2", metaModel.WithPrefix())) 349 350 mock.ExpectBegin() 351 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `fakeTable` WHERE job_id = ? AND (meta_key >= ? AND meta_key < ?)")). 352 WithArgs(fakeJob, []byte("key0"), []byte("key999")). 353 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}).AddRow("key0", "value0").AddRow("key1", "value1")) 354 mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `fakeTable` (`created_at`,`updated_at`,"+ 355 "`meta_key`,`meta_value`,`job_id`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `updated_at`=?,"+ 356 "`meta_key`=VALUES(`meta_key`),`meta_value`=VALUES(`meta_value`),`job_id`=VALUES(`job_id`)")). 357 WithArgs(anyT, anyT, []byte("key1"), []byte("value1"), fakeJob, anyT). 358 WillReturnResult(sqlmock.NewResult(1, 1)) 359 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `fakeTable` WHERE job_id = ? AND meta_key like ?")). 360 WithArgs(fakeJob, []byte("key2%")). 361 WillReturnResult(sqlmock.NewResult(1, 1)) 362 mock.ExpectCommit() 363 364 rsp, err := txn.Commit() 365 require.NoError(t, err) 366 require.Len(t, rsp.Responses, 3) 367 368 rr := rsp.Responses[0].GetResponseGet() 369 require.NotNil(t, rr) 370 require.Len(t, rr.Kvs, 2) 371 require.Equal(t, []byte("key0"), rr.Kvs[0].Key) 372 require.Equal(t, []byte("value0"), rr.Kvs[0].Value) 373 require.Equal(t, []byte("key1"), rr.Kvs[1].Key) 374 require.Equal(t, []byte("value1"), rr.Kvs[1].Value) 375 376 rr1 := rsp.Responses[1].GetResponsePut() 377 require.NotNil(t, rr1) 378 379 rr2 := rsp.Responses[2].GetResponseDelete() 380 require.NotNil(t, rr2) 381 } 382 383 func testInner(t *testing.T, m sqlmock.Sqlmock, cli *sqlKVClientImpl, c tCase) { 384 // set the mock expectation 385 c.mockExpectResFn(m) 386 387 var args []reflect.Value 388 args = append(args, reflect.ValueOf(context.Background())) 389 for _, ip := range c.inputs { 390 args = append(args, reflect.ValueOf(ip)) 391 } 392 result := reflect.ValueOf(cli).MethodByName(c.fn).Call(args) 393 // only error 394 if len(result) == 1 { 395 if c.err == nil { 396 require.Nil(t, result[0].Interface()) 397 } else { 398 require.NotNil(t, result[0].Interface()) 399 require.Error(t, result[0].Interface().(error)) 400 } 401 } else if len(result) == 2 { 402 // result and error 403 if c.err != nil { 404 require.NotNil(t, result[1].Interface()) 405 require.Error(t, result[1].Interface().(error)) 406 } else { 407 require.Equal(t, c.output, result[0].Interface()) 408 } 409 } 410 require.NoError(t, m.ExpectationsWereMet()) 411 } 412 413 func TestSQLImplWithoutNamespace(t *testing.T) { 414 t.Parallel() 415 416 sqlDB, mock := mockGetDBConn(t, sqlkvModel.MetaKVTableName) 417 defer sqlDB.Close() //nolint:staticcheck 418 defer mock.ExpectClose() 419 cli, err := NewSQLKVClientImpl(sqlDB, defaultTestStoreType, sqlkvModel.MetaKVTableName, "") 420 require.Nil(t, err) 421 require.NotNil(t, cli) 422 var anyT anyTime 423 ctx := context.TODO() 424 425 mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `meta_kvs` (`created_at`,`updated_at`,"+ 426 "`meta_key`,`meta_value`,`job_id`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `updated_at`=?,"+ 427 "`meta_key`=VALUES(`meta_key`),`meta_value`=VALUES(`meta_value`),`job_id`=VALUES(`job_id`)")). 428 WithArgs(anyT, anyT, []byte("key0"), []byte("value0"), "", anyT). 429 WillReturnResult(sqlmock.NewResult(1, 1)) 430 cli.Put(ctx, "key0", "value0") 431 432 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `meta_kvs` WHERE job_id = ? AND "+ 433 "meta_key = ? ORDER BY `meta_kvs`.`seq_id` LIMIT 1")). 434 WithArgs("", []byte("key1")). 435 WillReturnRows(sqlmock.NewRows([]string{"key", "value"})) 436 cli.Get(ctx, "key1") 437 438 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `meta_kvs` WHERE job_id = ? AND meta_key = ?")). 439 WithArgs("", []byte("key2")). 440 WillReturnResult(sqlmock.NewResult(1, 1)) 441 cli.Delete(ctx, "key2") 442 443 mock.ExpectBegin() 444 mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `meta_kvs` WHERE job_id = ? AND (meta_key >= ? AND meta_key < ?)")). 445 WithArgs("", []byte("key0"), []byte("key999")). 446 WillReturnRows(sqlmock.NewRows([]string{"meta_key", "meta_value"}).AddRow("key0", "value0").AddRow("key1", "value1")) 447 mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `meta_kvs` (`created_at`,`updated_at`,"+ 448 "`meta_key`,`meta_value`,`job_id`) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE `updated_at`=?,"+ 449 "`meta_key`=VALUES(`meta_key`),`meta_value`=VALUES(`meta_value`),`job_id`=VALUES(`job_id`)")). 450 WithArgs(anyT, anyT, []byte("key1"), []byte("value1"), "", anyT). 451 WillReturnResult(sqlmock.NewResult(1, 1)) 452 mock.ExpectExec(regexp.QuoteMeta("DELETE FROM `meta_kvs` WHERE job_id = ? AND meta_key like ?")). 453 WithArgs("", []byte("key2%")). 454 WillReturnResult(sqlmock.NewResult(1, 1)) 455 mock.ExpectCommit() 456 txn := cli.Txn(context.Background()) 457 txn.Do(metaModel.OpGet("key0", metaModel.WithRange("key999"))) 458 txn.Do(metaModel.OpPut("key1", "value1")) 459 txn.Do(metaModel.OpDelete("key2", metaModel.WithPrefix())) 460 txn.Commit() 461 require.NoError(t, mock.ExpectationsWereMet()) 462 } 463 464 func TestInitializeError(t *testing.T) { 465 t.Parallel() 466 467 log.SetLevel(zapcore.DebugLevel) 468 defer log.SetLevel(zapcore.InfoLevel) 469 470 db, mock, err := sqlmock.New() 471 require.Nil(t, err) 472 defer db.Close() 473 defer mock.ExpectClose() 474 475 // table exists error 476 mock.ExpectQuery("SELECT VERSION()"). 477 WillReturnRows(sqlmock.NewRows([]string{"VERSION()"}). 478 AddRow("5.7.35-log")) 479 mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"SCHEMA_NAME"})) 480 mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf("CREATE TABLE `%s`", "test"))). 481 WillReturnError(&mysql.MySQLError{Number: mysqlerr.ER_TABLE_EXISTS_ERROR, Message: "table already exists"}) 482 cli, err := NewSQLKVClientImpl(db, defaultTestStoreType, "test", "") 483 require.Nil(t, err) 484 require.NotNil(t, cli) 485 defer cli.Close() 486 487 // other mysql error 488 mock.ExpectQuery("SELECT VERSION()"). 489 WillReturnRows(sqlmock.NewRows([]string{"VERSION()"}). 490 AddRow("5.7.35-log")) 491 mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"SCHEMA_NAME"})) 492 mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf("CREATE TABLE `%s`", "test"))). 493 WillReturnError(&mysql.MySQLError{Number: mysqlerr.ER_WRONG_OUTER_JOIN, Message: "other mysql error"}) 494 _, err = NewSQLKVClientImpl(db, defaultTestStoreType, "test", "") 495 require.Regexp(t, "other mysql error", err.Error()) 496 // other error 497 mock.ExpectQuery("SELECT VERSION()"). 498 WillReturnRows(sqlmock.NewRows([]string{"VERSION()"}). 499 AddRow("5.7.35-log")) 500 mock.ExpectQuery(".*").WillReturnRows(sqlmock.NewRows([]string{"SCHEMA_NAME"})) 501 mock.ExpectExec(regexp.QuoteMeta(fmt.Sprintf("CREATE TABLE `%s`", "test"))). 502 WillReturnError(errors.New("other error")) 503 _, err = NewSQLKVClientImpl(db, defaultTestStoreType, "test", "") 504 require.Regexp(t, "other error", err.Error()) 505 require.NoError(t, mock.ExpectationsWereMet()) 506 }