thunguo commented on code in PR #1141:
URL: 
https://github.com/apache/incubator-seata-go/pull/1141#discussion_r3851256218


##########
pkg/datasource/sql/undo/executor/mysql_undo_insert_executor.go:
##########
@@ -43,13 +43,19 @@ func newMySQLUndoInsertExecutor(sqlUndoLog undo.SQLUndoLog) 
*mySQLUndoInsertExec
 // ExecuteOn execute insert undo logic
 func (m *mySQLUndoInsertExecutor) ExecuteOn(ctx context.Context, dbType 
types.DBType, conn *sql.Conn) error {
        m.BaseExecutor.dbType = dbType
-
-       if err := m.BaseExecutor.ExecuteOn(ctx, dbType, conn); err != nil {
+       ok, err := m.BaseExecutor.dataValidationAndGoOn(ctx, conn)

Review Comment:
   When `dataValidationAndGoOn` is enabled here, standard `INSERT` operations 
involving `DECIMAL` types fail to roll back correctly. The Phase 1 image scans 
`DECIMAL` values ​​as `float64` based on table metadata, whereas 
`queryCurrentRecords` scans them as strings (e.g., `"13.370000"`) based on 
`go-sql-driver`'s `ColumnTypes`, leading to a mismatch (e.g., `float64(13.37)` 
vs. `"13.370000"`). Since the current `DeepEqual` logic does not perform 
string-to-numeric conversion, it incorrectly triggers a `SQLUndoDirtyError`. It 
is recommended to standardize the scanning/normalization of `DECIMAL` values 
​​based on the undo image's `JDBCType` and to add test cases specifically for 
`INSERT` rollbacks against a real MySQL instance; however, one should avoid 
parsing all numeric strings during general comparisons to prevent `VARCHAR` 
values ​​from being misidentified.



##########
pkg/datasource/sql/undo/parser/parser_protobuf.go:
##########
@@ -244,6 +246,55 @@ func convertAnyToInterface(anyValue *any.Any) 
(interface{}, error) {
        return value, nil
 }
 
+func convertAnyToColumnValue(anyValue *any.Any, columnType types.JDBCType) 
(interface{}, error) {
+       bytesValue := &wrappers.BytesValue{}
+       if err := anypb.UnmarshalTo(anyValue, bytesValue, 
proto.UnmarshalOptions{}); err != nil {
+               return nil, err
+       }
+       if bytes.Equal(bytesValue.Value, []byte("null")) {
+               return nil, nil
+       }
+
+       switch columnType {
+       case types.JDBCTypeReal, types.JDBCTypeDecimal, types.JDBCTypeDouble,
+               types.JDBCTypeTinyInt, types.JDBCTypeSmallInt, 
types.JDBCTypeInteger, types.JDBCTypeBigInt:
+               decoder := json.NewDecoder(bytes.NewReader(bytesValue.Value))
+               decoder.UseNumber()
+               var value json.Number
+               if err := decoder.Decode(&value); err != nil {
+                       return nil, err
+               }
+               switch columnType {
+               case types.JDBCTypeReal:
+                       parsed, err := strconv.ParseFloat(value.String(), 32)
+                       return float32(parsed), err
+               case types.JDBCTypeDecimal, types.JDBCTypeDouble:
+                       return strconv.ParseFloat(value.String(), 64)
+               case types.JDBCTypeTinyInt:
+                       parsed, err := strconv.ParseInt(value.String(), 10, 8)

Review Comment:
   Here, `ParseInt` is called based on JDBC signed width; however, MySQL's 
unsigned types—`TINYINT UNSIGNED` (255), `SMALLINT UNSIGNED` (65535), and `INT 
UNSIGNED` (4294967295)—cannot be parsed as 8-bit, 16-bit, or 32-bit signed 
integers, respectively. The JSON undo log decoding fails outright, while the 
protobuf path silently skips the column (via a `continue` statement), 
potentially resulting in incomplete undo SQL. Since `ColumnImage` currently 
lacks the unsigned attribute, the type cannot be narrowed based solely on 
`JDBCType`. It is recommended to either retain the type as `int64`/`uint64` or 
pass the unsigned metadata into the decoding process, while also prohibiting 
the silent skipping of columns upon conversion failure. Additionally, please 
add JSON/protobuf round-trip tests covering the boundary values ​​for these 
three unsigned types.



##########
pkg/datasource/sql/exec/at/insert_executor.go:
##########
@@ -385,57 +485,146 @@ func (i *insertExecutor) buildAfterImageSQL(ctx 
context.Context) (string, []driv
        for _, column := range i.parserCtx.InsertStmt.Columns {
                insertColumns = append(insertColumns, column.Name.O)
        }
-       sb.WriteString("SELECT " + strings.Join(i.getNeedColumns(meta, 
insertColumns, dbType), ", "))
+       selectColumns, err := buildImageSelectColumns(meta, insertColumns, 
dbType, undo.UndoConfig.OnlyCareUpdateColumns)
+       if err != nil {
+               return "", nil, err
+       }
+       sb.WriteString("SELECT " + strings.Join(selectColumns, ", "))
        suffix.WriteString(" FROM " + tableName)
        whereSQL := i.buildWhereConditionByPKs(pkColumnNameList, rowSize, 
dbType, maxInSize)
        suffix.WriteString(" WHERE " + whereSQL + " ")
        sb.WriteString(suffix.String())
+       i.resolvedPKRows = pkRowImages
        return sb.String(), i.buildPKParams(pkRowImages, pkColumnNameList, 
dbType), nil
 }
 
-func (i *insertExecutor) getPkValues(ctx context.Context, execCtx 
*types.ExecContext, parseCtx *types.ParseContext, meta types.TableMeta) 
(map[string][]interface{}, error) {
-       pkColumnNameList := meta.GetPrimaryKeyOnlyName()
-       pkValuesMap := make(map[string][]interface{})
-       var err error
-       // when there is only one pk in the table
-       if len(pkColumnNameList) == 1 {
-               if i.containsPK(meta, parseCtx) {
-                       // the insert sql contain pk value
-                       pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               } else if containsColumns(parseCtx) {
-                       // the insert table pk auto generated
-                       pkValuesMap, err = i.getPkValuesByAuto(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               } else {
-                       pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               }
-       } else {
-               // when there is multiple pk in the table
-               // 1,all pk columns are filled value.
-               // 2,the auto increment pk column value is null, and other pk 
value are not null.
-               pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
+func (i *insertExecutor) getPkValues(execCtx *types.ExecContext, meta 
types.TableMeta) (map[string][]interface{}, error) {
+       if i.keyPlan == nil {
+               plan, err := i.buildInsertKeyPlan(&meta)
                if err != nil {
                        return nil, err
                }
-               for _, columnName := range pkColumnNameList {
-                       if _, ok := pkValuesMap[columnName]; !ok {
-                               curPkValuesMap, err := i.getPkValuesByAuto(ctx, 
execCtx)
-                               if err != nil {
-                                       return nil, err
+               i.keyPlan = plan
+       }
+       return i.resolveInsertKeyPlan(execCtx)
+}
+
+func (i *insertExecutor) buildInsertKeyPlan(meta *types.TableMeta) 
(*insertKeyPlan, error) {
+       if meta == nil || !i.isAstStmtValid() {
+               return nil, fmt.Errorf("invalid insert metadata or statement")
+       }
+       stmt := i.parserCtx.InsertStmt
+       if stmt.Select != nil || len(stmt.Lists) == 0 {
+               return nil, fmt.Errorf("insert source other than VALUES is 
unsupported")
+       }
+       if stmt.IgnoreErr && len(stmt.Lists) > 1 {
+               return nil, fmt.Errorf("multi-values INSERT IGNORE is 
unsupported because successful rows cannot be determined")
+       }
+
+       pkNames := meta.GetPrimaryKeyOnlyName()
+       if len(pkNames) == 0 {
+               return nil, fmt.Errorf("pk columnName size is zero")
+       }
+       pkValues, err := i.parsePkValuesFromStatement(stmt, *meta, 
i.execContext.NamedValues)
+       if err != nil {
+               return nil, err
+       }
+
+       plan := &insertKeyPlan{rowCount: len(stmt.Lists), pkValues: 
make(map[string][]interface{})}
+       pkMeta := meta.GetPrimaryKeyMap()
+       for _, pkName := range pkNames {
+               columnMeta, ok := pkMeta[pkName]
+               if !ok {
+                       return nil, fmt.Errorf("primary key metadata not found 
for %s", pkName)
+               }
+               values, present := pkValues[pkName]
+               generatedCount := 0
+               if present {
+                       if len(values) != plan.rowCount {
+                               return nil, fmt.Errorf("insert primary key %s 
has %d values, want %d", pkName, len(values), plan.rowCount)
+                       }
+                       for _, value := range values {
+                               switch value.(type) {
+                               case nil, *ast.DefaultExpr, ast.DefaultExpr:

Review Comment:
   By default, MySQL treats a value of `0` in an `AUTO_INCREMENT` column as a 
signal to generate a new value; the literal `0` is stored only when the session 
has `NO_AUTO_VALUE_ON_ZERO` enabled. Since only `nil` or `DEFAULT` are 
classified here as "generated," an `INSERT ... VALUES (0, ...)` statement 
causes the system to look up the "after-image" using `id=0`, whereas the actual 
inserted row uses a newly generated ID; this ultimately results in an 
after-image validation error after the business SQL has executed. It is 
recommended to read `@@SESSION.sql_mode` for the current connection and—when 
that mode is disabled—route literal or bound zeros through the `LastInsertId` 
path, while also ensuring test coverage for both `sql_mode` settings and 
prepared statement parameters.



##########
pkg/datasource/sql/exec/at/insert_executor.go:
##########
@@ -540,94 +715,54 @@ func (i *insertExecutor) matchPKColumnName(columnName 
string, meta types.TableMe
 // return the primary key and values<key:primary key,value:primary key 
values></key:primary>
 func (i *insertExecutor) parsePkValuesFromStatement(insertStmt 
*ast.InsertStmt, meta types.TableMeta, nameValues []driver.NamedValue) 
(map[string][]interface{}, error) {
        if insertStmt == nil {
-               return nil, nil
-       }
-       pkIndexMap := i.getPkIndex(insertStmt, meta)
-       if pkIndexMap == nil || len(pkIndexMap) == 0 {
-               return nil, fmt.Errorf("pkIndex is not found")
+               return nil, fmt.Errorf("insert statement is nil")
        }
-       var pkIndexArray []int
-       for _, val := range pkIndexMap {
-               tmpVal := val
-               pkIndexArray = append(pkIndexArray, tmpVal)
+       if len(insertStmt.Lists) == 0 {
+               return nil, fmt.Errorf("insert VALUES list is empty")
        }
 
-       if insertStmt == nil || len(insertStmt.Lists) == 0 {
-               return nil, fmt.Errorf("parCtx is nil, perhaps InsertStmt is 
empty")
+       expectedColumns := len(insertStmt.Columns)

Review Comment:
   Both `INSERT INTO t () VALUES ()` and `INSERT INTO t VALUES ()` are 
represented by the current parser as a `VALUES` list with a length of zero. 
Since the expected width defaults to the total number of table columns when no 
explicit column list is provided, a row-width error inevitably occurs. However, 
this is valid MySQL syntax for "using default values ​​for all columns"—often 
used to generate only an auto-incrementing primary key. It is recommended to 
treat an empty `VALUES` row as all `DEFAULT`s and construct the primary key 
plan using `LastInsertId`, while also adding test cases for these two syntaxes.



##########
pkg/datasource/sql/exec/at/insert_executor.go:
##########
@@ -385,57 +485,146 @@ func (i *insertExecutor) buildAfterImageSQL(ctx 
context.Context) (string, []driv
        for _, column := range i.parserCtx.InsertStmt.Columns {
                insertColumns = append(insertColumns, column.Name.O)
        }
-       sb.WriteString("SELECT " + strings.Join(i.getNeedColumns(meta, 
insertColumns, dbType), ", "))
+       selectColumns, err := buildImageSelectColumns(meta, insertColumns, 
dbType, undo.UndoConfig.OnlyCareUpdateColumns)
+       if err != nil {
+               return "", nil, err
+       }
+       sb.WriteString("SELECT " + strings.Join(selectColumns, ", "))
        suffix.WriteString(" FROM " + tableName)
        whereSQL := i.buildWhereConditionByPKs(pkColumnNameList, rowSize, 
dbType, maxInSize)
        suffix.WriteString(" WHERE " + whereSQL + " ")
        sb.WriteString(suffix.String())
+       i.resolvedPKRows = pkRowImages
        return sb.String(), i.buildPKParams(pkRowImages, pkColumnNameList, 
dbType), nil
 }
 
-func (i *insertExecutor) getPkValues(ctx context.Context, execCtx 
*types.ExecContext, parseCtx *types.ParseContext, meta types.TableMeta) 
(map[string][]interface{}, error) {
-       pkColumnNameList := meta.GetPrimaryKeyOnlyName()
-       pkValuesMap := make(map[string][]interface{})
-       var err error
-       // when there is only one pk in the table
-       if len(pkColumnNameList) == 1 {
-               if i.containsPK(meta, parseCtx) {
-                       // the insert sql contain pk value
-                       pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               } else if containsColumns(parseCtx) {
-                       // the insert table pk auto generated
-                       pkValuesMap, err = i.getPkValuesByAuto(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               } else {
-                       pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
-                       if err != nil {
-                               return nil, err
-                       }
-               }
-       } else {
-               // when there is multiple pk in the table
-               // 1,all pk columns are filled value.
-               // 2,the auto increment pk column value is null, and other pk 
value are not null.
-               pkValuesMap, err = i.getPkValuesByColumn(ctx, execCtx)
+func (i *insertExecutor) getPkValues(execCtx *types.ExecContext, meta 
types.TableMeta) (map[string][]interface{}, error) {
+       if i.keyPlan == nil {
+               plan, err := i.buildInsertKeyPlan(&meta)
                if err != nil {
                        return nil, err
                }
-               for _, columnName := range pkColumnNameList {
-                       if _, ok := pkValuesMap[columnName]; !ok {
-                               curPkValuesMap, err := i.getPkValuesByAuto(ctx, 
execCtx)
-                               if err != nil {
-                                       return nil, err
+               i.keyPlan = plan
+       }
+       return i.resolveInsertKeyPlan(execCtx)
+}
+
+func (i *insertExecutor) buildInsertKeyPlan(meta *types.TableMeta) 
(*insertKeyPlan, error) {
+       if meta == nil || !i.isAstStmtValid() {
+               return nil, fmt.Errorf("invalid insert metadata or statement")
+       }
+       stmt := i.parserCtx.InsertStmt
+       if stmt.Select != nil || len(stmt.Lists) == 0 {
+               return nil, fmt.Errorf("insert source other than VALUES is 
unsupported")
+       }
+       if stmt.IgnoreErr && len(stmt.Lists) > 1 {
+               return nil, fmt.Errorf("multi-values INSERT IGNORE is 
unsupported because successful rows cannot be determined")
+       }
+
+       pkNames := meta.GetPrimaryKeyOnlyName()
+       if len(pkNames) == 0 {
+               return nil, fmt.Errorf("pk columnName size is zero")
+       }
+       pkValues, err := i.parsePkValuesFromStatement(stmt, *meta, 
i.execContext.NamedValues)
+       if err != nil {
+               return nil, err
+       }
+
+       plan := &insertKeyPlan{rowCount: len(stmt.Lists), pkValues: 
make(map[string][]interface{})}
+       pkMeta := meta.GetPrimaryKeyMap()
+       for _, pkName := range pkNames {
+               columnMeta, ok := pkMeta[pkName]
+               if !ok {
+                       return nil, fmt.Errorf("primary key metadata not found 
for %s", pkName)
+               }
+               values, present := pkValues[pkName]
+               generatedCount := 0
+               if present {
+                       if len(values) != plan.rowCount {
+                               return nil, fmt.Errorf("insert primary key %s 
has %d values, want %d", pkName, len(values), plan.rowCount)
+                       }
+                       for _, value := range values {
+                               switch value.(type) {
+                               case nil, *ast.DefaultExpr, ast.DefaultExpr:

Review Comment:
   https://dev.mysql.com/doc/refman/8.0/en/example-auto-increment.html



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to