You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@seata.apache.org by fl...@apache.org on 2026/03/22 14:15:55 UTC

(incubator-seata-go) branch master updated: fix: resolve undo_log duplicate PK when column names have escape characters (#1065)

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

flypiggy 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 1b526c36 fix: resolve undo_log duplicate PK when column names have escape characters (#1065)
1b526c36 is described below

commit 1b526c361555fcb6d7ef50221ba296bc022ae674
Author: ThunGuo <te...@apache.org>
AuthorDate: Sun Mar 22 22:15:47 2026 +0800

    fix: resolve undo_log duplicate PK when column names have escape characters (#1065)
    
    Co-authored-by: FengZhang <zf...@qq.com>
---
 pkg/datasource/sql/datasource/mysql/trigger.go     |   6 +-
 pkg/datasource/sql/exec/at/base_executor.go        |  19 +-
 pkg/datasource/sql/exec/at/base_executor_test.go   | 111 +++++++++++
 pkg/datasource/sql/exec/at/escape.go               | 210 ---------------------
 pkg/datasource/sql/exec/at/escape_test.go          |  56 ------
 pkg/datasource/sql/exec/at/insert_executor.go      |   8 +-
 pkg/datasource/sql/exec/at/insert_executor_test.go |  35 ++++
 .../sql/exec/at/insert_on_update_executor.go       |   8 +-
 .../sql/undo/builder/basic_undo_log_builder.go     |  11 +-
 .../undo/builder/mysql_insert_undo_log_builder.go  |   6 +-
 ...ql_insertonduplicate_update_undo_log_builder.go |   4 +-
 pkg/datasource/sql/undo/executor/executor.go       |   4 +-
 pkg/datasource/sql/undo/executor/executor_test.go  |  49 +++++
 .../undo/executor/mysql_undo_delete_executor.go    |   7 +-
 .../executor/mysql_undo_delete_executor_test.go    |   9 +-
 .../undo/executor/mysql_undo_insert_executor.go    |   5 +-
 .../executor/mysql_undo_insert_executor_test.go    |  28 ++-
 .../undo/executor/mysql_undo_update_executor.go    |   9 +-
 .../executor/mysql_undo_update_executor_test.go    |  13 +-
 pkg/datasource/sql/undo/executor/utils.go          |  14 +-
 pkg/datasource/sql/undo/executor/utils_test.go     |  53 ++++++
 .../sql/{undo/executor/sql.go => util/escape.go}   |   8 +-
 .../executor/sql_test.go => util/escape_test.go}   |  16 +-
 pkg/datasource/sql/util/lockkey.go                 |   3 +-
 pkg/datasource/sql/util/lockkey_test.go            |  68 +++++++
 25 files changed, 429 insertions(+), 331 deletions(-)

diff --git a/pkg/datasource/sql/datasource/mysql/trigger.go b/pkg/datasource/sql/datasource/mysql/trigger.go
index 1d9f0ee1..a14189c5 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger.go
@@ -26,7 +26,7 @@ import (
 	"github.com/pkg/errors"
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo/executor"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 type mysqlTrigger struct {
@@ -99,7 +99,7 @@ func (m *mysqlTrigger) getColumnMetas(ctx context.Context, dbName string, table
 		return m.getColumnMetasFn(ctx, dbName, table, conn)
 	}
 
-	table = executor.DelEscape(table, types.DBTypeMySQL)
+	table = util.DelEscape(table, types.DBTypeMySQL)
 	var columnMetas []types.ColumnMeta
 
 	columnMetaSql := "SELECT `TABLE_NAME`, `TABLE_SCHEMA`, `COLUMN_NAME`, `DATA_TYPE`, `COLUMN_TYPE`, `COLUMN_KEY`, `IS_NULLABLE`, `COLUMN_DEFAULT`, `EXTRA` FROM INFORMATION_SCHEMA.COLUMNS WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?"
@@ -175,7 +175,7 @@ func (m *mysqlTrigger) getIndexes(ctx context.Context, dbName string, tableName
 		return m.getIndexesFn(ctx, dbName, tableName, conn)
 	}
 
-	tableName = executor.DelEscape(tableName, types.DBTypeMySQL)
+	tableName = util.DelEscape(tableName, types.DBTypeMySQL)
 	result := make([]types.IndexMeta, 0)
 
 	indexMetaSql := "SELECT `INDEX_NAME`, `COLUMN_NAME`, `NON_UNIQUE` FROM `INFORMATION_SCHEMA`.`STATISTICS` WHERE `TABLE_SCHEMA` = ? AND `TABLE_NAME` = ?"
diff --git a/pkg/datasource/sql/exec/at/base_executor.go b/pkg/datasource/sql/exec/at/base_executor.go
index 6e031488..c34654e2 100644
--- a/pkg/datasource/sql/exec/at/base_executor.go
+++ b/pkg/datasource/sql/exec/at/base_executor.go
@@ -242,10 +242,11 @@ func (b *baseExecutor) buildRecordImages(rowsi driver.Rows, tableMetaData *types
 		columns := make([]types.ColumnImage, 0)
 		// build record image
 		for i, name := range columnNames {
-			columnMeta := tableMetaData.Columns[name]
+			cleanName := util.DelEscape(name, types.DBTypeMySQL)
+			columnMeta := tableMetaData.Columns[cleanName]
 
 			keyType := types.IndexTypeNull
-			if _, ok := tableMetaData.GetPrimaryKeyMap()[name]; ok {
+			if _, ok := tableMetaData.GetPrimaryKeyMap()[cleanName]; ok {
 				keyType = types.IndexTypePrimaryKey
 			}
 			jdbcType := types.MySQLStrToJavaType(columnMeta.DatabaseTypeString)
@@ -281,7 +282,7 @@ func (b *baseExecutor) getNeedColumns(meta *types.TableMeta, columns []string, d
 	}
 
 	for i := range needUpdateColumns {
-		needUpdateColumns[i] = AddEscape(needUpdateColumns[i], dbType)
+		needUpdateColumns[i] = util.AddEscape(needUpdateColumns[i], dbType)
 	}
 	return needUpdateColumns
 }
@@ -294,9 +295,9 @@ func (b *baseExecutor) containsPKByName(meta *types.TableMeta, columns []string)
 
 	matchCounter := 0
 	for _, column := range columns {
+		cleanColumn := util.DelEscape(column, types.DBTypeMySQL)
 		for _, pkName := range pkColumnNameList {
-			if strings.EqualFold(pkName, column) ||
-				strings.EqualFold(pkName, strings.ToLower(column)) {
+			if strings.EqualFold(pkName, cleanColumn) {
 				matchCounter++
 			}
 		}
@@ -479,8 +480,14 @@ func (b *baseExecutor) buildPKParams(rows []types.RowImage, pkNameList []string)
 	params := make([]driver.NamedValue, 0)
 	for _, row := range rows {
 		coumnMap := row.GetColumnMap()
+		// Build a normalized map with escaped characters removed
+		normalizedMap := make(map[string]*types.ColumnImage, len(coumnMap))
+		for k, v := range coumnMap {
+			normalizedMap[util.DelEscape(k, types.DBTypeMySQL)] = v
+		}
 		for i, pk := range pkNameList {
-			if col, ok := coumnMap[pk]; ok {
+			cleanPK := util.DelEscape(pk, types.DBTypeMySQL)
+			if col, ok := normalizedMap[cleanPK]; ok {
 				params = append(params, driver.NamedValue{
 					Ordinal: i, Value: col.Value,
 				})
diff --git a/pkg/datasource/sql/exec/at/base_executor_test.go b/pkg/datasource/sql/exec/at/base_executor_test.go
index 9d1d52a4..29843831 100644
--- a/pkg/datasource/sql/exec/at/base_executor_test.go
+++ b/pkg/datasource/sql/exec/at/base_executor_test.go
@@ -211,3 +211,114 @@ func TestBaseExecBuildLockKey(t *testing.T) {
 		})
 	}
 }
+
+func TestBaseExecContainsPKByName_EscapedColumns(t *testing.T) {
+	meta := &types.TableMeta{
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	tests := []struct {
+		name    string
+		columns []string
+		want    bool
+	}{
+		{
+			name:    "plain column matches PK",
+			columns: []string{"id", "name"},
+			want:    true,
+		},
+		{
+			name:    "backtick-escaped column matches PK",
+			columns: []string{"`id`", "name"},
+			want:    true,
+		},
+		{
+			name:    "all backtick-escaped columns",
+			columns: []string{"`id`", "`name`"},
+			want:    true,
+		},
+		{
+			name:    "case-insensitive backtick-escaped",
+			columns: []string{"`ID`", "`NAME`"},
+			want:    true,
+		},
+		{
+			name:    "no PK in columns",
+			columns: []string{"`name`", "`age`"},
+			want:    false,
+		},
+	}
+
+	var exec baseExecutor
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := exec.containsPKByName(meta, tt.columns)
+			assert.Equal(t, tt.want, result)
+		})
+	}
+}
+
+func TestBaseExecBuildLockKey_EscapedColumnNames(t *testing.T) {
+	var exec baseExecutor
+
+	getColumnImage := func(columnName string, value interface{}) types.ColumnImage {
+		return types.ColumnImage{KeyType: types.IndexTypePrimaryKey, ColumnName: columnName, Value: value}
+	}
+
+	tests := []struct {
+		name     string
+		metaData types.TableMeta
+		records  types.RecordImage
+		expected string
+	}{
+		{
+			name: "Backtick-escaped single PK",
+			metaData: types.TableMeta{
+				TableName: "test_table",
+				Indexs: map[string]types.IndexMeta{
+					"PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: []types.ColumnMeta{{ColumnName: "id"}}},
+				},
+			},
+			records: types.RecordImage{
+				TableName: "test_table",
+				Rows: []types.RowImage{
+					{Columns: []types.ColumnImage{getColumnImage("`id`", 1), {ColumnName: "`name`", Value: "test"}}},
+				},
+			},
+			expected: "TEST_TABLE:1",
+		},
+		{
+			name: "Backtick-escaped composite PK",
+			metaData: types.TableMeta{
+				TableName: "orders",
+				Indexs: map[string]types.IndexMeta{
+					"PRIMARY_KEY": {IType: types.IndexTypePrimaryKey, Columns: []types.ColumnMeta{
+						{ColumnName: "order_id"},
+						{ColumnName: "user_id"},
+					}},
+				},
+			},
+			records: types.RecordImage{
+				TableName: "orders",
+				Rows: []types.RowImage{
+					{Columns: []types.ColumnImage{getColumnImage("`order_id`", 100), getColumnImage("`user_id`", 1)}},
+					{Columns: []types.ColumnImage{getColumnImage("`order_id`", 200), getColumnImage("`user_id`", 2)}},
+				},
+			},
+			expected: "ORDERS:100_1,200_2",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			lockKeys := exec.buildLockKey(&tt.records, tt.metaData)
+			assert.Equal(t, tt.expected, lockKeys)
+		})
+	}
+}
diff --git a/pkg/datasource/sql/exec/at/escape.go b/pkg/datasource/sql/exec/at/escape.go
deleted file mode 100644
index 676959d2..00000000
--- a/pkg/datasource/sql/exec/at/escape.go
+++ /dev/null
@@ -1,210 +0,0 @@
-/*
- * 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 at
-
-import (
-	"database/sql"
-	"strings"
-
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
-)
-
-const (
-	dot            = "."
-	escapeStandard = "\""
-	escapeMysql    = "`"
-)
-
-// DelEscape del escape by db type
-func DelEscape(colName string, dbType types.DBType) string {
-	newColName := delEscape(colName, escapeStandard)
-	if dbType == types.DBTypeMySQL {
-		newColName = delEscape(newColName, escapeMysql)
-	}
-	return newColName
-}
-
-// delEscape
-func delEscape(colName string, escape string) string {
-	if colName == "" {
-		return ""
-	}
-
-	if string(colName[0]) == escape && string(colName[len(colName)-1]) == escape {
-		// like "scheme"."id" `scheme`.`id`
-		str := escape + dot + escape
-		index := strings.Index(colName, str)
-		if index > -1 {
-			return colName[1:index] + dot + colName[index+len(str):len(colName)-1]
-		}
-
-		return colName[1 : len(colName)-1]
-	} else {
-		// like "scheme".id `scheme`.id
-		str := escape + dot
-		index := strings.Index(colName, str)
-		if index > -1 && string(colName[0]) == escape {
-			return colName[1:index] + dot + colName[index+len(str):]
-		}
-
-		// like scheme."id" scheme.`id`
-		str = dot + escape
-		index = strings.Index(colName, str)
-		if index > -1 && string(colName[len(colName)-1]) == escape {
-			return colName[0:index] + dot + colName[index+len(str):len(colName)-1]
-		}
-	}
-
-	return colName
-}
-
-// AddEscape if necessary, add escape by db type
-func AddEscape(colName string, dbType types.DBType) string {
-	if dbType == types.DBTypeMySQL {
-		return addEscape(colName, dbType, escapeMysql)
-	}
-
-	return addEscape(colName, dbType, escapeStandard)
-}
-
-func addEscape(colName string, dbType types.DBType, escape string) string {
-	if colName == "" {
-		return colName
-	}
-
-	if string(colName[0]) == escape && string(colName[len(colName)-1]) == escape {
-		return colName
-	}
-
-	if !checkEscape(colName, dbType) {
-		return colName
-	}
-
-	if strings.Contains(colName, dot) {
-		// like "scheme".id `scheme`.id
-		str := escape + dot
-		dotIndex := strings.Index(colName, str)
-		if dotIndex > -1 {
-			tempStr := strings.Builder{}
-			tempStr.WriteString(colName[0 : dotIndex+len(str)])
-			tempStr.WriteString(escape)
-			tempStr.WriteString(colName[dotIndex+len(str):])
-			tempStr.WriteString(escape)
-
-			return tempStr.String()
-		}
-
-		// like scheme."id" scheme.`id`
-		str = dot + escape
-		dotIndex = strings.Index(colName, str)
-		if dotIndex > -1 {
-			tempStr := strings.Builder{}
-			tempStr.WriteString(escape)
-			tempStr.WriteString(colName[0:dotIndex])
-			tempStr.WriteString(escape)
-			tempStr.WriteString(colName[dotIndex:])
-
-			return tempStr.String()
-		}
-
-		str = dot
-		dotIndex = strings.Index(colName, str)
-		if dotIndex > -1 {
-			tempStr := strings.Builder{}
-			tempStr.WriteString(escape)
-			tempStr.WriteString(colName[0:dotIndex])
-			tempStr.WriteString(escape)
-			tempStr.WriteString(dot)
-			tempStr.WriteString(escape)
-			tempStr.WriteString(colName[dotIndex+len(str):])
-			tempStr.WriteString(escape)
-
-			return tempStr.String()
-		}
-	}
-
-	buf := make([]byte, len(colName)+2)
-	buf[0], buf[len(buf)-1] = escape[0], escape[0]
-
-	for key := range colName {
-		buf[key+1] = colName[key]
-	}
-
-	return string(buf)
-}
-
-// checkEscape check whether given field or table name use keywords. the method has database special logic.
-func checkEscape(colName string, dbType types.DBType) bool {
-	switch dbType {
-	case types.DBTypeMySQL:
-		if _, ok := types.GetMysqlKeyWord()[strings.ToUpper(colName)]; ok {
-			return true
-		}
-
-		return false
-	// TODO impl Oracle PG SQLServer ...
-	default:
-		return true
-	}
-}
-
-// BuildWhereConditionByPKs each pk is a condition.the result will like :" id =? and userCode =?"
-func BuildWhereConditionByPKs(pkNameList []string, dbType types.DBType) string {
-	whereStr := strings.Builder{}
-	for i := 0; i < len(pkNameList); i++ {
-		if i > 0 {
-			whereStr.WriteString(" and ")
-		}
-
-		pkName := pkNameList[i]
-		whereStr.WriteString(AddEscape(pkName, dbType))
-		whereStr.WriteString(" = ? ")
-	}
-
-	return whereStr.String()
-}
-
-// DataValidationAndGoOn check data valid
-// Todo implement dataValidationAndGoOn
-func DataValidationAndGoOn(sqlUndoLog undo.SQLUndoLog, conn *sql.Conn) bool {
-	return true
-}
-
-func GetOrderedPkList(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
-
-	pkColumnNameListByOrder := image.TableMeta.GetPrimaryKeyOnlyName()
-
-	pkColumnNameListNoOrder := make([]types.ColumnImage, 0)
-	pkFields := make([]types.ColumnImage, 0)
-
-	for _, column := range row.PrimaryKeys(row.Columns) {
-		column.ColumnName = DelEscape(column.ColumnName, dbType)
-		pkColumnNameListNoOrder = append(pkColumnNameListNoOrder, column)
-	}
-
-	for _, pkName := range pkColumnNameListByOrder {
-		for _, col := range pkColumnNameListNoOrder {
-			if strings.Index(col.ColumnName, pkName) > -1 {
-				pkFields = append(pkFields, col)
-			}
-		}
-	}
-
-	return pkFields, nil
-}
diff --git a/pkg/datasource/sql/exec/at/escape_test.go b/pkg/datasource/sql/exec/at/escape_test.go
deleted file mode 100644
index 2481810d..00000000
--- a/pkg/datasource/sql/exec/at/escape_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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 at
-
-import (
-	"log"
-	"testing"
-
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
-
-	"github.com/stretchr/testify/assert"
-)
-
-// TestDelEscape
-func TestDelEscape(t *testing.T) {
-	strSlice := []string{`"scheme"."id"`, "`scheme`.`id`", `"scheme".id`, `scheme."id"`, `scheme."id"`, "scheme.`id`"}
-
-	for k, v := range strSlice {
-		res := DelEscape(v, types.DBTypeMySQL)
-		log.Printf("val_%d: %s, res_%d: %s\n", k, v, k, res)
-		assert.Equal(t, "scheme.id", res)
-	}
-}
-
-// TestAddEscape
-func TestAddEscape(t *testing.T) {
-	strSlice := []string{`"scheme".id`, "`scheme`.id", `scheme."id"`, "scheme.`id`"}
-
-	for k, v := range strSlice {
-		res := AddEscape(v, types.DBTypeMySQL)
-		log.Printf("val_%d: %s, res_%d: %s\n", k, v, k, res)
-		assert.Equal(t, v, res)
-	}
-
-	strSlice1 := []string{"ALTER", "ANALYZE"}
-	for k, v := range strSlice1 {
-		res := AddEscape(v, types.DBTypeMySQL)
-		log.Printf("val_%d: %s, res_%d: %s\n", k, v, k, res)
-		assert.Equal(t, "`"+v+"`", res)
-	}
-}
diff --git a/pkg/datasource/sql/exec/at/insert_executor.go b/pkg/datasource/sql/exec/at/insert_executor.go
index 30f3d60b..12e83c64 100644
--- a/pkg/datasource/sql/exec/at/insert_executor.go
+++ b/pkg/datasource/sql/exec/at/insert_executor.go
@@ -258,9 +258,9 @@ func (i *insertExecutor) containsPK(meta types.TableMeta, parseCtx *types.ParseC
 
 	matchCounter := 0
 	for _, column := range parseCtx.InsertStmt.Columns {
+		cleanName := util.DelEscape(column.Name.O, types.DBTypeMySQL)
 		for _, pkName := range pkColumnNameList {
-			if strings.EqualFold(pkName, column.Name.O) ||
-				strings.EqualFold(pkName, column.Name.L) {
+			if strings.EqualFold(pkName, cleanName) {
 				matchCounter++
 			}
 		}
@@ -271,7 +271,7 @@ func (i *insertExecutor) containsPK(meta types.TableMeta, parseCtx *types.ParseC
 
 // containPK compare column name and primary key name
 func (i *insertExecutor) containPK(columnName string, meta types.TableMeta) bool {
-	newColumnName := DelEscape(columnName, types.DBTypeMySQL)
+	newColumnName := util.DelEscape(columnName, types.DBTypeMySQL)
 	pkColumnNameList := meta.GetPrimaryKeyOnlyName()
 	if len(pkColumnNameList) == 0 {
 		return false
@@ -314,7 +314,7 @@ func (i *insertExecutor) getPkIndex(InsertStmt *ast.InsertStmt, meta types.Table
 		tmpColumnMeta := columnMeta
 		pkIndex++
 		if i.containPK(tmpColumnMeta.ColumnName, meta) {
-			pkIndexMap[DelEscape(tmpColumnMeta.ColumnName, types.DBTypeMySQL)] = pkIndex
+			pkIndexMap[util.DelEscape(tmpColumnMeta.ColumnName, types.DBTypeMySQL)] = pkIndex
 		}
 	}
 
diff --git a/pkg/datasource/sql/exec/at/insert_executor_test.go b/pkg/datasource/sql/exec/at/insert_executor_test.go
index d745dc75..ea57969e 100644
--- a/pkg/datasource/sql/exec/at/insert_executor_test.go
+++ b/pkg/datasource/sql/exec/at/insert_executor_test.go
@@ -217,6 +217,41 @@ func TestMySQLInsertUndoLogBuilder_containsPK(t *testing.T) {
 				Columns: []*ast.ColumnName{{}},
 			},
 		}}, want: false},
+		{name: "test-escaped-backtick-true", fields: fields{}, args: args{meta: types.TableMeta{
+			Indexs: map[string]types.IndexMeta{
+				"id": {
+					IType: types.IndexTypePrimaryKey,
+					Columns: []types.ColumnMeta{{
+						ColumnName: "id",
+					}},
+				},
+			},
+		}, parseCtx: &types.ParseContext{
+			InsertStmt: &ast.InsertStmt{
+				Columns: []*ast.ColumnName{{
+					Name: model.CIStr{O: "`id`", L: "`id`"},
+				}, {
+					Name: model.CIStr{O: "`name`", L: "`name`"},
+				}},
+			},
+		}}, want: true},
+		// Issue #702: mixed escaped and unescaped columns
+		{name: "test-mixed-escape-true", fields: fields{}, args: args{meta: types.TableMeta{
+			Indexs: map[string]types.IndexMeta{
+				"id": {
+					IType: types.IndexTypePrimaryKey,
+					Columns: []types.ColumnMeta{{
+						ColumnName: "id",
+					}},
+				},
+			},
+		}, parseCtx: &types.ParseContext{
+			InsertStmt: &ast.InsertStmt{
+				Columns: []*ast.ColumnName{{
+					Name: model.CIStr{O: "`id`", L: "`id`"},
+				}},
+			},
+		}}, want: true},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/datasource/sql/exec/at/insert_on_update_executor.go b/pkg/datasource/sql/exec/at/insert_on_update_executor.go
index b935ed65..ada89562 100644
--- a/pkg/datasource/sql/exec/at/insert_on_update_executor.go
+++ b/pkg/datasource/sql/exec/at/insert_on_update_executor.go
@@ -217,15 +217,15 @@ func (i *insertOnUpdateExecutor) buildBeforeImageSQLParameters(insertStmt *ast.I
 			return nil, 0, fmt.Errorf("invalid insert row's column size")
 		}
 		for i, col := range insertColumns {
-			columnName := DelEscape(col, types.DBTypeMySQL)
+			columnName := util.DelEscape(col, types.DBTypeMySQL)
 			val := rowColumns[i]
 			rStr, ok := val.(string)
 			if ok && strings.EqualFold(rStr, sqlPlaceholder) {
 				objects := args[placeHolderIndex]
-				parameterMap[columnName] = append(parameterMap[col], objects)
+				parameterMap[columnName] = append(parameterMap[columnName], objects)
 				placeHolderIndex++
 			} else {
-				parameterMap[columnName] = append(parameterMap[col], driver.NamedValue{
+				parameterMap[columnName] = append(parameterMap[columnName], driver.NamedValue{
 					Ordinal: i + 1,
 					Name:    columnName,
 					Value:   val,
@@ -315,7 +315,7 @@ func (i *insertOnUpdateExecutor) buildAfterImageSQL(beforeImage *types.RecordIma
 
 // isPKColumn check the column name to see if it is a primary key column
 func (i *insertOnUpdateExecutor) isPKColumn(columnName string, meta types.TableMeta) bool {
-	newColumnName := DelEscape(columnName, types.DBTypeMySQL)
+	newColumnName := util.DelEscape(columnName, types.DBTypeMySQL)
 	pkColumnNameList := meta.GetPrimaryKeyOnlyName()
 	if len(pkColumnNameList) == 0 {
 		return false
diff --git a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
index 7ed71262..ac031f47 100644
--- a/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
+++ b/pkg/datasource/sql/undo/builder/basic_undo_log_builder.go
@@ -29,9 +29,8 @@ import (
 	"github.com/arana-db/parser/test_driver"
 	gxsort "github.com/dubbogo/gost/sort"
 
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
-
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 // todo the executor should be stateful
@@ -233,8 +232,14 @@ func (b *BasicUndoLogBuilder) buildPKParams(rows []types.RowImage, pkNameList []
 	params := make([]driver.Value, 0)
 	for _, row := range rows {
 		coumnMap := row.GetColumnMap()
+		// Build a normalized map with escaped characters removed
+		normalizedMap := make(map[string]*types.ColumnImage, len(coumnMap))
+		for k, v := range coumnMap {
+			normalizedMap[util.DelEscape(k, types.DBTypeMySQL)] = v
+		}
 		for _, pk := range pkNameList {
-			col := coumnMap[pk]
+			cleanPK := util.DelEscape(pk, types.DBTypeMySQL)
+			col := normalizedMap[cleanPK]
 			if col != nil {
 				params = append(params, col.Value)
 			}
diff --git a/pkg/datasource/sql/undo/builder/mysql_insert_undo_log_builder.go b/pkg/datasource/sql/undo/builder/mysql_insert_undo_log_builder.go
index fbb58103..bb88654b 100644
--- a/pkg/datasource/sql/undo/builder/mysql_insert_undo_log_builder.go
+++ b/pkg/datasource/sql/undo/builder/mysql_insert_undo_log_builder.go
@@ -27,7 +27,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo/executor"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 	"seata.apache.org/seata-go/v2/pkg/util/log"
 )
 
@@ -211,7 +211,7 @@ func (u *MySQLInsertUndoLogBuilder) containsPK(meta types.TableMeta, parseCtx *t
 
 // containPK compare column name and primary key name
 func (u *MySQLInsertUndoLogBuilder) containPK(columnName string, meta types.TableMeta) bool {
-	newColumnName := executor.DelEscape(columnName, types.DBTypeMySQL)
+	newColumnName := util.DelEscape(columnName, types.DBTypeMySQL)
 	pkColumnNameList := meta.GetPrimaryKeyOnlyName()
 	if len(pkColumnNameList) == 0 {
 		return false
@@ -254,7 +254,7 @@ func (u *MySQLInsertUndoLogBuilder) getPkIndex(InsertStmt *ast.InsertStmt, meta
 		tmpColumnMeta := columnMeta
 		pkIndex++
 		if u.containPK(tmpColumnMeta.ColumnName, meta) {
-			pkIndexMap[executor.DelEscape(tmpColumnMeta.ColumnName, types.DBTypeMySQL)] = pkIndex
+			pkIndexMap[util.DelEscape(tmpColumnMeta.ColumnName, types.DBTypeMySQL)] = pkIndex
 		}
 	}
 
diff --git a/pkg/datasource/sql/undo/builder/mysql_insertonduplicate_update_undo_log_builder.go b/pkg/datasource/sql/undo/builder/mysql_insertonduplicate_update_undo_log_builder.go
index 75955eb6..58f43275 100644
--- a/pkg/datasource/sql/undo/builder/mysql_insertonduplicate_update_undo_log_builder.go
+++ b/pkg/datasource/sql/undo/builder/mysql_insertonduplicate_update_undo_log_builder.go
@@ -27,7 +27,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
-	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo/executor"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 	"seata.apache.org/seata-go/v2/pkg/util/log"
 )
 
@@ -316,7 +316,7 @@ func (u *MySQLInsertOnDuplicateUndoLogBuilder) buildImageParameters(insert *ast.
 			return nil, fmt.Errorf("insert row's column size not equal to insert column size")
 		}
 		for i, col := range insertColumns {
-			columnName := strings.ToLower(executor.DelEscape(col, types.DBTypeMySQL))
+			columnName := strings.ToLower(util.DelEscape(col, types.DBTypeMySQL))
 			val := row[i]
 			if str, ok := val.(string); ok && strings.EqualFold(str, SqlPlaceholder) {
 				if placeHolderIndex >= len(args) {
diff --git a/pkg/datasource/sql/undo/executor/executor.go b/pkg/datasource/sql/undo/executor/executor.go
index a4246323..ae4b9912 100644
--- a/pkg/datasource/sql/undo/executor/executor.go
+++ b/pkg/datasource/sql/undo/executor/executor.go
@@ -29,6 +29,7 @@ import (
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/datasource"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 	serr "seata.apache.org/seata-go/v2/pkg/util/errors"
 	"seata.apache.org/seata-go/v2/pkg/util/log"
 )
@@ -181,7 +182,8 @@ func (b *BaseExecutor) parsePkValues(rows []types.RowImage, pkNameList []string)
 
 	for _, row := range rows {
 		for _, column := range row.Columns {
-			columnNameLower := strings.ToLower(column.ColumnName)
+			cleanName := util.DelEscape(column.ColumnName, types.DBTypeMySQL)
+			columnNameLower := strings.ToLower(cleanName)
 			if originalPk, exists := pkLookup[columnNameLower]; exists {
 				if pkValues[originalPk] == nil {
 					pkValues[originalPk] = make([]types.ColumnImage, 0, len(rows))
diff --git a/pkg/datasource/sql/undo/executor/executor_test.go b/pkg/datasource/sql/undo/executor/executor_test.go
index eaaf6b37..5b2a2da9 100644
--- a/pkg/datasource/sql/undo/executor/executor_test.go
+++ b/pkg/datasource/sql/undo/executor/executor_test.go
@@ -609,3 +609,52 @@ func TestParsePkValuesNoMatchingPK(t *testing.T) {
 	assert.NotNil(t, result)
 	assert.Len(t, result, 0)
 }
+
+func TestParsePkValuesEscapedColumnName(t *testing.T) {
+	executor := &BaseExecutor{}
+
+	rows := []types.RowImage{
+		{Columns: []types.ColumnImage{
+			{ColumnName: "`id`", Value: 1},
+			{ColumnName: "`name`", Value: "test"},
+		}},
+		{Columns: []types.ColumnImage{
+			{ColumnName: "`id`", Value: 2},
+			{ColumnName: "`name`", Value: "test2"},
+		}},
+	}
+
+	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"], 2)
+	assert.Equal(t, 1, result["id"][0].Value)
+	assert.Equal(t, 2, result["id"][1].Value)
+}
+
+func TestParsePkValuesEscapedCompositePK(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.Equal(t, 100, result["order_id"][0].Value)
+	assert.Equal(t, 1, result["user_id"][0].Value)
+}
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor.go b/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor.go
index 85a01d5f..011da4c0 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor.go
@@ -25,6 +25,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 type mySQLUndoDeleteExecutor struct {
@@ -53,7 +54,7 @@ func (m *mySQLUndoDeleteExecutor) ExecuteOn(ctx context.Context, dbType types.DB
 
 	for _, row := range beforeImage.Rows {
 		undoValues := make([]interface{}, 0)
-		pkList, err := GetOrderedPkList(beforeImage, row, dbType)
+		pkList, err := util.GetOrderedPkList(beforeImage, row, dbType)
 		if err != nil {
 			return err
 		}
@@ -85,7 +86,7 @@ func (m *mySQLUndoDeleteExecutor) buildUndoSQL(dbType types.DBType) (string, err
 
 	row := rows[0]
 	fields := row.NonPrimaryKeys(row.Columns)
-	pkList, err := GetOrderedPkList(beforeImage, row, dbType)
+	pkList, err := util.GetOrderedPkList(beforeImage, row, dbType)
 	if err != nil {
 		return "", err
 	}
@@ -98,7 +99,7 @@ func (m *mySQLUndoDeleteExecutor) buildUndoSQL(dbType types.DBType) (string, err
 	)
 
 	for key := range fields {
-		insertColumnSlice = append(insertColumnSlice, AddEscape(fields[key].ColumnName, dbType))
+		insertColumnSlice = append(insertColumnSlice, util.AddEscape(fields[key].ColumnName, dbType))
 		insertValueSlice = append(insertValueSlice, "?")
 	}
 
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
index d4f095d2..065cb2c8 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor_test.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_delete_executor_test.go
@@ -29,6 +29,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 func TestNewMySQLUndoDeleteExecutor(t *testing.T) {
@@ -126,7 +127,7 @@ func TestMySQLUndoDeleteExecutor_BuildUndoSQL(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.PrimaryKey.Number() {
@@ -138,7 +139,7 @@ func TestMySQLUndoDeleteExecutor_BuildUndoSQL(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock AddEscape function
-			patches.ApplyFunc(AddEscape, func(columnName string, dbType types.DBType) string {
+			patches.ApplyFunc(util.AddEscape, func(columnName string, dbType types.DBType) string {
 				return "`" + columnName + "`"
 			})
 
@@ -215,7 +216,7 @@ func TestMySQLUndoDeleteExecutor_ExecuteOn(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.PrimaryKey.Number() {
@@ -227,7 +228,7 @@ func TestMySQLUndoDeleteExecutor_ExecuteOn(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock AddEscape function
-			patches.ApplyFunc(AddEscape, func(columnName string, dbType types.DBType) string {
+			patches.ApplyFunc(util.AddEscape, func(columnName string, dbType types.DBType) string {
 				return "`" + columnName + "`"
 			})
 
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor.go b/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor.go
index 92174268..0eea275c 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor.go
@@ -24,6 +24,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 type mySQLUndoInsertExecutor struct {
@@ -90,7 +91,7 @@ func (m *mySQLUndoInsertExecutor) generateDeleteSql(
 	image *types.RecordImage, rows []types.RowImage,
 	dbType types.DBType, sqlUndoLog undo.SQLUndoLog) (string, error) {
 
-	colImages, err := GetOrderedPkList(image, rows[0], dbType)
+	colImages, err := util.GetOrderedPkList(image, rows[0], dbType)
 	if err != nil {
 		return "", err
 	}
@@ -100,7 +101,7 @@ func (m *mySQLUndoInsertExecutor) generateDeleteSql(
 		pkList = append(pkList, colImages[key].ColumnName)
 	}
 
-	whereSql := BuildWhereConditionByPKs(pkList, dbType)
+	whereSql := util.BuildWhereConditionByPKs(pkList, dbType)
 
 	deleteSqlTemplate := "DELETE FROM %s WHERE %s "
 	return fmt.Sprintf(deleteSqlTemplate, sqlUndoLog.TableName, whereSql), nil
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
index 934199d2..0f9be1ff 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor_test.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_insert_executor_test.go
@@ -29,6 +29,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 func TestNewMySQLUndoInsertExecutor(t *testing.T) {
@@ -129,7 +130,7 @@ func TestMySQLUndoInsertExecutor_BuildUndoSQL(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.PrimaryKey.Number() {
@@ -141,7 +142,7 @@ func TestMySQLUndoInsertExecutor_BuildUndoSQL(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock BuildWhereConditionByPKs function
-			patches.ApplyFunc(BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
+			patches.ApplyFunc(util.BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
 				if len(pkNameList) == 1 {
 					return "`" + pkNameList[0] + "` = ?"
 				} else if len(pkNameList) == 2 {
@@ -201,7 +202,7 @@ func TestMySQLUndoInsertExecutor_GenerateDeleteSql(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.PrimaryKey.Number() {
@@ -213,7 +214,7 @@ func TestMySQLUndoInsertExecutor_GenerateDeleteSql(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock BuildWhereConditionByPKs function
-			patches.ApplyFunc(BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
+			patches.ApplyFunc(util.BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
 				return "`" + pkNameList[0] + "` = ?"
 			})
 
@@ -329,6 +330,25 @@ func TestMySQLUndoInsertExecutor_ExecuteOn(t *testing.T) {
 			})
 			defer patches.Reset()
 
+			// Mock GetOrderedPkList function
+			patches.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+				var pkList []types.ColumnImage
+				for _, col := range row.Columns {
+					if col.KeyType == types.PrimaryKey.Number() {
+						pkList = append(pkList, col)
+					}
+				}
+				return pkList, nil
+			})
+
+			// Mock BuildWhereConditionByPKs function
+			patches.ApplyFunc(util.BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
+				if len(pkNameList) == 0 {
+					return ""
+				}
+				return "`" + pkNameList[0] + "` = ?"
+			})
+
 			executor := &mySQLUndoInsertExecutor{
 				BaseExecutor: &BaseExecutor{
 					sqlUndoLog: undo.SQLUndoLog{
diff --git a/pkg/datasource/sql/undo/executor/mysql_undo_update_executor.go b/pkg/datasource/sql/undo/executor/mysql_undo_update_executor.go
index acbd9d95..8b22983b 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_update_executor.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_update_executor.go
@@ -25,6 +25,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 type mySQLUndoUpdateExecutor struct {
@@ -59,7 +60,7 @@ func (m *mySQLUndoUpdateExecutor) ExecuteOn(ctx context.Context, dbType types.DB
 	beforeImage := m.sqlUndoLog.BeforeImage
 	for _, row := range beforeImage.Rows {
 		undoValues := make([]interface{}, 0)
-		pkList, err := GetOrderedPkList(beforeImage, row, dbType)
+		pkList, err := util.GetOrderedPkList(beforeImage, row, dbType)
 		if err != nil {
 			return err
 		}
@@ -95,11 +96,11 @@ func (m *mySQLUndoUpdateExecutor) buildUndoSQL(dbType types.DBType) (string, err
 
 	nonPkFields := row.NonPrimaryKeys(row.Columns)
 	for key := range nonPkFields {
-		updateColumnSlice = append(updateColumnSlice, AddEscape(nonPkFields[key].ColumnName, dbType)+" = ? ")
+		updateColumnSlice = append(updateColumnSlice, util.AddEscape(nonPkFields[key].ColumnName, dbType)+" = ? ")
 	}
 
 	updateColumns = strings.Join(updateColumnSlice, ", ")
-	pkList, err := GetOrderedPkList(beforeImage, row, dbType)
+	pkList, err := util.GetOrderedPkList(beforeImage, row, dbType)
 	if err != nil {
 		return "", err
 	}
@@ -108,7 +109,7 @@ func (m *mySQLUndoUpdateExecutor) buildUndoSQL(dbType types.DBType) (string, err
 		pkNameList = append(pkNameList, pkList[key].ColumnName)
 	}
 
-	whereSql := BuildWhereConditionByPKs(pkNameList, dbType)
+	whereSql := util.BuildWhereConditionByPKs(pkNameList, dbType)
 
 	// UpdateSqlTemplate UPDATE a SET x = ?, y = ?, z = ? WHERE pk1 in (?) pk2 in (?)
 	updateSqlTemplate := "UPDATE %s SET %s WHERE %s "
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
index 33a376bf..5c869636 100644
--- a/pkg/datasource/sql/undo/executor/mysql_undo_update_executor_test.go
+++ b/pkg/datasource/sql/undo/executor/mysql_undo_update_executor_test.go
@@ -29,6 +29,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/undo"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 )
 
 func TestNewMySQLUndoUpdateExecutor(t *testing.T) {
@@ -120,7 +121,7 @@ func TestMySQLUndoUpdateExecutor_BuildUndoSQL(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.IndexTypePrimaryKey {
@@ -132,12 +133,12 @@ func TestMySQLUndoUpdateExecutor_BuildUndoSQL(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock AddEscape function
-			patches.ApplyFunc(AddEscape, func(columnName string, dbType types.DBType) string {
+			patches.ApplyFunc(util.AddEscape, func(columnName string, dbType types.DBType) string {
 				return "`" + columnName + "`"
 			})
 
 			// Mock BuildWhereConditionByPKs function
-			patches.ApplyFunc(BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
+			patches.ApplyFunc(util.BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
 				if len(pkNameList) == 1 {
 					return "`" + pkNameList[0] + "` = ?"
 				} else if len(pkNameList) == 2 {
@@ -237,7 +238,7 @@ func TestMySQLUndoUpdateExecutor_ExecuteOn(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			// Mock GetOrderedPkList function
-			patches := gomonkey.ApplyFunc(GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
+			patches := gomonkey.ApplyFunc(util.GetOrderedPkList, func(image *types.RecordImage, row types.RowImage, dbType types.DBType) ([]types.ColumnImage, error) {
 				var pkList []types.ColumnImage
 				for _, col := range row.Columns {
 					if col.KeyType == types.IndexTypePrimaryKey {
@@ -249,12 +250,12 @@ func TestMySQLUndoUpdateExecutor_ExecuteOn(t *testing.T) {
 			defer patches.Reset()
 
 			// Mock AddEscape function
-			patches.ApplyFunc(AddEscape, func(columnName string, dbType types.DBType) string {
+			patches.ApplyFunc(util.AddEscape, func(columnName string, dbType types.DBType) string {
 				return "`" + columnName + "`"
 			})
 
 			// Mock BuildWhereConditionByPKs function
-			patches.ApplyFunc(BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
+			patches.ApplyFunc(util.BuildWhereConditionByPKs, func(pkNameList []string, dbType types.DBType) string {
 				if len(pkNameList) == 1 {
 					return "`" + pkNameList[0] + "` = ?"
 				}
diff --git a/pkg/datasource/sql/undo/executor/utils.go b/pkg/datasource/sql/undo/executor/utils.go
index 7becdc05..b95dc83c 100644
--- a/pkg/datasource/sql/undo/executor/utils.go
+++ b/pkg/datasource/sql/undo/executor/utils.go
@@ -23,6 +23,7 @@ import (
 
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/datasource"
 	"seata.apache.org/seata-go/v2/pkg/datasource/sql/types"
+	"seata.apache.org/seata-go/v2/pkg/datasource/sql/util"
 	"seata.apache.org/seata-go/v2/pkg/util/log"
 )
 
@@ -74,8 +75,9 @@ func rowListToMap(rows []types.RowImage, primaryKeyList []string) map[string]map
 		var firstUnderline bool
 
 		for _, column := range row.Columns {
+			cleanName := util.DelEscape(column.ColumnName, types.DBTypeMySQL)
 			for i, key := range primaryKeyList {
-				if column.ColumnName == key {
+				if cleanName == key {
 					if firstUnderline && i > 0 {
 						rowKey += "_##$$_"
 					}
@@ -84,7 +86,7 @@ func rowListToMap(rows []types.RowImage, primaryKeyList []string) map[string]map
 					firstUnderline = true
 				}
 			}
-			fieldMap[strings.ToUpper(column.ColumnName)] = column.Value
+			fieldMap[strings.ToUpper(cleanName)] = column.Value
 		}
 		rowMap[rowKey] = fieldMap
 	}
@@ -152,8 +154,14 @@ func buildPKParams(rows []types.RowImage, pkNameList []string) []interface{} {
 	params := make([]interface{}, 0)
 	for _, row := range rows {
 		coumnMap := row.GetColumnMap()
+		// Build a normalized map with escaped characters removed
+		normalizedMap := make(map[string]*types.ColumnImage, len(coumnMap))
+		for k, v := range coumnMap {
+			normalizedMap[util.DelEscape(k, types.DBTypeMySQL)] = v
+		}
 		for _, pk := range pkNameList {
-			col := coumnMap[pk]
+			cleanPK := util.DelEscape(pk, types.DBTypeMySQL)
+			col := normalizedMap[cleanPK]
 			if col != nil {
 				params = append(params, col.Value)
 			}
diff --git a/pkg/datasource/sql/undo/executor/utils_test.go b/pkg/datasource/sql/undo/executor/utils_test.go
index 06780c4c..98a038e6 100644
--- a/pkg/datasource/sql/undo/executor/utils_test.go
+++ b/pkg/datasource/sql/undo/executor/utils_test.go
@@ -516,3 +516,56 @@ func TestBuildPKParams(t *testing.T) {
 		})
 	}
 }
+
+func TestRowListToMap_EscapedColumnNames(t *testing.T) {
+	rows := []types.RowImage{
+		{
+			Columns: []types.ColumnImage{
+				{ColumnName: "`id`", Value: 1},
+				{ColumnName: "`name`", Value: "test"},
+			},
+		},
+		{
+			Columns: []types.ColumnImage{
+				{ColumnName: "`id`", Value: 2},
+				{ColumnName: "`name`", Value: "test2"},
+			},
+		},
+	}
+	primaryKeyList := []string{"id"}
+
+	result := rowListToMap(rows, primaryKeyList)
+
+	assert.Len(t, result, 2)
+	// Verify rows can be found by their PK values
+	row1, exists := result["1"]
+	assert.True(t, exists, "Row with PK=1 should exist")
+	if exists {
+		// After fix, fieldMap key uses cleaned (unescaped) uppercase column name
+		assert.Equal(t, 1, row1["ID"])
+		assert.Equal(t, "test", row1["NAME"])
+	}
+}
+
+func TestBuildPKParams_EscapedColumnNames(t *testing.T) {
+	rows := []types.RowImage{
+		{
+			Columns: []types.ColumnImage{
+				{ColumnName: "`id`", Value: 1},
+				{ColumnName: "`name`", Value: "test"},
+			},
+		},
+		{
+			Columns: []types.ColumnImage{
+				{ColumnName: "`id`", Value: 2},
+				{ColumnName: "`name`", Value: "test2"},
+			},
+		},
+	}
+	pkNameList := []string{"id"}
+
+	result := buildPKParams(rows, pkNameList)
+
+	assert.Len(t, result, 2)
+	assert.Equal(t, []interface{}{1, 2}, result)
+}
diff --git a/pkg/datasource/sql/undo/executor/sql.go b/pkg/datasource/sql/util/escape.go
similarity index 96%
rename from pkg/datasource/sql/undo/executor/sql.go
rename to pkg/datasource/sql/util/escape.go
index 5c7e540a..1970477a 100644
--- a/pkg/datasource/sql/undo/executor/sql.go
+++ b/pkg/datasource/sql/util/escape.go
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package executor
+package util
 
 import (
 	"database/sql"
@@ -92,7 +92,7 @@ func addEscape(colName string, dbType types.DBType, escape string) string {
 		return colName
 	}
 
-	if !checkEscape(colName, dbType) {
+	if !CheckEscape(colName, dbType) {
 		return colName
 	}
 
@@ -149,8 +149,8 @@ func addEscape(colName string, dbType types.DBType, escape string) string {
 	return string(buf)
 }
 
-// checkEscape check whether given field or table name use keywords. the method has database special logic.
-func checkEscape(colName string, dbType types.DBType) bool {
+// CheckEscape check whether given field or table name use keywords. the method has database special logic.
+func CheckEscape(colName string, dbType types.DBType) bool {
 	switch dbType {
 	case types.DBTypeMySQL:
 		if _, ok := types.GetMysqlKeyWord()[strings.ToUpper(colName)]; ok {
diff --git a/pkg/datasource/sql/undo/executor/sql_test.go b/pkg/datasource/sql/util/escape_test.go
similarity index 95%
rename from pkg/datasource/sql/undo/executor/sql_test.go
rename to pkg/datasource/sql/util/escape_test.go
index fa1b4a5b..4571f871 100644
--- a/pkg/datasource/sql/undo/executor/sql_test.go
+++ b/pkg/datasource/sql/util/escape_test.go
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-package executor
+package util
 
 import (
 	"log"
@@ -139,31 +139,31 @@ func TestAddEscapeSQLServer(t *testing.T) {
 func TestCheckEscapeMySQLKeywords(t *testing.T) {
 	keywords := []string{"SELECT", "INSERT", "UPDATE", "DELETE", "ALTER", "CREATE", "DROP", "TABLE"}
 	for _, keyword := range keywords {
-		result := checkEscape(keyword, types.DBTypeMySQL)
+		result := CheckEscape(keyword, types.DBTypeMySQL)
 		assert.True(t, result, "Expected %s to be a MySQL keyword", keyword)
 
 		lowerKeyword := keyword
-		result = checkEscape(lowerKeyword, types.DBTypeMySQL)
+		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)
+	result := CheckEscape("my_column", types.DBTypeMySQL)
 	assert.False(t, result)
 
-	result = checkEscape("user_name", types.DBTypeMySQL)
+	result = CheckEscape("user_name", types.DBTypeMySQL)
 	assert.False(t, result)
 }
 
 func TestCheckEscapeNonMySQL(t *testing.T) {
-	result := checkEscape("anything", types.DBTypePostgreSQL)
+	result := CheckEscape("anything", types.DBTypePostgreSQL)
 	assert.True(t, result)
 
-	result = checkEscape("anything", types.DBTypeOracle)
+	result = CheckEscape("anything", types.DBTypeOracle)
 	assert.True(t, result)
 
-	result = checkEscape("anything", types.DBTypeSQLServer)
+	result = CheckEscape("anything", types.DBTypeSQLServer)
 	assert.True(t, result)
 }
 
diff --git a/pkg/datasource/sql/util/lockkey.go b/pkg/datasource/sql/util/lockkey.go
index 9ce60985..b7e35d27 100644
--- a/pkg/datasource/sql/util/lockkey.go
+++ b/pkg/datasource/sql/util/lockkey.go
@@ -47,7 +47,8 @@ func BuildLockKey(records *types.RecordImage, meta types.TableMeta) string {
 	columns := make([]ColMapItem, 0, len(keys))
 	if len(records.Rows) > 0 {
 		for colIdx, column := range records.Rows[0].Columns {
-			if pkIdx, ok := keyIndexMap[column.ColumnName]; ok {
+			cleanName := DelEscape(column.ColumnName, types.DBTypeMySQL)
+			if pkIdx, ok := keyIndexMap[cleanName]; ok {
 				columns = append(columns, ColMapItem{pkIndex: pkIdx, colIndex: colIdx})
 			}
 		}
diff --git a/pkg/datasource/sql/util/lockkey_test.go b/pkg/datasource/sql/util/lockkey_test.go
index e09db49d..c1656307 100644
--- a/pkg/datasource/sql/util/lockkey_test.go
+++ b/pkg/datasource/sql/util/lockkey_test.go
@@ -541,3 +541,71 @@ func TestBuildLockKey_LargeNumberOfRows(t *testing.T) {
 	assert.Contains(t, lockKey, "1,2,3")
 	assert.Contains(t, lockKey, ",99,100")
 }
+
+func TestBuildLockKey_EscapedColumnNames(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_order",
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "id"},
+				},
+			},
+		},
+	}
+
+	recordImage := &types.RecordImage{
+		TableName: "t_order",
+		SQLType:   types.SQLTypeInsert,
+		Rows: []types.RowImage{
+			{
+				Columns: []types.ColumnImage{
+					{ColumnName: "`id`", Value: 1, KeyType: types.IndexTypePrimaryKey},
+					{ColumnName: "`name`", Value: "order1"},
+				},
+			},
+			{
+				Columns: []types.ColumnImage{
+					{ColumnName: "`id`", Value: 2, KeyType: types.IndexTypePrimaryKey},
+					{ColumnName: "`name`", Value: "order2"},
+				},
+			},
+		},
+	}
+
+	lockKey := BuildLockKey(recordImage, tableMeta)
+	assert.Equal(t, "T_ORDER:1,2", lockKey)
+}
+
+func TestBuildLockKey_EscapedCompositeKey(t *testing.T) {
+	tableMeta := types.TableMeta{
+		TableName: "t_order",
+		Indexs: map[string]types.IndexMeta{
+			"PRIMARY": {
+				IType: types.IndexTypePrimaryKey,
+				Columns: []types.ColumnMeta{
+					{ColumnName: "order_id"},
+					{ColumnName: "user_id"},
+				},
+			},
+		},
+	}
+
+	recordImage := &types.RecordImage{
+		TableName: "t_order",
+		SQLType:   types.SQLTypeInsert,
+		Rows: []types.RowImage{
+			{
+				Columns: []types.ColumnImage{
+					{ColumnName: "`order_id`", Value: 100, KeyType: types.IndexTypePrimaryKey},
+					{ColumnName: "`user_id`", Value: 1, KeyType: types.IndexTypePrimaryKey},
+					{ColumnName: "`amount`", Value: 99.99},
+				},
+			},
+		},
+	}
+
+	lockKey := BuildLockKey(recordImage, tableMeta)
+	assert.Equal(t, "T_ORDER:100_1", lockKey)
+}


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