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 <[email protected]>
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 <[email protected]>
---
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: [email protected]
For additional commands, e-mail: [email protected]