Repository: apache/incubator-seata-go (go)

Issue:
feat: Implement automatic refresh functionality for TableMetaCache.
<!-- Please only use this template for submitting enhancement requests -->

**What would you like to be added**:
![image](https://github.com/user-attachments/assets/202be702-7db7-4405-8a0d-18eeba44062f)
![image](https://github.com/user-attachments/assets/63056310-1aee-448b-b462-6f9bc9912271)

As shown in the image above, since loadAll always returns an empty struct, TableMetaCache currently cannot refresh automatically.

**Why is this needed**:
Align with Seata-Java.

Pull request:
feat: Implement automatic refresh functionality for TableMetaCache
<!--  Thanks for sending a pull request! 
-->

**What this PR does**:

 Implement automatic refresh functionality for TableMetaCache

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
-->
Fixes #720

**Special notes for your reviewer**:

**Does this PR introduce a user-facing change?**:
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->
```release-note

```

Patch:
diff --git a/pkg/datasource/sql/datasource/base/meta_cache.go b/pkg/datasource/sql/datasource/base/meta_cache.go
index 859157e..c4694d8 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache.go
@@ -24,6 +24,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/go-sql-driver/mysql"
+
 	"seata.apache.org/seata-go/pkg/datasource/sql/types"
 )
 
@@ -32,7 +34,7 @@ type (
 	trigger interface {
 		LoadOne(ctx context.Context, dbName string, table string, conn *sql.Conn) (*types.TableMeta, error)
 
-		LoadAll() ([]types.TableMeta, error)
+		LoadAll(ctx context.Context, dbName string, conn *sql.Conn, tables ...string) ([]types.TableMeta, error)
 	}
 
 	entry struct {
@@ -50,10 +52,12 @@ type BaseTableMetaCache struct {
 	cache          map[string]*entry
 	cancel         context.CancelFunc
 	trigger        trigger
+	db             *sql.DB
+	cfg            *mysql.Config
 }
 
 // NewBaseCache
-func NewBaseCache(capity int32, expireDuration time.Duration, trigger trigger) *BaseTableMetaCache {
+func NewBaseCache(capity int32, expireDuration time.Duration, trigger trigger, db *sql.DB, cfg *mysql.Config) *BaseTableMetaCache {
 	ctx, cancel := context.WithCancel(context.Background())
 
 	c := &BaseTableMetaCache{
@@ -64,6 +68,8 @@ func NewBaseCache(capity int32, expireDuration time.Duration, trigger trigger) *
 		cache:          map[string]*entry{},
 		cancel:         cancel,
 		trigger:        trigger,
+		cfg:            cfg,
+		db:             db,
 	}
 
 	c.Init(ctx)
@@ -82,7 +88,19 @@ func (c *BaseTableMetaCache) Init(ctx context.Context) error {
 // refresh
 func (c *BaseTableMetaCache) refresh(ctx context.Context) {
 	f := func() {
-		v, err := c.trigger.LoadAll()
+		if c.db == nil || c.cfg == nil || c.cache == nil || len(c.cache) == 0 {
+			return
+		}
+
+		tables := make([]string, 0, len(c.cache))
+		for table := range c.cache {
+			tables = append(tables, table)
+		}
+		conn, err := c.db.Conn(ctx)
+		if err != nil {
+			return
+		}
+		v, err := c.trigger.LoadAll(ctx, c.cfg.DBName, conn, tables...)
 		if err != nil {
 			return
 		}
@@ -92,7 +110,7 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) {
 
 		for i := range v {
 			tm := v[i]
-			if _, ok := c.cache[tm.TableName]; !ok {
+			if _, ok := c.cache[tm.TableName]; ok {
 				c.cache[tm.TableName] = &entry{
 					value: tm,
 				}
diff --git a/pkg/datasource/sql/datasource/mysql/meta_cache.go b/pkg/datasource/sql/datasource/mysql/meta_cache.go
index 82ac0fb..2b0a45e 100644
--- a/pkg/datasource/sql/datasource/mysql/meta_cache.go
+++ b/pkg/datasource/sql/datasource/mysql/meta_cache.go
@@ -24,6 +24,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/go-sql-driver/mysql"
+
 	"seata.apache.org/seata-go/pkg/datasource/sql/datasource/base"
 	"seata.apache.org/seata-go/pkg/datasource/sql/types"
 )
@@ -39,9 +41,9 @@ type TableMetaCache struct {
 	db             *sql.DB
 }
 
-func NewTableMetaInstance(db *sql.DB) *TableMetaCache {
+func NewTableMetaInstance(db *sql.DB, cfg *mysql.Config) *TableMetaCache {
 	tableMetaInstance := &TableMetaCache{
-		tableMetaCache: base.NewBaseCache(capacity, EexpireTime, NewMysqlTrigger()),
+		tableMetaCache: base.NewBaseCache(capacity, EexpireTime, NewMysqlTrigger(), db, cfg),
 		db:             db,
 	}
 	return tableMetaInstance
diff --git a/pkg/datasource/sql/datasource/mysql/trigger.go b/pkg/datasource/sql/datasource/mysql/trigger.go
index 689ad2c..adbbb2f 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger.go
@@ -79,8 +79,16 @@ func (m *mysqlTrigger) LoadOne(ctx context.Context, dbName string, tableName str
 }
 
 // LoadAll
-func (m *mysqlTrigger) LoadAll() ([]types.TableMeta, error) {
-	return []types.TableMeta{}, nil
+func (m *mysqlTrigger) LoadAll(ctx context.Context, dbName string, conn *sql.Conn, tables ...string) ([]types.TableMeta, error) {
+	var tableMetas []types.TableMeta
+	for _, tableName := range tables {
+		tableMeta, err := m.LoadOne(ctx, dbName, tableName, conn)
+		if err != nil {
+			continue
+		}
+		tableMetas = append(tableMetas, *tableMeta)
+	}
+	return tableMetas, nil
 }
 
 // getColumnMetas get tableMeta column
diff --git a/pkg/datasource/sql/driver.go b/pkg/datasource/sql/driver.go
index a4b5111..905d81d 100644
--- a/pkg/datasource/sql/driver.go
+++ b/pkg/datasource/sql/driver.go
@@ -155,7 +155,7 @@ func (d *seataDriver) getOpenConnectorProxy(connector driver.Connector, dbType t
 		log.Errorf("create new resource: %w", err)
 		return nil, err
 	}
-	datasource.RegisterTableCache(types.DBTypeMySQL, mysql2.NewTableMetaInstance(db))
+	datasource.RegisterTableCache(types.DBTypeMySQL, mysql2.NewTableMetaInstance(db, cfg))
 	if err = datasource.GetDataSourceManager(d.branchType).RegisterResource(res); err != nil {
 		log.Errorf("regisiter resource: %w", err)
 		return nil, err


Test patch:
diff --git a/pkg/datasource/sql/datasource/base/meta_cache_test.go b/pkg/datasource/sql/datasource/base/meta_cache_test.go
new file mode 100644
index 0000000..570674e
--- /dev/null
+++ b/pkg/datasource/sql/datasource/base/meta_cache_test.go
@@ -0,0 +1,126 @@
+/*
+ * 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 base
+
+import (
+	"context"
+	"database/sql"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/agiledragon/gomonkey/v2"
+	"github.com/go-sql-driver/mysql"
+	"github.com/stretchr/testify/assert"
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/testdata"
+)
+
+var (
+	capacity      int32 = 1024
+	EexpireTime         = 15 * time.Minute
+	tableMetaOnce sync.Once
+)
+
+type mockTrigger struct {
+}
+
+func (m *mockTrigger) LoadOne(ctx context.Context, dbName string, table string, conn *sql.Conn) (*types.TableMeta, error) {
+	return nil, nil
+}
+
+func (m *mockTrigger) LoadAll(ctx context.Context, dbName string, conn *sql.Conn, tables ...string) ([]types.TableMeta, error) {
+	return nil, nil
+}
+
+func TestBaseTableMetaCache_refresh(t *testing.T) {
+	type fields struct {
+		lock           sync.RWMutex
+		expireDuration time.Duration
+		capity         int32
+		size           int32
+		cache          map[string]*entry
+		cancel         context.CancelFunc
+		trigger        trigger
+		db             *sql.DB
+		cfg            *mysql.Config
+	}
+	type args struct {
+		ctx context.Context
+	}
+	ctx, cancel := context.WithCancel(context.Background())
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   types.TableMeta
+	}{
+		{name: "test-1",
+			fields: fields{
+				lock:           sync.RWMutex{},
+				capity:         capacity,
+				size:           0,
+				expireDuration: EexpireTime,
+				cache: map[string]*entry{
+					"test": {
+						value:      types.TableMeta{},
+						lastAccess: time.Now(),
+					},
+				},
+				cancel:  cancel,
+				trigger: &mockTrigger{},
+				cfg:     &mysql.Config{},
+				db:      &sql.DB{},
+			}, args: args{ctx: ctx},
+			want: testdata.MockWantTypesMeta("test")},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			connStub := gomonkey.ApplyMethodFunc(tt.fields.db, "Conn",
+				func(_ context.Context) (*sql.Conn, error) {
+					return &sql.Conn{}, nil
+				})
+
+			defer connStub.Reset()
+
+			loadAllStub := gomonkey.ApplyMethodFunc(tt.fields.trigger, "LoadAll",
+				func(_ context.Context, _ string, _ *sql.Conn, _ ...string) ([]types.TableMeta, error) {
+					return []types.TableMeta{tt.want}, nil
+				})
+
+			defer loadAllStub.Reset()
+
+			c := &BaseTableMetaCache{
+				lock:           tt.fields.lock,
+				expireDuration: tt.fields.expireDuration,
+				capity:         tt.fields.capity,
+				size:           tt.fields.size,
+				cache:          tt.fields.cache,
+				cancel:         tt.fields.cancel,
+				trigger:        tt.fields.trigger,
+				db:             tt.fields.db,
+				cfg:            tt.fields.cfg,
+			}
+			go c.refresh(tt.args.ctx)
+			time.Sleep(time.Second * 3)
+
+			assert.Equal(t, c.cache["test"].value, tt.want)
+		})
+	}
+}
diff --git a/pkg/datasource/sql/datasource/mysql/trigger_test.go b/pkg/datasource/sql/datasource/mysql/trigger_test.go
new file mode 100644
index 0000000..4c8bc8b
--- /dev/null
+++ b/pkg/datasource/sql/datasource/mysql/trigger_test.go
@@ -0,0 +1,190 @@
+/*
+ * 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"
+	"database/sql"
+	"testing"
+
+	"github.com/agiledragon/gomonkey/v2"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+	"seata.apache.org/seata-go/testdata"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/mock"
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/protocol/branch"
+	"seata.apache.org/seata-go/pkg/rm"
+)
+
+func initMockIndexMeta() []types.IndexMeta {
+	return []types.IndexMeta{
+		{
+			IType:      types.IndexTypePrimaryKey,
+			ColumnName: "id",
+			Columns: []types.ColumnMeta{
+				{
+					ColumnName:   "id",
+					DatabaseType: types.GetSqlDataType("BIGINT"),
+				},
+			},
+		},
+	}
+}
+
+func initMockColumnMeta() []types.ColumnMeta {
+	return []types.ColumnMeta{
+		{
+			ColumnName: "id",
+		},
+		{
+			ColumnName: "name",
+		},
+	}
+}
+
+func initGetIndexesStub(m *mysqlTrigger, indexMeta []types.IndexMeta) *gomonkey.Patches {
+	getIndexesStub := gomonkey.ApplyPrivateMethod(m, "getIndexes",
+		func(_ *mysqlTrigger, ctx context.Context, dbName string, tableName string, conn *sql.Conn) ([]types.IndexMeta, error) {
+			return indexMeta, nil
+		})
+	return getIndexesStub
+}
+
+func initGetColumnMetasStub(m *mysqlTrigger, columnMeta []types.ColumnMeta) *gomonkey.Patches {
+	getColumnMetasStub := gomonkey.ApplyPrivateMethod(m, "getColumnMetas",
+		func(_ *mysqlTrigger, ctx context.Context, dbName string, table string, conn *sql.Conn) ([]types.ColumnMeta, error) {
+			return columnMeta, nil
+		})
+	return getColumnMetasStub
+}
+
+func Test_mysqlTrigger_LoadOne(t *testing.T) {
+	wantTableMeta := testdata.MockWantTypesMeta("test")
+	type args struct {
+		ctx       context.Context
+		dbName    string
+		tableName string
+		conn      *sql.Conn
+	}
+	tests := []struct {
+		name          string
+		args          args
+		columnMeta    []types.ColumnMeta
+		indexMeta     []types.IndexMeta
+		wantTableMeta *types.TableMeta
+	}{
+		{
+			name:          "1",
+			args:          args{ctx: context.Background(), dbName: "dbName", tableName: "test", conn: nil},
+			indexMeta:     initMockIndexMeta(),
+			columnMeta:    initMockColumnMeta(),
+			wantTableMeta: &wantTableMeta,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m := &mysqlTrigger{}
+
+			getColumnMetasStub := initGetColumnMetasStub(m, tt.columnMeta)
+			defer getColumnMetasStub.Reset()
+
+			getIndexesStub := initGetIndexesStub(m, tt.indexMeta)
+			defer getIndexesStub.Reset()
+
+			got, err := m.LoadOne(tt.args.ctx, tt.args.dbName, tt.args.tableName, tt.args.conn)
+			if err != nil {
+				t.Errorf("LoadOne() error = %v", err)
+				return
+			}
+
+			assert.Equal(t, tt.wantTableMeta, got)
+		})
+	}
+}
+
+func initMockResourceManager(branchType branch.BranchType, ctrl *gomock.Controller) *mock.MockDataSourceManager {
+	mockResourceMgr := mock.NewMockDataSourceManager(ctrl)
+	mockResourceMgr.SetBranchType(branchType)
+	mockResourceMgr.EXPECT().BranchRegister(gomock.Any(), gomock.Any()).AnyTimes().Return(int64(0), nil)
+	rm.GetRmCacheInstance().RegisterResourceManager(mockResourceMgr)
+	mockResourceMgr.EXPECT().RegisterResource(gomock.Any()).AnyTimes().Return(nil)
+	mockResourceMgr.EXPECT().CreateTableMetaCache(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(nil, nil)
+	return mockResourceMgr
+}
+
+func Test_mysqlTrigger_LoadAll(t *testing.T) {
+	sql.Register("seata-at-mysql", &mock.MockTestDriver{})
+
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	mockMgr := initMockResourceManager(branch.BranchTypeAT, ctrl)
+	_ = mockMgr
+
+	conn := sql.Conn{}
+	type args struct {
+		ctx    context.Context
+		dbName string
+		conn   *sql.Conn
+		tables []string
+	}
+	tests := []struct {
+		name       string
+		args       args
+		columnMeta []types.ColumnMeta
+		indexMeta  []types.IndexMeta
+		want       []types.TableMeta
+	}{
+		{
+			name: "test-01",
+			args: args{
+				ctx:    nil,
+				dbName: "dbName",
+				conn:   &conn,
+				tables: []string{
+					"test_01",
+					"test_02",
+				},
+			},
+			indexMeta:  initMockIndexMeta(),
+			columnMeta: initMockColumnMeta(),
+			want:       []types.TableMeta{testdata.MockWantTypesMeta("test_01"), testdata.MockWantTypesMeta("test_02")},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			m := &mysqlTrigger{}
+
+			getColumnMetasStub := initGetColumnMetasStub(m, tt.columnMeta)
+			defer getColumnMetasStub.Reset()
+
+			getIndexesStub := initGetIndexesStub(m, tt.indexMeta)
+			defer getIndexesStub.Reset()
+
+			got, err := m.LoadAll(tt.args.ctx, tt.args.dbName, tt.args.conn, tt.args.tables...)
+			if err != nil {
+				t.Errorf("LoadAll() error = %v", err)
+				return
+			}
+
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}
diff --git a/pkg/datasource/sql/exec/at/insert_executor_test.go b/pkg/datasource/sql/exec/at/insert_executor_test.go
index 742249b..ecf3739 100644
--- a/pkg/datasource/sql/exec/at/insert_executor_test.go
+++ b/pkg/datasource/sql/exec/at/insert_executor_test.go
@@ -114,7 +114,7 @@ func TestBuildSelectSQLByInsert(t *testing.T) {
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 			stub := gomonkey.ApplyMethod(reflect.TypeOf(datasource.GetTableCache(types.DBTypeMySQL)), "GetTableMeta",
 				func(_ *mysql.TableMetaCache, ctx context.Context, dbName, tableName string) (*types.TableMeta, error) {
 					return &test.metaData, nil
@@ -629,7 +629,7 @@ func TestMySQLInsertUndoLogBuilder_getPkValuesByColumn(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 			stub := gomonkey.ApplyMethod(reflect.TypeOf(datasource.GetTableCache(types.DBTypeMySQL)), "GetTableMeta",
 				func(_ *mysql.TableMetaCache, ctx context.Context, dbName, tableName string) (*types.TableMeta, error) {
 					return &tt.args.meta, nil
@@ -731,7 +731,7 @@ func TestMySQLInsertUndoLogBuilder_getPkValuesByAuto(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 			stub := gomonkey.ApplyMethod(reflect.TypeOf(datasource.GetTableCache(types.DBTypeMySQL)), "GetTableMeta",
 				func(_ *mysql.TableMetaCache, ctx context.Context, dbName, tableName string) (*types.TableMeta, error) {
 					return &tt.args.meta, nil
@@ -824,7 +824,7 @@ func TestMySQLInsertUndoLogBuilder_autoGeneratePks(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+			datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 			stub := gomonkey.ApplyMethod(reflect.TypeOf(datasource.GetTableCache(types.DBTypeMySQL)), "GetTableMeta",
 				func(_ *mysql.TableMetaCache, ctx context.Context, dbName, tableName string) (*types.TableMeta, error) {
 					return &tt.args.meta, nil
diff --git a/pkg/datasource/sql/exec/at/multi_update_excutor_test.go b/pkg/datasource/sql/exec/at/multi_update_excutor_test.go
index 9207346..21ef2e3 100644
--- a/pkg/datasource/sql/exec/at/multi_update_excutor_test.go
+++ b/pkg/datasource/sql/exec/at/multi_update_excutor_test.go
@@ -34,7 +34,7 @@ import (
 
 func TestBuildSelectSQLByMultiUpdate(t *testing.T) {
 	undo.InitUndoConfig(undo.Config{OnlyCareUpdateColumns: true})
-	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 
 	tests := []struct {
 		name            string
@@ -101,7 +101,7 @@ func TestBuildSelectSQLByMultiUpdate(t *testing.T) {
 
 func TestBuildSelectSQLByMultiUpdateAllColumns(t *testing.T) {
 	undo.InitUndoConfig(undo.Config{OnlyCareUpdateColumns: false})
-	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 
 	tests := []struct {
 		name            string
diff --git a/pkg/datasource/sql/exec/at/update_executor_test.go b/pkg/datasource/sql/exec/at/update_executor_test.go
index 770f216..a6ffc9b 100644
--- a/pkg/datasource/sql/exec/at/update_executor_test.go
+++ b/pkg/datasource/sql/exec/at/update_executor_test.go
@@ -38,7 +38,7 @@ import (
 
 func TestBuildSelectSQLByUpdate(t *testing.T) {
 	undo.InitUndoConfig(undo.Config{OnlyCareUpdateColumns: true})
-	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil))
+	datasource.RegisterTableCache(types.DBTypeMySQL, mysql.NewTableMetaInstance(nil, nil))
 	stub := gomonkey.ApplyMethod(reflect.TypeOf(datasource.GetTableCache(types.DBTypeMySQL)), "GetTableMeta",
 		func(_ *mysql.TableMetaCache, ctx context.Context, dbName, tableName string) (*types.TableMeta, error) {
 			return &types.TableMeta{
diff --git a/testdata/meta_cache.go b/testdata/meta_cache.go
new file mode 100644
index 0000000..38c2e64
--- /dev/null
+++ b/testdata/meta_cache.go
@@ -0,0 +1,53 @@
+/*
+ * 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 testdata
+
+import "seata.apache.org/seata-go/pkg/datasource/sql/types"
+
+func MockWantTypesMeta(tableName string) types.TableMeta {
+	return types.TableMeta{
+		TableName: tableName,
+		Columns: map[string]types.ColumnMeta{
+			"id": {
+				ColumnName: "id",
+			},
+			"name": {
+				ColumnName: "name",
+			},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"": {
+				ColumnName: "id",
+				IType:      types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{
+						ColumnName:   "id",
+						DatabaseType: types.GetSqlDataType("BIGINT"),
+					},
+					{
+						ColumnName: "id",
+					},
+				},
+			},
+		},
+		ColumnNames: []string{
+			"id",
+			"name",
+		},
+	}
+}