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]