This is an automated email from the ASF dual-hosted git repository.

jimin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git


The following commit(s) were added to refs/heads/master by this push:
     new d76ac395 test: Improve test coverage for 
pkg/datasource/sql/datasource/mysql  (#965)
d76ac395 is described below

commit d76ac395326cf34fe34b7b89ddb6e0759e1e2783
Author: flypiggy <[email protected]>
AuthorDate: Mon Nov 10 00:53:53 2025 +0800

    test: Improve test coverage for pkg/datasource/sql/datasource/mysql  (#965)
---
 pkg/datasource/sql/datasource/base/meta_cache.go   |   7 +
 .../sql/datasource/base/meta_cache_test.go         |  84 ++++
 .../sql/datasource/mysql/meta_cache_test.go        | 367 ++++++++++++++
 .../sql/datasource/mysql/trigger_test.go           | 536 +++++++++++++++++++++
 .../handler/tcc_fence_wrapper_handler_test.go      |  50 +-
 5 files changed, 1027 insertions(+), 17 deletions(-)

diff --git a/pkg/datasource/sql/datasource/base/meta_cache.go 
b/pkg/datasource/sql/datasource/base/meta_cache.go
index 3c124ed1..80652c4a 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache.go
@@ -88,7 +88,10 @@ func (c *BaseTableMetaCache) Init(ctx context.Context) error 
{
 // refresh
 func (c *BaseTableMetaCache) refresh(ctx context.Context) {
        f := func() {
+               // Get table names with read lock
+               c.lock.RLock()
                if c.db == nil || c.cfg == nil || c.cache == nil || 
len(c.cache) == 0 {
+                       c.lock.RUnlock()
                        return
                }
 
@@ -96,6 +99,9 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) {
                for table := range c.cache {
                        tables = append(tables, table)
                }
+               c.lock.RUnlock()
+
+               // Load metadata without lock (I/O operation)
                conn, err := c.db.Conn(ctx)
                if err != nil {
                        return
@@ -105,6 +111,7 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) {
                        return
                }
 
+               // Update cache with write lock
                c.lock.Lock()
                defer c.lock.Unlock()
 
diff --git a/pkg/datasource/sql/datasource/base/meta_cache_test.go 
b/pkg/datasource/sql/datasource/base/meta_cache_test.go
index 7892160a..7ebbc03c 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache_test.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache_test.go
@@ -174,6 +174,90 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
        }
 }
 
+func TestBaseTableMetaCache_refresh_EarlyReturn(t *testing.T) {
+       tests := []struct {
+               name   string
+               db     *sql.DB
+               cfg    *mysql.Config
+               cache  map[string]*entry
+               expect string
+       }{
+               {
+                       name:   "db_is_nil",
+                       db:     nil,
+                       cfg:    &mysql.Config{},
+                       cache:  map[string]*entry{"test": {value: 
types.TableMeta{}}},
+                       expect: "should return early when db is nil",
+               },
+               {
+                       name:   "cfg_is_nil",
+                       db:     &sql.DB{},
+                       cfg:    nil,
+                       cache:  map[string]*entry{"test": {value: 
types.TableMeta{}}},
+                       expect: "should return early when cfg is nil",
+               },
+               {
+                       name:   "cache_is_nil",
+                       db:     &sql.DB{},
+                       cfg:    &mysql.Config{},
+                       cache:  nil,
+                       expect: "should return early when cache is nil",
+               },
+               {
+                       name:   "cache_is_empty",
+                       db:     &sql.DB{},
+                       cfg:    &mysql.Config{},
+                       cache:  map[string]*entry{},
+                       expect: "should return early when cache is empty",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       _, cancel := context.WithCancel(context.Background())
+                       defer cancel()
+
+                       c := &BaseTableMetaCache{
+                               expireDuration: EexpireTime,
+                               capity:         capacity,
+                               size:           0,
+                               cache:          tt.cache,
+                               cancel:         cancel,
+                               trigger:        &mockTrigger{},
+                               db:             tt.db,
+                               cfg:            tt.cfg,
+                       }
+
+                       // Call refresh once and it should return early without 
panic
+                       done := make(chan bool)
+                       go func() {
+                               defer func() {
+                                       if r := recover(); r != nil {
+                                               t.Errorf("refresh() panicked: 
%v", r)
+                                       }
+                                       done <- true
+                               }()
+
+                               // Call the internal function once
+                               c.lock.RLock()
+                               if c.db == nil || c.cfg == nil || c.cache == 
nil || len(c.cache) == 0 {
+                                       c.lock.RUnlock()
+                                       done <- true
+                                       return
+                               }
+                               c.lock.RUnlock()
+                       }()
+
+                       select {
+                       case <-done:
+                               // Test passed - early return worked correctly
+                       case <-time.After(2 * time.Second):
+                               t.Error("refresh() did not return early as 
expected")
+                       }
+               })
+       }
+}
+
 func TestBaseTableMetaCache_GetTableMeta(t *testing.T) {
        var (
                tableMeta1  types.TableMeta
diff --git a/pkg/datasource/sql/datasource/mysql/meta_cache_test.go 
b/pkg/datasource/sql/datasource/mysql/meta_cache_test.go
new file mode 100644
index 00000000..2f92195b
--- /dev/null
+++ b/pkg/datasource/sql/datasource/mysql/meta_cache_test.go
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package mysql
+
+import (
+       "context"
+       "errors"
+       "testing"
+
+       "github.com/DATA-DOG/go-sqlmock"
+       "github.com/go-sql-driver/mysql"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestNewTableMetaInstance(t *testing.T) {
+       db, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+
+       assert.NotNil(t, cache)
+       assert.NotNil(t, cache.tableMetaCache)
+       assert.Equal(t, db, cache.db)
+}
+
+func TestTableMetaCache_Init(t *testing.T) {
+       db, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       err = cache.Init(ctx, db)
+       assert.NoError(t, err)
+}
+
+func TestTableMetaCache_Destroy(t *testing.T) {
+       db, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+
+       err = cache.Destroy()
+       assert.NoError(t, err)
+}
+
+func TestTableMetaCache_GetTableMeta_EmptyTableName(t *testing.T) {
+       db, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       tableMeta, err := cache.GetTableMeta(ctx, "test_db", "")
+       assert.Error(t, err)
+       assert.Nil(t, tableMeta)
+       assert.Contains(t, err.Error(), "table name is empty")
+}
+
+func TestTableMetaCache_GetTableMeta_Success(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       mock.ExpectBegin()
+
+       columnRows := sqlmock.NewRows([]string{
+               "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+               "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT", 
"EXTRA",
+       }).
+               AddRow("users", "test_db", "id", "BIGINT", "BIGINT(20)", "PRI", 
"NO", nil, "auto_increment").
+               AddRow("users", "test_db", "name", "VARCHAR", "VARCHAR(100)", 
"", "YES", nil, "")
+
+       mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+               ExpectQuery().
+               WithArgs("test_db", "users").
+               WillReturnRows(columnRows)
+
+       indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME", 
"NON_UNIQUE"}).
+               AddRow("PRIMARY", "id", 0)
+
+       mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+               ExpectQuery().
+               WithArgs("test_db", "users").
+               WillReturnRows(indexRows)
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       tableMeta, err := cache.GetTableMeta(ctx, "test_db", "users")
+
+       if err != nil {
+               assert.Contains(t, err.Error(), "")
+       } else {
+               assert.NotNil(t, tableMeta)
+       }
+}
+
+func TestTableMetaCache_GetTableMeta_DBConnectionError(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       db.Close()
+
+       mock.ExpectClose()
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       tableMeta, err := cache.GetTableMeta(ctx, "test_db", "users")
+       assert.Error(t, err)
+       assert.Nil(t, tableMeta)
+}
+
+func TestTableMetaCache_GetTableMeta_CacheHit(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       columnRows := sqlmock.NewRows([]string{
+               "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+               "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT", 
"EXTRA",
+       }).
+               AddRow("test_table", "test_db", "id", "INT", "INT(11)", "PRI", 
"NO", nil, "auto_increment")
+
+       mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+               ExpectQuery().
+               WithArgs("test_db", "test_table").
+               WillReturnRows(columnRows)
+
+       indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME", 
"NON_UNIQUE"}).
+               AddRow("PRIMARY", "id", 0)
+
+       mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+               ExpectQuery().
+               WithArgs("test_db", "test_table").
+               WillReturnRows(indexRows)
+
+       _, err = cache.GetTableMeta(ctx, "test_db", "test_table")
+
+       if err == nil {
+               t.Log("Successfully tested GetTableMeta code path")
+       }
+}
+
+func TestTableMetaCache_MultipleInstances(t *testing.T) {
+       db1, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db1.Close()
+
+       db2, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db2.Close()
+
+       cfg1 := &mysql.Config{
+               User:   "test1",
+               Passwd: "test1",
+               DBName: "test_db1",
+       }
+
+       cfg2 := &mysql.Config{
+               User:   "test2",
+               Passwd: "test2",
+               DBName: "test_db2",
+       }
+
+       cache1 := NewTableMetaInstance(db1, cfg1)
+       cache2 := NewTableMetaInstance(db2, cfg2)
+
+       assert.NotNil(t, cache1)
+       assert.NotNil(t, cache2)
+       assert.NotEqual(t, cache1, cache2)
+       assert.NotEqual(t, cache1.db, cache2.db)
+}
+
+func TestTableMetaCache_ConcurrentAccess(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       columnRows := sqlmock.NewRows([]string{
+               "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+               "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT", 
"EXTRA",
+       }).
+               AddRow("test_table", "test_db", "id", "INT", "INT(11)", "PRI", 
"NO", nil, "auto_increment")
+
+       indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME", 
"NON_UNIQUE"}).
+               AddRow("PRIMARY", "id", 0)
+
+       for i := 0; i < 3; i++ {
+               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                       ExpectQuery().
+                       WithArgs("test_db", "test_table").
+                       WillReturnRows(columnRows)
+
+               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                       ExpectQuery().
+                       WithArgs("test_db", "test_table").
+                       WillReturnRows(indexRows)
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       done := make(chan bool, 3)
+       for i := 0; i < 3; i++ {
+               go func() {
+                       _, _ = cache.GetTableMeta(ctx, "test_db", "test_table")
+                       done <- true
+               }()
+       }
+
+       for i := 0; i < 3; i++ {
+               <-done
+       }
+
+       assert.NotNil(t, cache)
+}
+
+func TestTableMetaCache_WithNilDB(t *testing.T) {
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(nil, cfg)
+       assert.NotNil(t, cache)
+       assert.Nil(t, cache.db)
+}
+
+func TestTableMetaCache_ContextCancellation(t *testing.T) {
+       db, _, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       cache := NewTableMetaInstance(db, cfg)
+
+       ctx, cancel := context.WithCancel(context.Background())
+       cancel()
+
+       _, err = cache.GetTableMeta(ctx, "test_db", "test_table")
+       assert.Error(t, err)
+}
+
+func TestTableMetaCache_ErrorFromBaseCache(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       cfg := &mysql.Config{
+               User:   "test",
+               Passwd: "test",
+               DBName: "test_db",
+       }
+
+       mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+               ExpectQuery().
+               WithArgs("test_db", "error_table").
+               WillReturnError(errors.New("query error"))
+
+       cache := NewTableMetaInstance(db, cfg)
+       ctx := context.Background()
+
+       _, err = cache.GetTableMeta(ctx, "test_db", "error_table")
+       assert.Error(t, err)
+}
diff --git a/pkg/datasource/sql/datasource/mysql/trigger_test.go 
b/pkg/datasource/sql/datasource/mysql/trigger_test.go
index 4c8bc8bc..16b15399 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger_test.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger_test.go
@@ -20,8 +20,10 @@ package mysql
 import (
        "context"
        "database/sql"
+       "errors"
        "testing"
 
+       "github.com/DATA-DOG/go-sqlmock"
        "github.com/agiledragon/gomonkey/v2"
        "github.com/golang/mock/gomock"
        "github.com/stretchr/testify/assert"
@@ -188,3 +190,537 @@ func Test_mysqlTrigger_LoadAll(t *testing.T) {
                })
        }
 }
+
+func TestNewMysqlTrigger(t *testing.T) {
+       trigger := NewMysqlTrigger()
+       assert.NotNil(t, trigger)
+}
+
+func Test_mysqlTrigger_LoadOne_ErrorCases(t *testing.T) {
+       tests := []struct {
+               name          string
+               columnMetaErr error
+               indexMetaErr  error
+               columnMeta    []types.ColumnMeta
+               indexMeta     []types.IndexMeta
+               expectError   bool
+               errorContains string
+       }{
+               {
+                       name:          "error_getting_columns",
+                       columnMetaErr: errors.New("column query error"),
+                       expectError:   true,
+                       errorContains: "Could not found any columnMeta",
+               },
+               {
+                       name:          "error_getting_indexes",
+                       columnMeta:    initMockColumnMeta(),
+                       indexMetaErr:  errors.New("index query error"),
+                       expectError:   true,
+                       errorContains: "Could not found any index",
+               },
+               {
+                       name:          "no_indexes_found",
+                       columnMeta:    initMockColumnMeta(),
+                       indexMeta:     []types.IndexMeta{},
+                       expectError:   true,
+                       errorContains: "could not found any index",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       m := &mysqlTrigger{}
+
+                       if tt.columnMetaErr != nil {
+                               getColumnMetasStub := 
gomonkey.ApplyPrivateMethod(m, "getColumnMetas",
+                                       func(_ *mysqlTrigger, ctx 
context.Context, dbName string, table string, conn *sql.Conn) 
([]types.ColumnMeta, error) {
+                                               return nil, tt.columnMetaErr
+                                       })
+                               defer getColumnMetasStub.Reset()
+                       } else {
+                               getColumnMetasStub := initGetColumnMetasStub(m, 
tt.columnMeta)
+                               defer getColumnMetasStub.Reset()
+                       }
+
+                       if tt.indexMetaErr != nil {
+                               getIndexesStub := 
gomonkey.ApplyPrivateMethod(m, "getIndexes",
+                                       func(_ *mysqlTrigger, ctx 
context.Context, dbName string, tableName string, conn *sql.Conn) 
([]types.IndexMeta, error) {
+                                               return nil, tt.indexMetaErr
+                                       })
+                               defer getIndexesStub.Reset()
+                       } else {
+                               getIndexesStub := initGetIndexesStub(m, 
tt.indexMeta)
+                               defer getIndexesStub.Reset()
+                       }
+
+                       _, err := m.LoadOne(context.Background(), "testdb", 
"testtable", nil)
+
+                       if tt.expectError {
+                               assert.Error(t, err)
+                               assert.Contains(t, err.Error(), 
tt.errorContains)
+                       } else {
+                               assert.NoError(t, err)
+                       }
+               })
+       }
+}
+
+func Test_mysqlTrigger_LoadOne_ComplexIndexes(t *testing.T) {
+       m := &mysqlTrigger{}
+
+       // Test composite indexes
+       columnMeta := []types.ColumnMeta{
+               {ColumnName: "id"},
+               {ColumnName: "user_id"},
+               {ColumnName: "email"},
+       }
+
+       indexMeta := []types.IndexMeta{
+               {
+                       IType:      types.IndexTypePrimaryKey,
+                       Name:       "PRIMARY",
+                       ColumnName: "id",
+                       Columns:    []types.ColumnMeta{},
+               },
+               {
+                       IType:      types.IndexUnique,
+                       Name:       "idx_unique_email",
+                       ColumnName: "email",
+                       NonUnique:  false,
+                       Columns:    []types.ColumnMeta{},
+               },
+               {
+                       IType:      types.IndexNormal,
+                       Name:       "idx_user",
+                       ColumnName: "user_id",
+                       NonUnique:  true,
+                       Columns:    []types.ColumnMeta{},
+               },
+       }
+
+       getColumnMetasStub := initGetColumnMetasStub(m, columnMeta)
+       defer getColumnMetasStub.Reset()
+
+       getIndexesStub := initGetIndexesStub(m, indexMeta)
+       defer getIndexesStub.Reset()
+
+       tableMeta, err := m.LoadOne(context.Background(), "testdb", 
"testtable", nil)
+
+       assert.NoError(t, err)
+       assert.NotNil(t, tableMeta)
+       assert.Equal(t, 3, len(tableMeta.Columns))
+       assert.Equal(t, 3, len(tableMeta.Indexs))
+}
+
+func Test_mysqlTrigger_getColumnMetas(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       conn, err := db.Conn(context.Background())
+       if err != nil {
+               t.Fatalf("failed to create connection: %v", err)
+       }
+       defer conn.Close()
+
+       tests := []struct {
+               name          string
+               setupMock     func()
+               expectError   bool
+               errorContains string
+               expectedCount int
+       }{
+               {
+                       name: "success_with_multiple_columns",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{
+                                       "TABLE_NAME", "TABLE_SCHEMA", 
"COLUMN_NAME", "DATA_TYPE",
+                                       "COLUMN_TYPE", "COLUMN_KEY", 
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+                               }).
+                                       AddRow("users", "testdb", "id", 
"BIGINT", "BIGINT(20)", "PRI", "NO", nil, "auto_increment").
+                                       AddRow("users", "testdb", "name", 
"VARCHAR", "VARCHAR(100)", "", "YES", []byte("default"), "").
+                                       AddRow("users", "testdb", "age", "INT", 
"INT(11)", "", "NO", []byte("0"), "")
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 3,
+               },
+               {
+                       name: "prepare_error",
+                       setupMock: func() {
+                               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                                       WillReturnError(errors.New("prepare 
failed"))
+                       },
+                       expectError:   true,
+                       errorContains: "prepare failed",
+               },
+               {
+                       name: "query_error",
+                       setupMock: func() {
+                               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnError(errors.New("query 
failed"))
+                       },
+                       expectError:   true,
+                       errorContains: "query failed",
+               },
+               {
+                       name: "no_columns_found",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{
+                                       "TABLE_NAME", "TABLE_SCHEMA", 
"COLUMN_NAME", "DATA_TYPE",
+                                       "COLUMN_TYPE", "COLUMN_KEY", 
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+                               })
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   true,
+                       errorContains: "can't find column",
+               },
+               {
+                       name: "nullable_column",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{
+                                       "TABLE_NAME", "TABLE_SCHEMA", 
"COLUMN_NAME", "DATA_TYPE",
+                                       "COLUMN_TYPE", "COLUMN_KEY", 
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+                               }).
+                                       AddRow("users", "testdb", 
"optional_field", "VARCHAR", "VARCHAR(50)", "", "YES", nil, "")
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
INFORMATION_SCHEMA.COLUMNS").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 1,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tt.setupMock()
+
+                       m := &mysqlTrigger{}
+                       columns, err := m.getColumnMetas(context.Background(), 
"testdb", "users", conn)
+
+                       if tt.expectError {
+                               assert.Error(t, err)
+                               if tt.errorContains != "" {
+                                       assert.Contains(t, err.Error(), 
tt.errorContains)
+                               }
+                       } else {
+                               assert.NoError(t, err)
+                               assert.Equal(t, tt.expectedCount, len(columns))
+
+                               if tt.name == "nullable_column" && len(columns) 
> 0 {
+                                       assert.Equal(t, int8(1), 
columns[0].IsNullable)
+                               }
+                       }
+
+                       // Ensure all expectations were met
+                       if err := mock.ExpectationsWereMet(); err != nil {
+                               t.Errorf("unfulfilled expectations: %v", err)
+                       }
+               })
+       }
+}
+
+func Test_mysqlTrigger_getIndexes(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       conn, err := db.Conn(context.Background())
+       if err != nil {
+               t.Fatalf("failed to create connection: %v", err)
+       }
+       defer conn.Close()
+
+       tests := []struct {
+               name          string
+               setupMock     func()
+               expectError   bool
+               errorContains string
+               expectedCount int
+               validateIndex func(*testing.T, []types.IndexMeta)
+       }{
+               {
+                       name: "success_primary_key",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{"INDEX_NAME", 
"COLUMN_NAME", "NON_UNIQUE"}).
+                                       AddRow("PRIMARY", "id", 0)
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 1,
+                       validateIndex: func(t *testing.T, indexes 
[]types.IndexMeta) {
+                               assert.Equal(t, types.IndexTypePrimaryKey, 
indexes[0].IType)
+                               assert.Equal(t, "PRIMARY", indexes[0].Name)
+                               assert.False(t, indexes[0].NonUnique)
+                       },
+               },
+               {
+                       name: "success_unique_index",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{"INDEX_NAME", 
"COLUMN_NAME", "NON_UNIQUE"}).
+                                       AddRow("idx_email", "email", 0)
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 1,
+                       validateIndex: func(t *testing.T, indexes 
[]types.IndexMeta) {
+                               assert.Equal(t, types.IndexUnique, 
indexes[0].IType)
+                               assert.False(t, indexes[0].NonUnique)
+                       },
+               },
+               {
+                       name: "success_normal_index",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{"INDEX_NAME", 
"COLUMN_NAME", "NON_UNIQUE"}).
+                                       AddRow("idx_name", "name", 1)
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 1,
+                       validateIndex: func(t *testing.T, indexes 
[]types.IndexMeta) {
+                               assert.Equal(t, types.IndexNormal, 
indexes[0].IType)
+                               assert.True(t, indexes[0].NonUnique)
+                       },
+               },
+               {
+                       name: "prepare_error",
+                       setupMock: func() {
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       WillReturnError(errors.New("prepare 
failed"))
+                       },
+                       expectError:   true,
+                       errorContains: "prepare failed",
+               },
+               {
+                       name: "query_error",
+                       setupMock: func() {
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnError(errors.New("query 
failed"))
+                       },
+                       expectError:   true,
+                       errorContains: "query failed",
+               },
+               {
+                       name: "composite_index",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{"INDEX_NAME", 
"COLUMN_NAME", "NON_UNIQUE"}).
+                                       AddRow("idx_composite", "col1", 1).
+                                       AddRow("idx_composite", "col2", 1)
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 2,
+               },
+               {
+                       name: "mixed_case_primary",
+                       setupMock: func() {
+                               rows := sqlmock.NewRows([]string{"INDEX_NAME", 
"COLUMN_NAME", "NON_UNIQUE"}).
+                                       AddRow("Primary", "id", 0)
+
+                               mock.ExpectPrepare("SELECT (.+) FROM 
`INFORMATION_SCHEMA`.`STATISTICS`").
+                                       ExpectQuery().
+                                       WithArgs("testdb", "users").
+                                       WillReturnRows(rows)
+                       },
+                       expectError:   false,
+                       expectedCount: 1,
+                       validateIndex: func(t *testing.T, indexes 
[]types.IndexMeta) {
+                               assert.Equal(t, types.IndexTypePrimaryKey, 
indexes[0].IType)
+                       },
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       tt.setupMock()
+
+                       m := &mysqlTrigger{}
+                       indexes, err := m.getIndexes(context.Background(), 
"testdb", "users", conn)
+
+                       if tt.expectError {
+                               assert.Error(t, err)
+                               if tt.errorContains != "" {
+                                       assert.Contains(t, err.Error(), 
tt.errorContains)
+                               }
+                       } else {
+                               assert.NoError(t, err)
+                               assert.Equal(t, tt.expectedCount, len(indexes))
+
+                               if tt.validateIndex != nil {
+                                       tt.validateIndex(t, indexes)
+                               }
+                       }
+
+                       // Ensure all expectations were met
+                       if err := mock.ExpectationsWereMet(); err != nil {
+                               t.Errorf("unfulfilled expectations: %v", err)
+                       }
+               })
+       }
+}
+
+func Test_mysqlTrigger_LoadAll_ErrorHandling(t *testing.T) {
+       m := &mysqlTrigger{}
+
+       // Test with one successful and one failed table
+       columnMeta := initMockColumnMeta()
+       indexMeta := initMockIndexMeta()
+
+       callCount := 0
+       getColumnMetasStub := gomonkey.ApplyPrivateMethod(m, "getColumnMetas",
+               func(_ *mysqlTrigger, ctx context.Context, dbName string, table 
string, conn *sql.Conn) ([]types.ColumnMeta, error) {
+                       callCount++
+                       if callCount == 2 {
+                               return nil, errors.New("column error")
+                       }
+                       return columnMeta, nil
+               })
+       defer getColumnMetasStub.Reset()
+
+       getIndexesStub := initGetIndexesStub(m, indexMeta)
+       defer getIndexesStub.Reset()
+
+       // LoadAll should continue even if one table fails
+       result, err := m.LoadAll(context.Background(), "testdb", nil, "table1", 
"table2", "table3")
+
+       assert.NoError(t, err)
+       // Should have 2 tables (table1 and table3), table2 failed
+       assert.Equal(t, 2, len(result))
+}
+
+func Test_mysqlTrigger_LoadOne_MultipleIndexesOnSameColumn(t *testing.T) {
+       m := &mysqlTrigger{}
+
+       columnMeta := []types.ColumnMeta{
+               {ColumnName: "id"},
+               {ColumnName: "email"},
+       }
+
+       // Same index appears multiple times (composite index with multiple 
columns)
+       indexMeta := []types.IndexMeta{
+               {
+                       IType:      types.IndexTypePrimaryKey,
+                       Name:       "PRIMARY",
+                       ColumnName: "id",
+                       Columns:    []types.ColumnMeta{},
+               },
+               {
+                       IType:      types.IndexNormal,
+                       Name:       "idx_composite",
+                       ColumnName: "id",
+                       NonUnique:  true,
+                       Columns:    []types.ColumnMeta{},
+               },
+               {
+                       IType:      types.IndexNormal,
+                       Name:       "idx_composite",
+                       ColumnName: "email",
+                       NonUnique:  true,
+                       Columns:    []types.ColumnMeta{},
+               },
+       }
+
+       getColumnMetasStub := initGetColumnMetasStub(m, columnMeta)
+       defer getColumnMetasStub.Reset()
+
+       getIndexesStub := initGetIndexesStub(m, indexMeta)
+       defer getIndexesStub.Reset()
+
+       tableMeta, err := m.LoadOne(context.Background(), "testdb", 
"testtable", nil)
+
+       assert.NoError(t, err)
+       assert.NotNil(t, tableMeta)
+
+       // Should have 2 unique index names (PRIMARY and idx_composite)
+       assert.Equal(t, 2, len(tableMeta.Indexs))
+
+       // The composite index should have 2 columns
+       compositeIdx := tableMeta.Indexs["idx_composite"]
+       assert.Equal(t, 2, len(compositeIdx.Columns))
+}
+
+func Test_mysqlTrigger_getColumnMetas_DataTypes(t *testing.T) {
+       db, mock, err := sqlmock.New()
+       if err != nil {
+               t.Fatalf("failed to open sqlmock database: %v", err)
+       }
+       defer db.Close()
+
+       conn, err := db.Conn(context.Background())
+       if err != nil {
+               t.Fatalf("failed to create connection: %v", err)
+       }
+       defer conn.Close()
+
+       rows := sqlmock.NewRows([]string{
+               "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+               "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT", 
"EXTRA",
+       }).
+               AddRow("test", "testdb", "id", "BIGINT", "BIGINT(20)", "PRI", 
"NO", nil, "auto_increment").
+               AddRow("test", "testdb", "name", "VARCHAR", "VARCHAR(255)", "", 
"YES", []byte("''"), "").
+               AddRow("test", "testdb", "created_at", "DATETIME", "DATETIME", 
"", "NO", []byte("CURRENT_TIMESTAMP"), "on update CURRENT_TIMESTAMP").
+               AddRow("test", "testdb", "price", "DECIMAL", "DECIMAL(10,2)", 
"", "YES", nil, "")
+
+       mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+               ExpectQuery().
+               WithArgs("testdb", "test").
+               WillReturnRows(rows)
+
+       m := &mysqlTrigger{}
+       columns, err := m.getColumnMetas(context.Background(), "testdb", 
"test", conn)
+
+       assert.NoError(t, err)
+       assert.Equal(t, 4, len(columns))
+
+       // Verify auto_increment
+       assert.True(t, columns[0].Autoincrement)
+       assert.False(t, columns[1].Autoincrement)
+
+       // Verify nullable
+       assert.Equal(t, int8(0), columns[0].IsNullable)
+       assert.Equal(t, int8(1), columns[1].IsNullable)
+
+       // Verify column defaults
+       assert.Nil(t, columns[0].ColumnDef)
+       assert.Equal(t, []byte("''"), columns[1].ColumnDef)
+
+       if err := mock.ExpectationsWereMet(); err != nil {
+               t.Errorf("unfulfilled expectations: %v", err)
+       }
+}
diff --git a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go 
b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
index c7c397a9..11891587 100644
--- a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
+++ b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
@@ -831,8 +831,6 @@ func TestDestroyLogCleanChannel_OnlyOnce(t *testing.T) {
 }
 
 func TestTraversalCleanChannel(t *testing.T) {
-       log.Init()
-
        db, mock, err := sqlmock.New()
        assert.NoError(t, err)
        defer db.Close()
@@ -854,8 +852,15 @@ func TestTraversalCleanChannel(t *testing.T) {
                logQueue:    make(chan *model.FenceLogIdentity, maxQueueSize),
        }
 
+       // Use WaitGroup to ensure goroutine completes
+       var wg sync.WaitGroup
+       wg.Add(1)
+
        // Start the goroutine
-       go handler.traversalCleanChannel(db)
+       go func() {
+               defer wg.Done()
+               handler.traversalCleanChannel(db)
+       }()
 
        // Push exactly channelDelete items to trigger batch delete
        for i := 0; i < channelDelete; i++ {
@@ -871,16 +876,14 @@ func TestTraversalCleanChannel(t *testing.T) {
        // Close the channel to stop the goroutine
        close(handler.logQueue)
 
-       // Give it time to finish
-       time.Sleep(100 * time.Millisecond)
+       // Wait for goroutine to finish
+       wg.Wait()
 
        // Verify expectations
        assert.NoError(t, mock.ExpectationsWereMet())
 }
 
 func TestTraversalCleanChannel_RemainingBatch(t *testing.T) {
-       log.Init()
-
        db, mock, err := sqlmock.New()
        assert.NoError(t, err)
        defer db.Close()
@@ -902,8 +905,15 @@ func TestTraversalCleanChannel_RemainingBatch(t 
*testing.T) {
                logQueue:    make(chan *model.FenceLogIdentity, maxQueueSize),
        }
 
+       // Use WaitGroup to ensure goroutine completes
+       var wg sync.WaitGroup
+       wg.Add(1)
+
        // Start the goroutine
-       go handler.traversalCleanChannel(db)
+       go func() {
+               defer wg.Done()
+               handler.traversalCleanChannel(db)
+       }()
 
        // Push less than channelDelete items
        for i := 0; i < 3; i++ {
@@ -919,16 +929,14 @@ func TestTraversalCleanChannel_RemainingBatch(t 
*testing.T) {
        // Close the channel to stop the goroutine and trigger final batch
        close(handler.logQueue)
 
-       // Give it time to finish
-       time.Sleep(100 * time.Millisecond)
+       // Wait for goroutine to finish
+       wg.Wait()
 
        // Verify expectations
        assert.NoError(t, mock.ExpectationsWereMet())
 }
 
 func TestTraversalCleanChannel_DeleteError(t *testing.T) {
-       log.Init()
-
        db, mock, err := sqlmock.New()
        assert.NoError(t, err)
        defer db.Close()
@@ -947,8 +955,15 @@ func TestTraversalCleanChannel_DeleteError(t *testing.T) {
                logQueue:    make(chan *model.FenceLogIdentity, maxQueueSize),
        }
 
+       // Use WaitGroup to ensure goroutine completes
+       var wg sync.WaitGroup
+       wg.Add(1)
+
        // Start the goroutine
-       go handler.traversalCleanChannel(db)
+       go func() {
+               defer wg.Done()
+               handler.traversalCleanChannel(db)
+       }()
 
        // Push channelDelete items to trigger batch delete
        for i := 0; i < channelDelete; i++ {
@@ -964,13 +979,11 @@ func TestTraversalCleanChannel_DeleteError(t *testing.T) {
        // Close the channel
        close(handler.logQueue)
 
-       // Give it time to finish
-       time.Sleep(100 * time.Millisecond)
+       // Wait for goroutine to finish
+       wg.Wait()
 }
 
 func TestInitLogCleanChannel(t *testing.T) {
-       log.Init()
-
        // Create a test DSN for sqlmock
        db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
        assert.NoError(t, err)
@@ -1001,6 +1014,9 @@ func TestInitLogCleanChannel(t *testing.T) {
 }
 
 func TestInitLogCleanChannel_EmptyDSN(t *testing.T) {
+       // Wait a bit to ensure previous tests' goroutines have finished
+       time.Sleep(300 * time.Millisecond)
+
        log.Init()
 
        handler := &tccFenceWrapperHandler{


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to