You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug] MIN_DELTA stream returns the wrong value after UPDATE followed by DELETE #65800
For a Merge-on-Write (MOW) unique-key table, a MIN_DELTA stream incorrectly returns the value from the DELETE operation when an update is followed by a delete on the same row.
MIN_DELTA should merge the sequence below into one DELETE operation using the original value from UPDATE_BEFORE.
Reproduction
CREATETABLEmow_min_delta_bug (
id BIGINT,
v1 INT
) ENGINE=OLAP
UNIQUE KEY(id)
DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES (
"replication_num"="1",
"enable_unique_key_merge_on_write"="true",
"binlog.enable"="true",
"binlog.format"="ROW",
"binlog.need_historical_value"="true"
);
INSERT INTO mow_min_delta_bug VALUES (1, 10);
CREATE STREAM mow_min_delta_bug_stream
ON TABLE mow_min_delta_bug
PROPERTIES (
"type"="min_delta",
"show_initial_rows"="false"
);
INSERT INTO mow_min_delta_bug VALUES (1, 11);
DELETEFROM mow_min_delta_bug WHERE id =1;
SELECT id, v1, __DORIS_STREAM_CHANGE_TYPE_COL__
FROM mow_min_delta_bug_stream;
The row-level operations are:
UPDATE_BEFORE(10), UPDATE_AFTER(11), DELETE(11)
Actual behavior
The stream returns:
id v1 change_type
1 11 DELETE
Expected behavior
The stream should return:
id v1 change_type
1 10 DELETE
When UPDATE and DELETE are merged into a single DELETE, the delete row should preserve the old value from UPDATE_BEFORE, not the updated value from UPDATE_AFTER or DELETE.
Breakwater-GitHub-Analysis-Slot: slot_7ebce944bc43
This content is generated by AI for reference only.
Initial code-level analysis:
The report looks valid against current upstream/master (91a12e13170268812c0002e653b408eca002ca3f). The MIN_DELTA transition itself already classifies UPDATE -> DELETE as a single DELETE in be/src/storage/iterator/binlog_block_reader_utils.h, so the likely problem is not the transition matrix.
The suspicious part is row materialization in BlockReader::_min_delta_next_block() (be/src/storage/iterator/block_reader.cpp). In the ResultType::DELETE branch, the output operation is correctly changed to STREAM_CHANGE_DELETE, but all non-op payload columns are copied from _stored_data_columns[idx] at group_size - 1, i.e. from the last row in the same-key change group. For the sequence in this issue, that last row is the physical DELETE row after the update, so the emitted value becomes 11.
For MIN_DELTA semantics, a folded DELETE should describe removal of the row that existed at the stream window start. In the UPDATE -> DELETE case, that value is available from the first UPDATE row's __BEFORE__ columns, not from the later DELETE row. This also matches the FE time-travel/table-stream comments in BindRelation.java, which assume UPDATE_BEFORE and DELETE rows carry the before image.
One more useful signal: current regression coverage appears to encode the same wrong behavior. regression-test/suites/table_stream_p0/test_min_delta_stream.groovy has an UPDATE then DELETE case and currently asserts the updated value (101) for the folded DELETE, while its comment says the value should use the before snapshot. That test should be corrected to assert the pre-window value.
Suggested next steps:
Fix the ResultType::DELETE materialization path in BlockReader::_min_delta_next_block() so value columns for an UPDATE -> DELETE group are read from the first row's before-image columns. Keep the stream sequence/binlog metadata behavior reviewed separately, because consumers may still expect the folded event to advance to the latest operation in the group.
Update/add regression coverage with the exact case from this issue: initial (1, 10), then update to 11, then delete, and assert DELETE(10).
Review the adjacent folded-delete cases (DELETE -> DELETE, multi-update followed by delete, cross-rowset same-key grouping) so pure delete still emits the deleted row value and update-delete emits the original pre-window value.
No runtime profile is needed for this report. The only missing public metadata is the exact Doris build/commit where the reporter reproduced it, but the issue is already reproducible from current upstream code inspection.
Activity
zclllyybb commentedon Jul 20, 2026
Breakwater-GitHub-Analysis-Slot: slot_7ebce944bc43
This content is generated by AI for reference only.
Initial code-level analysis:
The report looks valid against current
upstream/master(91a12e13170268812c0002e653b408eca002ca3f). TheMIN_DELTAtransition itself already classifiesUPDATE -> DELETEas a singleDELETEinbe/src/storage/iterator/binlog_block_reader_utils.h, so the likely problem is not the transition matrix.The suspicious part is row materialization in
BlockReader::_min_delta_next_block()(be/src/storage/iterator/block_reader.cpp). In theResultType::DELETEbranch, the output operation is correctly changed toSTREAM_CHANGE_DELETE, but all non-op payload columns are copied from_stored_data_columns[idx]atgroup_size - 1, i.e. from the last row in the same-key change group. For the sequence in this issue, that last row is the physical DELETE row after the update, so the emitted value becomes11.For
MIN_DELTAsemantics, a foldedDELETEshould describe removal of the row that existed at the stream window start. In theUPDATE -> DELETEcase, that value is available from the first UPDATE row's__BEFORE__columns, not from the later DELETE row. This also matches the FE time-travel/table-stream comments inBindRelation.java, which assumeUPDATE_BEFOREandDELETErows carry the before image.One more useful signal: current regression coverage appears to encode the same wrong behavior.
regression-test/suites/table_stream_p0/test_min_delta_stream.groovyhas anUPDATE then DELETEcase and currently asserts the updated value (101) for the folded DELETE, while its comment says the value should use the before snapshot. That test should be corrected to assert the pre-window value.Suggested next steps:
ResultType::DELETEmaterialization path inBlockReader::_min_delta_next_block()so value columns for anUPDATE -> DELETEgroup are read from the first row's before-image columns. Keep the stream sequence/binlog metadata behavior reviewed separately, because consumers may still expect the folded event to advance to the latest operation in the group.(1, 10), then update to11, then delete, and assertDELETE(10).DELETE -> DELETE, multi-update followed by delete, cross-rowset same-key grouping) so pure delete still emits the deleted row value and update-delete emits the original pre-window value.No runtime profile is needed for this report. The only missing public metadata is the exact Doris build/commit where the reporter reproduced it, but the issue is already reproducible from current upstream code inspection.