You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by ji...@apache.org on 2025/11/17 02:00:54 UTC

(incubator-seata-go) branch master updated: test: Improve test coverage (#983)

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 34dfa822 test:  Improve test coverage  (#983)
34dfa822 is described below

commit 34dfa8221611f7489e0403abc6b88e4275749739
Author: Eric Wang <iw...@gmail.com>
AuthorDate: Mon Nov 17 05:00:50 2025 +0300

    test:  Improve test coverage  (#983)
---
 pkg/datasource/sql/undo/executor/executor_test.go  | 353 ++++++++++++++++++++
 .../executor/mysql_undo_delete_executor_test.go    | 145 +++++++++
 .../executor/mysql_undo_executor_holder_test.go    | 130 ++++++++
 .../executor/mysql_undo_insert_executor_test.go    | 130 ++++++++
 .../executor/mysql_undo_update_executor_test.go    | 159 +++++++++
 pkg/datasource/sql/undo/executor/sql_test.go       | 260 +++++++++++++++
 pkg/datasource/sql/undo/executor/utils_test.go     | 360 +++++++++++++++++++++
 pkg/datasource/sql/undo/mysql/default_test.go      |  62 ++++
 pkg/datasource/sql/undo/mysql/undo_test.go         | 337 +++++++++++++++++++
 .../sql/undo/parser/parser_cache_test.go           |  32 ++
 pkg/datasource/sql/undo/parser/parser_json_test.go |  40 +++
 pkg/datasource/sql/undo/parser/parser_protobuf.go  |   7 +
 .../sql/undo/parser/parser_protobuf_test.go        |  32 ++
 pkg/tm/transaction_executor_test.go                |   1 +
 14 files changed, 2048 insertions(+)

diff --git a/pkg/datasource/sql/undo/executor/executor_test.go b/pkg/datasource/sql/undo/executor/executor_test.go
index be1064a1..e3c36b87 100644
--- a/pkg/datasource/sql/undo/executor/executor_test.go
+++ b/pkg/datasource/sql/undo/executor/executor_test.go
@@ -21,8 +21,10 @@ import (
 	"context"
 	"database/sql"
 	"encoding/json"
+	"fmt"
 	"testing"
 
+	"github.com/DATA-DOG/go-sqlmock"
 	"github.com/agiledragon/gomonkey/v2"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
@@ -256,3 +258,354 @@ func TestDataValidationAndGoOn(t *testing.T) {
 		})
 	}
 }
+
+func TestDataValidationAndGoOnDisabledValidation(t *testing.T) {
+	cfgPatch := gomonkey.ApplyGlobalVar(&undo.UndoConfig, undo.Config{DataValidation: false})
+	defer cfgPatch.Reset()
+
+	executor := &testableBaseExecutor{
+		BaseExecutor: BaseExecutor{
+			sqlUndoLog: undo.SQLUndoLog{
+				BeforeImage: &types.RecordImage{},
+				AfterImage:  &types.RecordImage{},
+			},
+		},
+	}
+
+	got, err := executor.dataValidationAndGoOn(context.Background(), nil)
+
+	assert.True(t, got)
+	assert.NoError(t, err)
+}
+
+func TestDataValidationAndGoOnIsRecordsEqualsError(t *testing.T) {
+	t.Skip("Skipping test that requires gomonkey function patching which doesn't work well with coverage mode")
+}
+
+func TestQueryCurrentRecordsNilUndoImage(t *testing.T) {
+	executor := &BaseExecutor{
+		undoImage: nil,
+	}
+
+	result, err := executor.queryCurrentRecords(context.Background(), nil)
+
+	assert.Nil(t, result)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "undo image is nil")
+}
+
+func TestQueryCurrentRecordsEmptyPKValues(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	executor := &BaseExecutor{
+		undoImage: &types.RecordImage{
+			TableName: "t_user",
+			TableMeta: &tableMeta,
+			Rows:      []types.RowImage{},
+		},
+	}
+
+	result, err := executor.queryCurrentRecords(context.Background(), nil)
+
+	assert.Nil(t, result)
+	assert.NoError(t, err)
+}
+
+func TestQueryCurrentRecordsSuccess(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	assert.NoError(t, err)
+	defer db.Close()
+
+	conn, err := db.Conn(context.Background())
+	assert.NoError(t, err)
+	defer conn.Close()
+
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id":   {ColumnName: "id"},
+			"name": {ColumnName: "name"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	executor := &BaseExecutor{
+		undoImage: &types.RecordImage{
+			TableName: "t_user",
+			TableMeta: &tableMeta,
+			Rows: []types.RowImage{
+				{Columns: []types.ColumnImage{
+					{ColumnName: "id", Value: 1},
+					{ColumnName: "name", Value: "test"},
+				}},
+			},
+		},
+	}
+
+	rows := sqlmock.NewRows([]string{"id", "name"}).
+		AddRow(1, "test_updated")
+
+	mock.ExpectQuery("SELECT \\* FROM t_user WHERE").
+		WithArgs(1).
+		WillReturnRows(rows)
+
+	result, err := executor.queryCurrentRecords(context.Background(), conn)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, "t_user", result.TableName)
+	assert.Len(t, result.Rows, 1)
+	assert.Len(t, result.Rows[0].Columns, 2)
+	assert.Equal(t, "id", result.Rows[0].Columns[0].ColumnName)
+	assert.NotNil(t, result.Rows[0].Columns[0].Value)
+	assert.Equal(t, "name", result.Rows[0].Columns[1].ColumnName)
+	assert.NotNil(t, result.Rows[0].Columns[1].Value)
+
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestQueryCurrentRecordsQueryError(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	assert.NoError(t, err)
+	defer db.Close()
+
+	conn, err := db.Conn(context.Background())
+	assert.NoError(t, err)
+	defer conn.Close()
+
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	executor := &BaseExecutor{
+		undoImage: &types.RecordImage{
+			TableName: "t_user",
+			TableMeta: &tableMeta,
+			Rows: []types.RowImage{
+				{Columns: []types.ColumnImage{
+					{ColumnName: "id", Value: 1},
+				}},
+			},
+		},
+	}
+
+	mock.ExpectQuery("SELECT \\* FROM t_user WHERE").
+		WithArgs(1).
+		WillReturnError(fmt.Errorf("database connection error"))
+
+	result, err := executor.queryCurrentRecords(context.Background(), conn)
+
+	assert.Nil(t, result)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "database connection error")
+
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestQueryCurrentRecordsCompositePrimaryKey(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	assert.NoError(t, err)
+	defer db.Close()
+
+	conn, err := db.Conn(context.Background())
+	assert.NoError(t, err)
+	defer conn.Close()
+
+	tableMeta := types.TableMeta{
+		TableName: "t_order",
+		Columns: map[string]types.ColumnMeta{
+			"order_id": {ColumnName: "order_id"},
+			"user_id":  {ColumnName: "user_id"},
+			"amount":   {ColumnName: "amount"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "order_id"},
+					{ColumnName: "user_id"},
+				},
+			},
+		},
+	}
+
+	executor := &BaseExecutor{
+		undoImage: &types.RecordImage{
+			TableName: "t_order",
+			TableMeta: &tableMeta,
+			Rows: []types.RowImage{
+				{Columns: []types.ColumnImage{
+					{ColumnName: "order_id", Value: 100},
+					{ColumnName: "user_id", Value: 1},
+					{ColumnName: "amount", Value: 99.99},
+				}},
+			},
+		},
+	}
+
+	rows := sqlmock.NewRows([]string{"order_id", "user_id", "amount"}).
+		AddRow(100, 1, 199.99)
+
+	mock.ExpectQuery("SELECT \\* FROM t_order WHERE").
+		WillReturnRows(rows)
+
+	result, err := executor.queryCurrentRecords(context.Background(), conn)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, "t_order", result.TableName)
+	assert.Len(t, result.Rows, 1)
+	assert.Len(t, result.Rows[0].Columns, 3)
+
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestParsePkValuesSinglePK(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1},
+			{ColumnName: "name", Value: "test1"},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Len(t, result, 1)
+	assert.Contains(t, result, "id")
+	assert.Len(t, result["id"], 1)
+	assert.Equal(t, 1, result["id"][0].Value)
+}
+
+func TestParsePkValuesMultipleRows(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1},
+			{ColumnName: "name", Value: "test1"},
+		}},
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 2},
+			{ColumnName: "name", Value: "test2"},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Contains(t, result, "id")
+}
+
+func TestParsePkValuesCompositePK(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "order_id", Value: 100},
+			{ColumnName: "user_id", Value: 1},
+			{ColumnName: "amount", Value: 99.99},
+		}},
+	}
+
+	pkNameList := []string{"order_id", "user_id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Len(t, result, 2)
+	assert.Contains(t, result, "order_id")
+	assert.Contains(t, result, "user_id")
+	assert.Len(t, result["order_id"], 1)
+	assert.Len(t, result["user_id"], 1)
+	assert.Equal(t, 100, result["order_id"][0].Value)
+	assert.Equal(t, 1, result["user_id"][0].Value)
+}
+
+func TestParsePkValuesCaseInsensitive(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "ID", Value: 1},
+			{ColumnName: "Name", Value: "test"},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Len(t, result, 1)
+	assert.Contains(t, result, "id")
+	assert.Len(t, result["id"], 1)
+	assert.Equal(t, 1, result["id"][0].Value)
+}
+
+func TestParsePkValuesEmptyRows(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{}
+	pkNameList := []string{"id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Len(t, result, 0)
+}
+
+func TestParsePkValuesNoMatchingPK(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "name", Value: "test"},
+			{ColumnName: "age", Value: 25},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+
+	result := executor.parsePkValues(rows, pkNameList)
+
+	assert.NotNil(t, result)
+	assert.Len(t, result, 0)
+}
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor_test.go b/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor_test.go
new file mode 100644
index 00000000..7ebec5ce
--- /dev/null
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor_test.go
@@ -0,0 +1,145 @@
+/*
+ * 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 executor
+
+import (
+	"context"
+	"testing"
+
+	"github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestNewMySQLUndoDeleteExecutor(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeDelete,
+	}
+
+	executor := newMySQLUndoDeleteExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.NotNil(t, executor.baseExecutor)
+	assert.Equal(t, "test_table", executor.sqlUndoLog.TableName)
+}
+
+func TestMySQLUndoDeleteExecutor_BuildUndoSQL(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeDelete,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+				Indexs: map[string]types.IndexMeta{
+					"id": {
+						IType: types.IndexTypePrimaryKey,
+						Columns: []types.ColumnMeta{
+							{ColumnName: "id"},
+						},
+					},
+				},
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "test"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoDeleteExecutor(sqlUndoLog)
+	sql, err := executor.buildUndoSQL(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.Contains(t, sql, "INSERT INTO")
+	assert.Contains(t, sql, "test_table")
+	assert.Contains(t, sql, "VALUES")
+}
+
+func TestMySQLUndoDeleteExecutor_BuildUndoSQL_EmptyRows(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeDelete,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+			Rows:      []types.RowImage{},
+		},
+	}
+
+	executor := newMySQLUndoDeleteExecutor(sqlUndoLog)
+	sql, err := executor.buildUndoSQL(types.DBTypeMySQL)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "invalid undo log")
+	assert.Empty(t, sql)
+}
+
+func TestMySQLUndoDeleteExecutor_ExecuteOn(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeDelete,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+				Indexs: map[string]types.IndexMeta{
+					"id": {
+						IType: types.IndexTypePrimaryKey,
+						Columns: []types.ColumnMeta{
+							{ColumnName: "id"},
+						},
+					},
+				},
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "test"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoDeleteExecutor(sqlUndoLog)
+
+	// Mock the prepare and exec
+	mock.ExpectPrepare("INSERT INTO (.+)").
+		ExpectExec().
+		WithArgs("test", 1).
+		WillReturnResult(sqlmock.NewResult(1, 1))
+
+	ctx := context.Background()
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	err = executor.ExecuteOn(ctx, types.DBTypeMySQL, conn)
+	assert.NoError(t, err)
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_executor_holder_test.go b/pkg/datasource/sql/undo/executor/mysql_undo_executor_holder_test.go
new file mode 100644
index 00000000..8effe15e
--- /dev/null
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_executor_holder_test.go
@@ -0,0 +1,130 @@
+/*
+ * 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 executor
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestNewMySQLUndoExecutorHolder(t *testing.T) {
+	holder := NewMySQLUndoExecutorHolder()
+	assert.NotNil(t, holder)
+	assert.IsType(t, &MySQLUndoExecutorHolder{}, holder)
+}
+
+func TestMySQLUndoExecutorHolder_GetInsertExecutor(t *testing.T) {
+	holder := NewMySQLUndoExecutorHolder()
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+		},
+	}
+
+	executor := holder.GetInsertExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.IsType(t, &mySQLUndoInsertExecutor{}, executor)
+}
+
+func TestMySQLUndoExecutorHolder_GetUpdateExecutor(t *testing.T) {
+	holder := NewMySQLUndoExecutorHolder()
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeUpdate,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+		},
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+		},
+	}
+
+	executor := holder.GetUpdateExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.IsType(t, &mySQLUndoUpdateExecutor{}, executor)
+}
+
+func TestMySQLUndoExecutorHolder_GetDeleteExecutor(t *testing.T) {
+	holder := NewMySQLUndoExecutorHolder()
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeDelete,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+		},
+	}
+
+	executor := holder.GetDeleteExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.IsType(t, &mySQLUndoDeleteExecutor{}, executor)
+}
+
+func TestMySQLUndoExecutorHolder_InterfaceImplementation(t *testing.T) {
+	var holder undo.UndoExecutorHolder = NewMySQLUndoExecutorHolder()
+	assert.NotNil(t, holder)
+	assert.Implements(t, (*undo.UndoExecutorHolder)(nil), holder)
+}
+
+func TestMySQLUndoExecutorHolder_AllExecutorTypes(t *testing.T) {
+	holder := NewMySQLUndoExecutorHolder()
+
+	testCases := []struct {
+		name     string
+		sqlType  types.SQLType
+		getFunc  func(undo.SQLUndoLog) undo.UndoExecutor
+		expected interface{}
+	}{
+		{
+			name:     "Insert Executor",
+			sqlType:  types.SQLTypeInsert,
+			getFunc:  holder.GetInsertExecutor,
+			expected: &mySQLUndoInsertExecutor{},
+		},
+		{
+			name:     "Update Executor",
+			sqlType:  types.SQLTypeUpdate,
+			getFunc:  holder.GetUpdateExecutor,
+			expected: &mySQLUndoUpdateExecutor{},
+		},
+		{
+			name:     "Delete Executor",
+			sqlType:  types.SQLTypeDelete,
+			getFunc:  holder.GetDeleteExecutor,
+			expected: &mySQLUndoDeleteExecutor{},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			sqlUndoLog := undo.SQLUndoLog{
+				TableName: "test_table",
+				SQLType:   tc.sqlType,
+			}
+
+			executor := tc.getFunc(sqlUndoLog)
+			assert.NotNil(t, executor)
+			assert.IsType(t, tc.expected, executor)
+		})
+	}
+}
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor_test.go b/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor_test.go
new file mode 100644
index 00000000..5abf4711
--- /dev/null
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor_test.go
@@ -0,0 +1,130 @@
+/*
+ * 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 executor
+
+import (
+	"context"
+	"testing"
+
+	"github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestNewMySQLUndoInsertExecutor(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+	}
+
+	executor := newMySQLUndoInsertExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.Equal(t, "test_table", executor.sqlUndoLog.TableName)
+}
+
+func TestMySQLUndoInsertExecutor_BuildUndoSQL(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "test"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoInsertExecutor(sqlUndoLog)
+	sql, err := executor.buildUndoSQL(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.Contains(t, sql, "DELETE FROM")
+	assert.Contains(t, sql, "test_table")
+	assert.Contains(t, sql, "WHERE")
+}
+
+func TestMySQLUndoInsertExecutor_BuildUndoSQL_EmptyRows(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+			Rows:      []types.RowImage{},
+		},
+	}
+
+	executor := newMySQLUndoInsertExecutor(sqlUndoLog)
+	sql, err := executor.buildUndoSQL(types.DBTypeMySQL)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "invalid undo log")
+	assert.Empty(t, sql)
+}
+
+func TestMySQLUndoInsertExecutor_ExecuteOn(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "test"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoInsertExecutor(sqlUndoLog)
+
+	// Mock the prepare and exec
+	mock.ExpectPrepare("DELETE FROM (.+)").
+		ExpectExec().
+		WithArgs(1).
+		WillReturnResult(sqlmock.NewResult(0, 1))
+
+	ctx := context.Background()
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	err = executor.ExecuteOn(ctx, types.DBTypeMySQL, conn)
+	// Note: BaseExecutor.ExecuteOn is nil, so this will likely succeed
+	// We're testing that the method is callable and handles the input
+	assert.NoError(t, err)
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_update_executor_test.go b/pkg/datasource/sql/undo/executor/mysql_undo_update_executor_test.go
new file mode 100644
index 00000000..1c5c82c8
--- /dev/null
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_update_executor_test.go
@@ -0,0 +1,159 @@
+/*
+ * 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 executor
+
+import (
+	"context"
+	"testing"
+
+	"github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestNewMySQLUndoUpdateExecutor(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeUpdate,
+	}
+
+	executor := newMySQLUndoUpdateExecutor(sqlUndoLog)
+	assert.NotNil(t, executor)
+	assert.NotNil(t, executor.baseExecutor)
+	assert.Equal(t, "test_table", executor.sqlUndoLog.TableName)
+}
+
+func TestMySQLUndoUpdateExecutor_BuildUndoSQL(t *testing.T) {
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeUpdate,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "old_name"},
+					},
+				},
+			},
+		},
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "new_name"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoUpdateExecutor(sqlUndoLog)
+	sql, err := executor.buildUndoSQL(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.Contains(t, sql, "UPDATE")
+	assert.Contains(t, sql, "test_table")
+	assert.Contains(t, sql, "SET")
+	assert.Contains(t, sql, "WHERE")
+}
+
+func TestMySQLUndoUpdateExecutor_ExecuteOn_DataValidationDisabled(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	// Disable data validation
+	originalConfig := undo.UndoConfig
+	undo.UndoConfig.DataValidation = false
+	defer func() { undo.UndoConfig = originalConfig }()
+
+	sqlUndoLog := undo.SQLUndoLog{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeUpdate,
+		BeforeImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+				Indexs: map[string]types.IndexMeta{
+					"id": {
+						IType: types.IndexTypePrimaryKey,
+						Columns: []types.ColumnMeta{
+							{ColumnName: "id"},
+						},
+					},
+				},
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "old"},
+					},
+				},
+			},
+		},
+		AfterImage: &types.RecordImage{
+			TableName: "test_table",
+			TableMeta: &types.TableMeta{
+				TableName: "test_table",
+				Indexs: map[string]types.IndexMeta{
+					"id": {
+						IType: types.IndexTypePrimaryKey,
+						Columns: []types.ColumnMeta{
+							{ColumnName: "id"},
+						},
+					},
+				},
+			},
+			Rows: []types.RowImage{
+				{
+					Columns: []types.ColumnImage{
+						{ColumnName: "id", KeyType: types.IndexTypePrimaryKey, Value: 1},
+						{ColumnName: "name", KeyType: types.IndexTypeNull, Value: "new"},
+					},
+				},
+			},
+		},
+	}
+
+	executor := newMySQLUndoUpdateExecutor(sqlUndoLog)
+
+	// Mock the prepare and exec
+	mock.ExpectPrepare("UPDATE (.+)").
+		ExpectExec().
+		WithArgs("old", 1).
+		WillReturnResult(sqlmock.NewResult(0, 1))
+
+	ctx := context.Background()
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	err = executor.ExecuteOn(ctx, types.DBTypeMySQL, conn)
+	assert.NoError(t, err)
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
diff --git a/pkg/datasource/sql/undo/executor/sql_test.go b/pkg/datasource/sql/undo/executor/sql_test.go
index 00aaf7f4..667d3f5f 100644
--- a/pkg/datasource/sql/undo/executor/sql_test.go
+++ b/pkg/datasource/sql/undo/executor/sql_test.go
@@ -54,3 +54,263 @@ func TestAddEscape(t *testing.T) {
 		assert.Equal(t, "`"+v+"`", res)
 	}
 }
+
+func TestDelEscapeEmptyString(t *testing.T) {
+	result := DelEscape("", types.DBTypeMySQL)
+	assert.Equal(t, "", result)
+}
+
+func TestDelEscapeNoEscape(t *testing.T) {
+	result := DelEscape("scheme.id", types.DBTypeMySQL)
+	assert.Equal(t, "scheme.id", result)
+}
+
+func TestDelEscapePartialEscapeSchemaOnly(t *testing.T) {
+	result := DelEscape(`"scheme".id`, types.DBTypeMySQL)
+	assert.Equal(t, "scheme.id", result)
+
+	result = DelEscape("`scheme`.id", types.DBTypeMySQL)
+	assert.Equal(t, "scheme.id", result)
+}
+
+func TestDelEscapePartialEscapeColumnOnly(t *testing.T) {
+	result := DelEscape(`scheme."id"`, types.DBTypeMySQL)
+	assert.Equal(t, "scheme.id", result)
+
+	result = DelEscape("scheme.`id`", types.DBTypeMySQL)
+	assert.Equal(t, "scheme.id", result)
+}
+
+func TestDelEscapeSingleColumn(t *testing.T) {
+	result := DelEscape(`"id"`, types.DBTypeMySQL)
+	assert.Equal(t, "id", result)
+
+	result = DelEscape("`id`", types.DBTypeMySQL)
+	assert.Equal(t, "id", result)
+}
+
+func TestAddEscapeEmptyString(t *testing.T) {
+	result := AddEscape("", types.DBTypeMySQL)
+	assert.Equal(t, "", result)
+}
+
+func TestAddEscapeAlreadyEscaped(t *testing.T) {
+	result := AddEscape("`ALTER`", types.DBTypeMySQL)
+	assert.Equal(t, "`ALTER`", result)
+
+	result = AddEscape(`"ALTER"`, types.DBTypePostgreSQL)
+	assert.Equal(t, `"ALTER"`, result)
+}
+
+func TestAddEscapeNonKeyword(t *testing.T) {
+	result := AddEscape("my_column", types.DBTypeMySQL)
+	assert.Equal(t, "my_column", result)
+
+	result = AddEscape("user_name", types.DBTypeMySQL)
+	assert.Equal(t, "user_name", result)
+}
+
+func TestAddEscapeSchemaColumnKeyword(t *testing.T) {
+	result := AddEscape("ALTER", types.DBTypeMySQL)
+	assert.Equal(t, "`ALTER`", result)
+
+	result = AddEscape("SELECT", types.DBTypeMySQL)
+	assert.Equal(t, "`SELECT`", result)
+}
+
+func TestAddEscapePostgreSQL(t *testing.T) {
+	result := AddEscape("user", types.DBTypePostgreSQL)
+	assert.Equal(t, `"user"`, result)
+
+	result = AddEscape("my_table.user", types.DBTypePostgreSQL)
+	assert.Equal(t, `"my_table"."user"`, result)
+}
+
+func TestAddEscapeOracle(t *testing.T) {
+	result := AddEscape("user", types.DBTypeOracle)
+	assert.Equal(t, `"user"`, result)
+}
+
+func TestAddEscapeSQLServer(t *testing.T) {
+	result := AddEscape("user", types.DBTypeSQLServer)
+	assert.Equal(t, `"user"`, result)
+}
+
+func TestCheckEscapeMySQLKeywords(t *testing.T) {
+	keywords := []string{"SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "CREATE", "DROP", "TABLE"}
+	for _, keyword := range keywords {
+		result := checkEscape(keyword, types.DBTypeMySQL)
+		assert.True(t, result, "Expected %s to be a MySQL keyword", keyword)
+
+		lowerKeyword := keyword
+		result = checkEscape(lowerKeyword, types.DBTypeMySQL)
+		assert.True(t, result, "Expected %s to be a MySQL keyword (case-insensitive)", lowerKeyword)
+	}
+}
+
+func TestCheckEscapeNonKeyword(t *testing.T) {
+	result := checkEscape("my_column", types.DBTypeMySQL)
+	assert.False(t, result)
+
+	result = checkEscape("user_name", types.DBTypeMySQL)
+	assert.False(t, result)
+}
+
+func TestCheckEscapeNonMySQL(t *testing.T) {
+	result := checkEscape("anything", types.DBTypePostgreSQL)
+	assert.True(t, result)
+
+	result = checkEscape("anything", types.DBTypeOracle)
+	assert.True(t, result)
+
+	result = checkEscape("anything", types.DBTypeSQLServer)
+	assert.True(t, result)
+}
+
+func TestGetOrderedPkListSinglePK(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id":   {ColumnName: "id"},
+			"name": {ColumnName: "name"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	image := &types.RecordImage{
+		TableName: "t_user",
+		TableMeta: &tableMeta,
+	}
+
+	row := types.RowImage{
+		Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test", KeyType: types.IndexTypeNull},
+		},
+	}
+
+	result, err := GetOrderedPkList(image, row, types.DBTypeMySQL)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Len(t, result, 1)
+	assert.Equal(t, "id", result[0].ColumnName)
+	assert.Equal(t, 1, result[0].Value)
+}
+
+func TestGetOrderedPkListCompositePK(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_order",
+		Columns: map[string]types.ColumnMeta{
+			"order_id": {ColumnName: "order_id"},
+			"user_id":  {ColumnName: "user_id"},
+			"amount":   {ColumnName: "amount"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "order_id"},
+					{ColumnName: "user_id"},
+				},
+			},
+		},
+	}
+
+	image := &types.RecordImage{
+		TableName: "t_order",
+		TableMeta: &tableMeta,
+	}
+
+	row := types.RowImage{
+		Columns: []types.ColumnImage{
+			{ColumnName: "user_id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "order_id", Value: 100, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "amount", Value: 99.99, KeyType: types.IndexTypeNull},
+		},
+	}
+
+	result, err := GetOrderedPkList(image, row, types.DBTypeMySQL)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Len(t, result, 2)
+	assert.Equal(t, "order_id", result[0].ColumnName)
+	assert.Equal(t, 100, result[0].Value)
+	assert.Equal(t, "user_id", result[1].ColumnName)
+	assert.Equal(t, 1, result[1].Value)
+}
+
+func TestGetOrderedPkListWithEscapedNames(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	image := &types.RecordImage{
+		TableName: "t_user",
+		TableMeta: &tableMeta,
+	}
+
+	row := types.RowImage{
+		Columns: []types.ColumnImage{
+			{ColumnName: "`id`", Value: 1, KeyType: types.IndexTypePrimaryKey},
+		},
+	}
+
+	result, err := GetOrderedPkList(image, row, types.DBTypeMySQL)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Len(t, result, 1)
+	assert.Equal(t, "id", result[0].ColumnName)
+}
+
+func TestGetOrderedPkListEmptyRow(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_user",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	image := &types.RecordImage{
+		TableName: "t_user",
+		TableMeta: &tableMeta,
+	}
+
+	row := types.RowImage{
+		Columns: []types.ColumnImage{},
+	}
+
+	result, err := GetOrderedPkList(image, row, types.DBTypeMySQL)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Len(t, result, 0)
+}
diff --git a/pkg/datasource/sql/undo/executor/utils_test.go b/pkg/datasource/sql/undo/executor/utils_test.go
new file mode 100644
index 00000000..edc99a1f
--- /dev/null
+++ b/pkg/datasource/sql/undo/executor/utils_test.go
@@ -0,0 +1,360 @@
+/*
+ * 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 executor
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+)
+
+func TestIsRecordsEquals_BothNil(t *testing.T) {
+	equal, err := IsRecordsEquals(nil, nil)
+	assert.NoError(t, err)
+	assert.True(t, equal)
+}
+
+func TestIsRecordsEquals_OneNil(t *testing.T) {
+	image := &types.RecordImage{
+		TableName: "test",
+		Rows:      []types.RowImage{},
+	}
+
+	equal, err := IsRecordsEquals(image, nil)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+
+	equal, err = IsRecordsEquals(nil, image)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+}
+
+func TestIsRecordsEquals_DifferentTableNames(t *testing.T) {
+	image1 := &types.RecordImage{
+		TableName: "table1",
+		Rows:      []types.RowImage{},
+	}
+	image2 := &types.RecordImage{
+		TableName: "table2",
+		Rows:      []types.RowImage{},
+	}
+
+	equal, err := IsRecordsEquals(image1, image2)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+}
+
+func TestIsRecordsEquals_DifferentRowCounts(t *testing.T) {
+	image1 := &types.RecordImage{
+		TableName: "test",
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{{ColumnName: "id", Value: 1}}},
+		},
+	}
+	image2 := &types.RecordImage{
+		TableName: "test",
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{{ColumnName: "id", Value: 1}}},
+			{Columns: []types.ColumnImage{{ColumnName: "id", Value: 2}}},
+		},
+	}
+
+	equal, err := IsRecordsEquals(image1, image2)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+}
+
+func TestIsRecordsEquals_EmptyRows(t *testing.T) {
+	image1 := &types.RecordImage{
+		TableName: "test",
+		Rows:      []types.RowImage{},
+	}
+	image2 := &types.RecordImage{
+		TableName: "test",
+		Rows:      []types.RowImage{},
+	}
+
+	equal, err := IsRecordsEquals(image1, image2)
+	assert.NoError(t, err)
+	assert.True(t, equal)
+}
+
+func TestIsRecordsEquals_SameData(t *testing.T) {
+	tableMeta := &types.TableMeta{
+		TableName: "test_table",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				Name:    "PRIMARY",
+				Columns: []types.ColumnMeta{{ColumnName: "id"}},
+				IType:   types.IndexTypePrimaryKey,
+			},
+		},
+	}
+
+	image1 := &types.RecordImage{
+		TableName: "test_table",
+		TableMeta: tableMeta,
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{
+				{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+				{ColumnName: "name", Value: "test"},
+			}},
+		},
+	}
+	image2 := &types.RecordImage{
+		TableName: "test_table",
+		TableMeta: tableMeta,
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{
+				{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+				{ColumnName: "name", Value: "test"},
+			}},
+		},
+	}
+
+	equal, err := IsRecordsEquals(image1, image2)
+	assert.NoError(t, err)
+	assert.True(t, equal)
+}
+
+func TestIsRecordsEquals_DifferentData(t *testing.T) {
+	tableMeta := &types.TableMeta{
+		TableName: "test_table",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				Name:    "PRIMARY",
+				Columns: []types.ColumnMeta{{ColumnName: "id"}},
+				IType:   types.IndexTypePrimaryKey,
+			},
+		},
+	}
+
+	image1 := &types.RecordImage{
+		TableName: "test_table",
+		TableMeta: tableMeta,
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{
+				{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+				{ColumnName: "name", Value: "test1"},
+			}},
+		},
+	}
+	image2 := &types.RecordImage{
+		TableName: "test_table",
+		TableMeta: tableMeta,
+		Rows: []types.RowImage{
+			{Columns: []types.ColumnImage{
+				{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+				{ColumnName: "name", Value: "test2"},
+			}},
+		},
+	}
+
+	equal, err := IsRecordsEquals(image1, image2)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+}
+
+func TestCompareRows_DifferentPrimaryKeys(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "test_table",
+		Columns: map[string]types.ColumnMeta{
+			"id": {ColumnName: "id"},
+		},
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				Name:    "PRIMARY",
+				Columns: []types.ColumnMeta{{ColumnName: "id"}},
+				IType:   types.IndexTypePrimaryKey,
+			},
+		},
+	}
+
+	oldRows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test"},
+		}},
+	}
+	newRows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 2, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test"},
+		}},
+	}
+
+	equal, err := compareRows(tableMeta, oldRows, newRows)
+	assert.NoError(t, err)
+	assert.False(t, equal)
+}
+
+func TestRowListToMap(t *testing.T) {
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test"},
+		}},
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 2, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test2"},
+		}},
+	}
+
+	primaryKeyList := []string{"id"}
+	rowMap := rowListToMap(rows, primaryKeyList)
+
+	assert.NotNil(t, rowMap)
+	assert.Equal(t, 2, len(rowMap))
+	assert.NotNil(t, rowMap["1"])
+	assert.NotNil(t, rowMap["2"])
+	assert.Equal(t, "test", rowMap["1"]["NAME"])
+	assert.Equal(t, "test2", rowMap["2"]["NAME"])
+}
+
+func TestRowListToMap_CompositePrimaryKey(t *testing.T) {
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "user_id", Value: 100, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test"},
+		}},
+	}
+
+	primaryKeyList := []string{"id", "user_id"}
+	rowMap := rowListToMap(rows, primaryKeyList)
+
+	assert.NotNil(t, rowMap)
+	assert.Equal(t, 1, len(rowMap))
+	// The key should be composite: "1_##$$_100"
+	var foundKey string
+	for k := range rowMap {
+		foundKey = k
+		break
+	}
+	assert.Contains(t, foundKey, "1")
+	assert.Contains(t, foundKey, "100")
+}
+
+func TestBuildWhereConditionByPKs_SingleBatch(t *testing.T) {
+	pkNameList := []string{"id"}
+	rowSize := 5
+	maxInSize := 1000
+
+	whereSQL := buildWhereConditionByPKs(pkNameList, rowSize, maxInSize)
+
+	assert.NotEmpty(t, whereSQL)
+	assert.Contains(t, whereSQL, "(`id`) IN")
+	assert.Contains(t, whereSQL, "(?)")     // Should have 5 placeholders
+	assert.NotContains(t, whereSQL, " OR ") // Single batch, no OR
+}
+
+func TestBuildWhereConditionByPKs_MultipleBatches(t *testing.T) {
+	pkNameList := []string{"id"}
+	rowSize := 2500
+	maxInSize := 1000
+
+	whereSQL := buildWhereConditionByPKs(pkNameList, rowSize, maxInSize)
+
+	assert.NotEmpty(t, whereSQL)
+	assert.Contains(t, whereSQL, "(`id`) IN")
+	assert.Contains(t, whereSQL, " OR ") // Multiple batches
+}
+
+func TestBuildWhereConditionByPKs_CompositePK(t *testing.T) {
+	pkNameList := []string{"id", "user_id"}
+	rowSize := 3
+	maxInSize := 1000
+
+	whereSQL := buildWhereConditionByPKs(pkNameList, rowSize, maxInSize)
+
+	assert.NotEmpty(t, whereSQL)
+	assert.Contains(t, whereSQL, "(`id`,`user_id`) IN")
+	assert.Contains(t, whereSQL, "(?,?)")
+}
+
+func TestBuildWhereConditionByPKs_ExactMultiple(t *testing.T) {
+	pkNameList := []string{"id"}
+	rowSize := 2000
+	maxInSize := 1000
+
+	whereSQL := buildWhereConditionByPKs(pkNameList, rowSize, maxInSize)
+
+	assert.NotEmpty(t, whereSQL)
+	assert.Contains(t, whereSQL, " OR ") // Should have 2 batches
+}
+
+func TestBuildPKParams(t *testing.T) {
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test1"},
+		}},
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 2, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test2"},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+	params := buildPKParams(rows, pkNameList)
+
+	assert.NotNil(t, params)
+	assert.Equal(t, 2, len(params))
+	assert.Equal(t, 1, params[0])
+	assert.Equal(t, 2, params[1])
+}
+
+func TestBuildPKParams_CompositePK(t *testing.T) {
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "id", Value: 1, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "user_id", Value: 100, KeyType: types.IndexTypePrimaryKey},
+			{ColumnName: "name", Value: "test"},
+		}},
+	}
+
+	pkNameList := []string{"id", "user_id"}
+	params := buildPKParams(rows, pkNameList)
+
+	assert.NotNil(t, params)
+	assert.Equal(t, 2, len(params))
+	assert.Equal(t, 1, params[0])
+	assert.Equal(t, 100, params[1])
+}
+
+func TestBuildPKParams_NoPK(t *testing.T) {
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "name", Value: "test"},
+		}},
+	}
+
+	pkNameList := []string{"id"}
+	params := buildPKParams(rows, pkNameList)
+
+	assert.NotNil(t, params)
+	assert.Equal(t, 0, len(params))
+}
diff --git a/pkg/datasource/sql/undo/mysql/default_test.go b/pkg/datasource/sql/undo/mysql/default_test.go
new file mode 100644
index 00000000..c51351fe
--- /dev/null
+++ b/pkg/datasource/sql/undo/mysql/default_test.go
@@ -0,0 +1,62 @@
+/*
+ * 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 (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestInitUndoLogManager(t *testing.T) {
+	// Call InitUndoLogManager
+	InitUndoLogManager()
+
+	// Verify that the manager was registered
+	manager, err := undo.GetUndoLogManager(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.NotNil(t, manager)
+	assert.Equal(t, types.DBTypeMySQL, manager.DBType())
+}
+
+func TestInitUndoLogManager_Multiple(t *testing.T) {
+	// Calling InitUndoLogManager multiple times should not panic
+	// The second call will be a no-op since the manager is already registered
+	InitUndoLogManager()
+	InitUndoLogManager()
+
+	// Verify manager is still accessible
+	manager, err := undo.GetUndoLogManager(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.NotNil(t, manager)
+}
+
+func TestInitUndoLogManager_ManagerType(t *testing.T) {
+	InitUndoLogManager()
+
+	manager, err := undo.GetUndoLogManager(types.DBTypeMySQL)
+	assert.NoError(t, err)
+	assert.NotNil(t, manager)
+
+	// Verify it's the correct type
+	_, ok := manager.(*undoLogManager)
+	assert.True(t, ok, "Manager should be of type *undoLogManager")
+}
diff --git a/pkg/datasource/sql/undo/mysql/undo_test.go b/pkg/datasource/sql/undo/mysql/undo_test.go
new file mode 100644
index 00000000..2557a140
--- /dev/null
+++ b/pkg/datasource/sql/undo/mysql/undo_test.go
@@ -0,0 +1,337 @@
+/*
+ * 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"
+	"database/sql/driver"
+	"testing"
+
+	"github.com/DATA-DOG/go-sqlmock"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"seata.apache.org/seata-go/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/pkg/datasource/sql/undo"
+)
+
+func TestNewUndoLogManager(t *testing.T) {
+	manager := NewUndoLogManager()
+	assert.NotNil(t, manager)
+	assert.NotNil(t, manager.Base)
+}
+
+func TestUndoLogManager_Init(t *testing.T) {
+	manager := NewUndoLogManager()
+	// Init should not panic
+	manager.Init()
+}
+
+func TestUndoLogManager_DBType(t *testing.T) {
+	manager := NewUndoLogManager()
+	dbType := manager.DBType()
+	assert.Equal(t, types.DBTypeMySQL, dbType)
+}
+
+func TestUndoLogManager_DeleteUndoLog(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+	xid := "test-xid-123"
+	branchID := int64(456)
+
+	// Mock the connection
+	mock.ExpectBegin()
+	mock.ExpectPrepare("DELETE FROM (.+) WHERE branch_id = \\? AND xid = \\?").
+		ExpectExec().
+		WithArgs(branchID, xid).
+		WillReturnResult(sqlmock.NewResult(0, 1))
+	mock.ExpectCommit()
+
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	// Start transaction to get the conn in proper state
+	tx, err := conn.BeginTx(ctx, nil)
+	require.NoError(t, err)
+
+	err = manager.DeleteUndoLog(ctx, xid, branchID, conn)
+	assert.NoError(t, err)
+
+	tx.Commit()
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_DeleteUndoLog_PrepareError(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+	xid := "test-xid-123"
+	branchID := int64(456)
+
+	// Mock connection and prepare error
+	mock.ExpectBegin()
+	mock.ExpectPrepare("DELETE FROM (.+) WHERE branch_id = \\? AND xid = \\?").
+		WillReturnError(sql.ErrConnDone)
+	mock.ExpectRollback()
+
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	tx, err := conn.BeginTx(ctx, nil)
+	require.NoError(t, err)
+
+	err = manager.DeleteUndoLog(ctx, xid, branchID, conn)
+	assert.Error(t, err)
+
+	tx.Rollback()
+}
+
+func TestUndoLogManager_BatchDeleteUndoLog(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+	xids := []string{"xid-1", "xid-2"}
+	branchIDs := []int64{100, 200}
+
+	// Mock the batch delete
+	mock.ExpectBegin()
+	mock.ExpectPrepare("DELETE FROM (.+) WHERE branch_id IN (.+) AND xid IN (.+)").
+		ExpectExec().
+		WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg()).
+		WillReturnResult(sqlmock.NewResult(0, 2))
+	mock.ExpectCommit()
+
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	tx, err := conn.BeginTx(ctx, nil)
+	require.NoError(t, err)
+
+	err = manager.BatchDeleteUndoLog(xids, branchIDs, conn)
+	assert.NoError(t, err)
+
+	tx.Commit()
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_BatchDeleteUndoLog_EmptySlices(t *testing.T) {
+	db, _, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	// Test with empty slices
+	err = manager.BatchDeleteUndoLog([]string{}, []int64{}, conn)
+	assert.Error(t, err)
+}
+
+func TestUndoLogManager_FlushUndoLog(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+
+	// Create a mock driver.Conn
+	driverConn, err := db.Conn(context.Background())
+	require.NoError(t, err)
+	defer driverConn.Close()
+
+	// Create transaction context with empty round images
+	tranCtx := &types.TransactionContext{
+		XID:         "test-xid",
+		BranchID:    123,
+		RoundImages: &types.RoundRecordImage{},
+	}
+
+	// Get raw driver connection
+	var rawConn driver.Conn
+	err = driverConn.Raw(func(dc interface{}) error {
+		rawConn = dc.(driver.Conn)
+		return nil
+	})
+	require.NoError(t, err)
+
+	// FlushUndoLog should return nil for empty images
+	err = manager.FlushUndoLog(tranCtx, rawConn)
+	assert.NoError(t, err)
+
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_FlushUndoLog_WithImages(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	// Save original config and restore after test
+	originalSerialization := undo.UndoConfig.LogSerialization
+	originalCompressType := undo.UndoConfig.CompressConfig.Type
+	defer func() {
+		undo.UndoConfig.LogSerialization = originalSerialization
+		undo.UndoConfig.CompressConfig.Type = originalCompressType
+	}()
+
+	undo.UndoConfig.LogSerialization = "json"
+	undo.UndoConfig.CompressConfig.Type = "none"
+
+	manager := NewUndoLogManager()
+
+	// Create a mock driver.Conn
+	driverConn, err := db.Conn(context.Background())
+	require.NoError(t, err)
+	defer driverConn.Close()
+
+	// Create transaction context with images
+	roundImages := &types.RoundRecordImage{}
+
+	// Add before images
+	beforeImage := &types.RecordImage{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		Rows:      []types.RowImage{},
+	}
+	roundImages.AppendBeofreImage(beforeImage)
+
+	// Add after images
+	afterImage := &types.RecordImage{
+		TableName: "test_table",
+		SQLType:   types.SQLTypeInsert,
+		Rows: []types.RowImage{
+			{
+				Columns: []types.ColumnImage{
+					{ColumnName: "id", Value: 1},
+				},
+			},
+		},
+	}
+	roundImages.AppendAfterImage(afterImage)
+
+	tranCtx := &types.TransactionContext{
+		XID:         "test-xid",
+		BranchID:    123,
+		RoundImages: roundImages,
+	}
+
+	// Mock the insert operation
+	mock.ExpectPrepare("INSERT INTO (.+) VALUES").
+		ExpectExec().
+		WillReturnResult(sqlmock.NewResult(1, 1))
+
+	// Get raw driver connection
+	var rawConn driver.Conn
+	err = driverConn.Raw(func(dc interface{}) error {
+		rawConn = dc.(driver.Conn)
+		return nil
+	})
+	require.NoError(t, err)
+
+	// FlushUndoLog should succeed
+	err = manager.FlushUndoLog(tranCtx, rawConn)
+	// Error expected as we're using sqlmock which doesn't fully implement all driver features
+	// The test verifies the method is callable and handles the input
+	assert.NoError(t, err)
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_RunUndo(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	// Save original config and restore after test
+	originalSerialization := undo.UndoConfig.LogSerialization
+	defer func() {
+		undo.UndoConfig.LogSerialization = originalSerialization
+	}()
+
+	undo.UndoConfig.LogSerialization = "json"
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+	xid := "test-xid"
+	branchID := int64(123)
+	dbName := "test_db"
+
+	// Mock the connection for Undo operation - expect full flow
+	mock.ExpectBegin()
+	mock.ExpectPrepare("SELECT (.+) FROM (.+) WHERE branch_id = \\? AND xid = \\? FOR UPDATE").
+		ExpectQuery().
+		WithArgs(branchID, xid).
+		WillReturnRows(sqlmock.NewRows([]string{"branch_id", "xid", "context", "rollback_info", "log_status"}))
+	// Expect INSERT for undo with global finished status
+	mock.ExpectPrepare("INSERT INTO undo_log").
+		ExpectExec().
+		WillReturnResult(sqlmock.NewResult(1, 1))
+	mock.ExpectCommit()
+
+	err = manager.RunUndo(ctx, xid, branchID, db, dbName)
+	assert.NoError(t, err)
+	// The method will execute and attempt database operations
+	// We're testing that it's callable and delegates to Base.Undo
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_HasUndoLogTable(t *testing.T) {
+	db, mock, err := sqlmock.New()
+	require.NoError(t, err)
+	defer db.Close()
+
+	manager := NewUndoLogManager()
+	ctx := context.Background()
+
+	// Mock successful query (table exists)
+	mock.ExpectQuery("SELECT 1 FROM (.+) LIMIT 1").
+		WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
+
+	conn, err := db.Conn(ctx)
+	require.NoError(t, err)
+	defer conn.Close()
+
+	exists, err := manager.HasUndoLogTable(ctx, conn)
+	assert.NoError(t, err)
+	assert.True(t, exists)
+
+	assert.NoError(t, mock.ExpectationsWereMet())
+}
+
+func TestUndoLogManager_InterfaceImplementation(t *testing.T) {
+	var _ undo.UndoLogManager = (*undoLogManager)(nil)
+	manager := NewUndoLogManager()
+	assert.Implements(t, (*undo.UndoLogManager)(nil), manager)
+}
diff --git a/pkg/datasource/sql/undo/parser/parser_cache_test.go b/pkg/datasource/sql/undo/parser/parser_cache_test.go
index a1b80918..5aebb39b 100644
--- a/pkg/datasource/sql/undo/parser/parser_cache_test.go
+++ b/pkg/datasource/sql/undo/parser/parser_cache_test.go
@@ -45,3 +45,35 @@ func TestLoad(t *testing.T) {
 	assert.Nil(t, err)
 	assert.NotNil(t, jsonParser)
 }
+
+func TestLoad_NotFound(t *testing.T) {
+	parser, err := GetCache().Load("nonexistent")
+	assert.NotNil(t, err)
+	assert.Nil(t, parser)
+	assert.Contains(t, err.Error(), "not found")
+}
+
+func TestLoad_Protobuf(t *testing.T) {
+	protobufParser, err := GetCache().Load("protobuf")
+	assert.Nil(t, err)
+	assert.NotNil(t, protobufParser)
+	assert.Equal(t, "protobuf", protobufParser.GetName())
+}
+
+func TestUndoLogParserCache_Store(t *testing.T) {
+	cache := GetCache()
+	assert.NotNil(t, cache)
+
+	// Verify both parsers are stored
+	jsonParser, err := cache.Load("json")
+	assert.NoError(t, err)
+	assert.NotNil(t, jsonParser)
+
+	protobufParser, err := cache.Load("protobuf")
+	assert.NoError(t, err)
+	assert.NotNil(t, protobufParser)
+}
+
+func TestDefaultSerializer(t *testing.T) {
+	assert.Equal(t, "json", DefaultSerializer)
+}
diff --git a/pkg/datasource/sql/undo/parser/parser_json_test.go b/pkg/datasource/sql/undo/parser/parser_json_test.go
index 2123f082..dc4eb095 100644
--- a/pkg/datasource/sql/undo/parser/parser_json_test.go
+++ b/pkg/datasource/sql/undo/parser/parser_json_test.go
@@ -103,3 +103,43 @@ func TestJsonDecode(t *testing.T) {
 	}
 
 }
+
+func TestJsonDecode_InvalidJSON(t *testing.T) {
+	logParser := &JsonParser{}
+	undoLog, err := logParser.Decode([]byte("invalid json"))
+	assert.NotNil(t, err)
+	assert.Nil(t, undoLog)
+}
+
+func TestJsonDecode_EmptyBytes(t *testing.T) {
+	logParser := &JsonParser{}
+	undoLog, err := logParser.Decode([]byte(""))
+	assert.NotNil(t, err)
+	assert.Nil(t, undoLog)
+}
+
+func TestJsonEncode_WithSQLUndoLogs(t *testing.T) {
+	logParser := &JsonParser{}
+	branchUndoLog := &undo.BranchUndoLog{
+		Xid:      "test-xid",
+		BranchID: 12345,
+		Logs: []undo.SQLUndoLog{
+			{
+				TableName: "test_table",
+			},
+		},
+	}
+
+	bytes, err := logParser.Encode(branchUndoLog)
+	assert.Nil(t, err)
+	assert.NotNil(t, bytes)
+	assert.Contains(t, string(bytes), "test-xid")
+	assert.Contains(t, string(bytes), "test_table")
+}
+
+func TestJsonParser_Interface(t *testing.T) {
+	var parser UndoLogParser = &JsonParser{}
+	assert.NotNil(t, parser)
+	assert.Equal(t, "json", parser.GetName())
+	assert.Equal(t, []byte("{}"), parser.GetDefaultContent())
+}
diff --git a/pkg/datasource/sql/undo/parser/parser_protobuf.go b/pkg/datasource/sql/undo/parser/parser_protobuf.go
index 7fd56082..c9f4529b 100644
--- a/pkg/datasource/sql/undo/parser/parser_protobuf.go
+++ b/pkg/datasource/sql/undo/parser/parser_protobuf.go
@@ -19,6 +19,7 @@ package parser
 
 import (
 	"encoding/json"
+	"fmt"
 
 	"github.com/golang/protobuf/ptypes/any"
 	"github.com/golang/protobuf/ptypes/wrappers"
@@ -44,6 +45,9 @@ func (p *ProtobufParser) GetDefaultContent() []byte {
 
 // Encode branch undo log to byte array
 func (p *ProtobufParser) Encode(branchUndoLog *undo.BranchUndoLog) ([]byte, error) {
+	if branchUndoLog == nil {
+		return nil, fmt.Errorf("branchUndoLog cannot be nil")
+	}
 	protoLog := ConvertToProto(branchUndoLog)
 	return proto.Marshal(protoLog)
 }
@@ -60,6 +64,9 @@ func (p *ProtobufParser) Decode(data []byte) (*undo.BranchUndoLog, error) {
 }
 
 func ConvertToProto(intreeLog *undo.BranchUndoLog) *BranchUndoLog {
+	if intreeLog == nil {
+		return nil
+	}
 	protoLog := &BranchUndoLog{
 		Xid:      intreeLog.Xid,
 		BranchID: intreeLog.BranchID,
diff --git a/pkg/datasource/sql/undo/parser/parser_protobuf_test.go b/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
index e8cb0d52..7427d590 100644
--- a/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
+++ b/pkg/datasource/sql/undo/parser/parser_protobuf_test.go
@@ -89,3 +89,35 @@ func TestConvertInterfaceToAnyAndBack(t *testing.T) {
 
 	assert.Equal(t, originalValue, convertedValue, "The converted value should match the original")
 }
+
+func TestProtobufParser_Interface(t *testing.T) {
+	var parser UndoLogParser = &ProtobufParser{}
+	assert.NotNil(t, parser)
+	assert.Equal(t, "protobuf", parser.GetName())
+	assert.Equal(t, []byte{}, parser.GetDefaultContent())
+}
+
+func TestProtobufDecode_InvalidData(t *testing.T) {
+	parser := &ProtobufParser{}
+	undoLog, err := parser.Decode([]byte("invalid protobuf data"))
+	assert.NotNil(t, err)
+	assert.Nil(t, undoLog)
+}
+
+func TestProtobufDecode_EmptyData(t *testing.T) {
+	parser := &ProtobufParser{}
+	undoLog, err := parser.Decode([]byte{})
+	// Empty data decodes to empty BranchUndoLog (protobuf behavior)
+	assert.NoError(t, err)
+	assert.NotNil(t, undoLog)
+	assert.Empty(t, undoLog.Xid)
+	assert.Equal(t, uint64(0), undoLog.BranchID)
+	assert.Empty(t, undoLog.Logs)
+}
+
+func TestProtobufEncode_Nil(t *testing.T) {
+	parser := &ProtobufParser{}
+	bytes, err := parser.Encode(nil)
+	assert.NotNil(t, err)
+	assert.Nil(t, bytes)
+}
diff --git a/pkg/tm/transaction_executor_test.go b/pkg/tm/transaction_executor_test.go
index 277f2397..cec6e24d 100644
--- a/pkg/tm/transaction_executor_test.go
+++ b/pkg/tm/transaction_executor_test.go
@@ -276,6 +276,7 @@ func TestCommitOrRollback(t *testing.T) {
 		{
 			ctx: context.Background(),
 			tx: GlobalTransaction{
+				Xid:    "test-xid-rollback",
 				TxRole: Launcher,
 			},
 			ok:                 false,


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@seata.apache.org
For additional commands, e-mail: notifications-help@seata.apache.org