thunguo opened a new issue, #1121:
URL: https://github.com/apache/incubator-seata-go/issues/1121

   ## Describe
   
   In AT mode, `REPLACE INTO` has a parser and `ReplaceIntoExecutor` enum + 
unit tests, but there is no executor and no undo-log builder wired up for it. 
As a result, a `REPLACE INTO` statement is silently executed as a plain 
`INSERT`, and on rollback the original (overwritten) row cannot be restored, 
causing data loss / dirty data.
   
   > **Note:** Seata Java rejects `REPLACE INTO` by throwing an exception 
precisely to avoid this. seata-go instead degrades silently, which is more 
dangerous.
   
   ## Root cause analysis
   
   1. **Parse layer** — `parser_factory.go:55-62`: for `REPLACE INTO`, 
`stmt.IsReplace == true` only changes `ExecutorType` to `ReplaceIntoExecutor`; 
`SQLType` stays `SQLTypeInsert` (parse test `parse_factory_test.go:45-54` 
confirms `sqlType: SQLTypeInsert`).
   2. **Execution layer** — `at_executor.go:71-86`: dispatch is keyed on 
`SQLType`, not `ExecutorType`. So `SQLTypeInsert` routes to 
`newInsertExecutor`. The `ReplaceIntoExecutor` enum is never used for dispatch 
— `REPLACE INTO` runs as a normal `INSERT`.
   3. **Undo image layer:**
      - `InsertExecutor`'s own path: `insert_executor.go:124-135` `beforeImage` 
returns an empty record image; `afterImage` only captures the newly inserted 
rows.
      - Hook path: `undo_log_hook.go:55` resolves the builder by `ExecutorType` 
→ `GetUndologBuilder(ReplaceIntoExecutor)`. `plugin.go:55-59` only registers 
Delete/Insert/InsertOnDuplicate/Multi/Update builders → no builder for 
`ReplaceIntoExecutor` → returns `nil` → no-op.
   
   ## Dirty-data scenario
   
   MySQL `REPLACE INTO` semantics on a primary/unique key conflict = DELETE old 
row + INSERT new row.
   
   - **No conflict:** equivalent to `INSERT`; rollback deletes the new row → 
correct.
   - **On conflict:** the deleted old row is NOT captured (before image is 
empty). On rollback the insert undo executor only deletes the new row, and the 
overwritten original row is permanently lost and cannot be restored → dirty 
data / data loss.
   
   In other words: it does generate INSERT-semantics undo data (empty before 
image + new-row after image), which is only correct for the no-conflict case; 
the implicit DELETE of the conflicting row is never captured.
   
   ## Steps to reproduce
   
   1. Table with a unique/primary key, e.g. row `(id=1, name='old')` already 
exists.
   2. Inside a global transaction, execute `REPLACE INTO t (id, name) VALUES 
(1, 'new')`.
   3. Force the global transaction to roll back.
   4. **Expected:** row restored to `(id=1, name='old')`.
   5. **Actual:** the new row is deleted and the original `(id=1, name='old')` 
is gone → data loss.
   
   ## Expected behavior
   
   At minimum, `REPLACE INTO` in AT mode should NOT silently lose data on 
rollback.
   
   ## Additional context
   
   Multi-statement `REPLACE` is incidentally safe — `multi_executor.go:95,129` 
hits the default branch and returns `not support multi sql`. Only a single 
`REPLACE INTO` statement triggers the dangerous path.


-- 
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