This is an automated email from the ASF dual-hosted git repository.
Yukang-Lian pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new bd6673b756d [Enhancement](row binlog) Support flexible partial updates
(#66899)
bd6673b756d is described below
commit bd6673b756d3672e1deebf147aa6e262faaaa18a
Author: Jamie <[email protected]>
AuthorDate: Tue Sep 1 15:33:22 2026 +0800
[Enhancement](row binlog) Support flexible partial updates (#66899)
### What problem does this PR solve?
Issue Number: N/A
Related PR: #65810
Problem Summary:
Row Binlog supports fixed-column partial updates, but did not support
flexible partial updates (`UPDATE_FLEXIBLE_COLUMNS`). Flexible updates
carry a per-row skip bitmap and may merge rows before writing, so Row
Binlog must reconstruct complete rows while keeping BEFORE/AFTER values,
operations, row counts, and LSNs aligned.
This PR:
- Routes flexible partial updates through the existing
`MowRowBinlogDeriveStage` transform chain.
- Reuses the existing flexible-update aggregation, a read-only MOW key
probe, and historical-row read plans to materialize complete AFTER rows
and optional complete BEFORE rows.
- Keeps per-row LSN and same-batch DELETE-to-INSERT state aligned
through aggregation. For DELETE-to-INSERT, omitted AFTER cells use their
default or null value, while a live historical row remains the UPDATE
BEFORE image.
- Preserves Base Rowset and Row Binlog `(segment_id, row_id)` alignment
for sequence losers; the synchronized delete bitmap makes those rows
logically invisible.
- Uses a writer-private copy-on-write Block, so mutated columns detach
without changing the Base Rowset writer's input.
- Keeps the existing Row Binlog on-disk format, query protocol, and
APPEND / UPDATE / DELETE semantics.
The implementation is shared by local and Cloud storage. It does not
change FE syntax, Table Stream metadata, Stream Offset handling, or
protobuf definitions.
Scope note: this PR does not lift the existing flexible partial-update
multi-segment load restriction and does not add a zero-row Segment
format.
---
be/src/storage/partial_update_info.cpp | 110 +++++-
be/src/storage/partial_update_info.h | 24 +-
.../storage/segment/historical_row_retriever.cpp | 135 +++++++
be/src/storage/segment/historical_row_retriever.h | 8 +
be/src/storage/transform/row_binlog_derive.cpp | 48 ++-
be/src/storage/transform/row_binlog_derive.h | 10 +-
.../storage/rowset/segment_flusher_format_test.cpp | 406 ++++++++++++++++++++-
.../storage/transform/row_binlog_derive_test.cpp | 50 ++-
.../test_row_binlog_flexible_partial_update.out | 69 ++++
..._binlog_flexible_partial_update_no_sequence.out | 44 +++
.../test_row_binlog_flexible_partial_update.groovy | 176 +++++++++
...nlog_flexible_partial_update_no_sequence.groovy | 101 +++++
12 files changed, 1125 insertions(+), 56 deletions(-)
diff --git a/be/src/storage/partial_update_info.cpp
b/be/src/storage/partial_update_info.cpp
index bc322a97f35..b6a2c9ae8b7 100644
--- a/be/src/storage/partial_update_info.cpp
+++ b/be/src/storage/partial_update_info.cpp
@@ -40,6 +40,7 @@
#include "storage/tablet/tablet_meta.h"
#include "storage/tablet/tablet_schema.h"
#include "storage/utils.h"
+#include "util/defer_op.h"
namespace doris {
@@ -884,12 +885,14 @@ void BlockAggregator::merge_one_row(MutableBlock&
dst_block, Block* src_block, i
rid);
}
}
+ merge_row_lsn(rid);
VLOG_DEBUG << fmt::format("merge a row, after merge,
output_block.rows()={}, state: {}",
dst_block.rows(), _state.to_string());
}
void BlockAggregator::append_one_row(MutableBlock& dst_block, Block*
src_block, int rid) {
dst_block.add_row(src_block, rid);
+ append_row_lsn(rid);
_state.rows++;
VLOG_DEBUG << fmt::format("append a new row, after append,
output_block.rows()={}, state: {}",
dst_block.rows(), _state.to_string());
@@ -901,6 +904,31 @@ void BlockAggregator::remove_last_n_rows(MutableBlock&
dst_block, int n) {
DCHECK_GE(dst_block.mutable_columns()[cid]->size(), n);
dst_block.mutable_columns()[cid]->pop_back(n);
}
+ remove_last_row_lsns(n);
+ }
+}
+
+void BlockAggregator::append_row_lsn(int rid) {
+ if (_output_row_lsns != nullptr) {
+ DCHECK(_input_row_lsns != nullptr);
+ DCHECK_LT(rid, _input_row_lsns->size());
+ _output_row_lsns->push_back(_input_row_lsns->at(rid));
+ }
+}
+
+void BlockAggregator::merge_row_lsn(int rid) {
+ if (_output_row_lsns != nullptr) {
+ DCHECK(_input_row_lsns != nullptr);
+ DCHECK(!_output_row_lsns->empty());
+ DCHECK_LT(rid, _input_row_lsns->size());
+ _output_row_lsns->back() = std::max(_output_row_lsns->back(),
_input_row_lsns->at(rid));
+ }
+}
+
+void BlockAggregator::remove_last_row_lsns(int n) {
+ if (_output_row_lsns != nullptr && n > 0) {
+ DCHECK_GE(_output_row_lsns->size(), n);
+ _output_row_lsns->resize(_output_row_lsns->size() - n);
}
}
@@ -930,6 +958,7 @@ Status BlockAggregator::aggregate_rows(
VLOG_DEBUG << fmt::format("merge rows in range=[{}-{})", start, end);
if (end - start == 1) {
output_block.add_row(block, start);
+ append_row_lsn(start);
VLOG_DEBUG << fmt::format("append a row directly, rid={}", start);
return Status::OK();
}
@@ -1036,8 +1065,10 @@ Status
BlockAggregator::_generate_encoded_default_seq_value(std::string* encoded
Status BlockAggregator::aggregate_for_sequence_column(
Block* block, int num_rows, const
std::vector<IOlapColumnDataAccessor*>& key_columns,
IOlapColumnDataAccessor* seq_column, const
std::vector<RowsetSharedPtr>& specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches) {
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns) {
DCHECK_EQ(block->columns(), _tablet_schema.num_columns());
+ DCHECK(row_lsns == nullptr || row_lsns->size() == num_rows);
// the process logic here is the same as
MemTable::_aggregate_for_flexible_partial_update_without_seq_col()
// after this function, there will be at most 2 rows for a specified key
std::vector<BitmapValue>* skip_bitmaps =
@@ -1047,6 +1078,16 @@ Status BlockAggregator::aggregate_for_sequence_column(
auto filtered_block = _tablet_schema.create_storage_block();
MutableBlock output_block =
MutableBlock::build_mutable_block(std::move(filtered_block));
+ std::vector<int64_t> output_row_lsns;
+ Defer reset_lsn_sidecar {[this] {
+ _input_row_lsns = nullptr;
+ _output_row_lsns = nullptr;
+ }};
+ if (row_lsns != nullptr) {
+ output_row_lsns.reserve(row_lsns->size());
+ _input_row_lsns = row_lsns;
+ _output_row_lsns = &output_row_lsns;
+ }
int same_key_rows {0};
std::string previous_key {};
@@ -1072,6 +1113,10 @@ Status BlockAggregator::aggregate_for_sequence_column(
}
block->swap(output_block.to_block());
+ if (row_lsns != nullptr) {
+ row_lsns->swap(output_row_lsns);
+ DCHECK_EQ(row_lsns->size(), block->rows());
+ }
return Status::OK();
}
@@ -1106,8 +1151,13 @@ Status BlockAggregator::fill_sequence_column(Block*
block, size_t num_rows,
Status BlockAggregator::aggregate_for_insert_after_delete(
Block* block, size_t num_rows, const
std::vector<IOlapColumnDataAccessor*>& key_columns,
const std::vector<RowsetSharedPtr>& specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches) {
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns, std::vector<uint8_t>*
insert_after_delete_flags) {
DCHECK_EQ(block->columns(), _tablet_schema.num_columns());
+ DCHECK(row_lsns == nullptr || row_lsns->size() == num_rows);
+ if (insert_after_delete_flags != nullptr) {
+ insert_after_delete_flags->assign(num_rows, 0);
+ }
// there will be at most 2 rows for a specified key in block when control
flow reaches here
// after this function, there will not be duplicate rows in block
@@ -1130,14 +1180,6 @@ Status
BlockAggregator::aggregate_for_insert_after_delete(
FixedReadPlan read_plan;
// This insert-after-delete pass doesn't report partial-update counts;
discard them.
PartialUpdateStats discarded;
- // the losing delete-sign row is removed from the block below instead of
- // marking itself, so the probe only marks the old row
- segment_v2::MowKeyProbe probe(
- _tablet.get(), &_tablet_schema, _tablet_schema.has_sequence_col(),
_mow_context,
- RowsetId {}, 0,
- segment_v2::MowKeyProbe::Policy {
- .mark_deleted =
segment_v2::MowKeyProbe::MarkDeleted::OLD_ROW,
- });
for (size_t block_pos {0}; block_pos < num_rows; block_pos++) {
size_t delta_pos = block_pos;
auto& skip_bitmap = skip_bitmaps->at(block_pos);
@@ -1152,9 +1194,9 @@ Status BlockAggregator::aggregate_for_insert_after_delete(
DCHECK(previous_has_delete_sign);
DCHECK(!have_delete_sign);
++duplicate_rows;
- auto probe_res = probe.probe(key, /*segment_pos=*/0,
/*key_has_seq_suffix=*/false,
- /*have_delete_sign=*/false,
specified_rowsets,
- segment_caches, discarded);
+ auto probe_res = _probe.probe(key, /*segment_pos=*/0,
/*key_has_seq_suffix=*/false,
+ /*have_delete_sign=*/false,
specified_rowsets,
+ segment_caches, discarded);
DCHECK(probe_res.has_value() ||
probe_res.error().is<ErrorCode::MEM_LIMIT_EXCEEDED>())
<< "[BlockAggregator::aggregate_for_insert_after_delete]
unexpected error "
"status while lookup_row_key:"
@@ -1180,6 +1222,13 @@ Status
BlockAggregator::aggregate_for_insert_after_delete(
}
// and remove the row with delete sign from the current block
filter_map[block_pos - 1] = 0;
+ if (insert_after_delete_flags != nullptr) {
+ insert_after_delete_flags->at(block_pos) = 1;
+ }
+ if (row_lsns != nullptr) {
+ row_lsns->at(block_pos) =
+ std::max(row_lsns->at(block_pos - 1),
row_lsns->at(block_pos));
+ }
}
previous_has_delete_sign = have_delete_sign;
previous_key = std::move(key);
@@ -1189,8 +1238,31 @@ Status
BlockAggregator::aggregate_for_insert_after_delete(
// fill sequence column value for some rows
RETURN_IF_ERROR(fill_sequence_column(block, num_rows, read_plan,
*skip_bitmaps));
}
+ if (row_lsns != nullptr) {
+ std::vector<int64_t> filtered_row_lsns;
+ filtered_row_lsns.reserve(row_lsns->size() - duplicate_rows);
+ for (size_t pos = 0; pos < num_rows; ++pos) {
+ if (filter_map[pos] != 0) {
+ filtered_row_lsns.push_back(row_lsns->at(pos));
+ }
+ }
+ row_lsns->swap(filtered_row_lsns);
+ }
+ if (insert_after_delete_flags != nullptr) {
+ std::vector<uint8_t> filtered_flags;
+ filtered_flags.reserve(insert_after_delete_flags->size() -
duplicate_rows);
+ for (size_t pos = 0; pos < num_rows; ++pos) {
+ if (filter_map[pos] != 0) {
+
filtered_flags.push_back(insert_after_delete_flags->at(pos));
+ }
+ }
+ insert_after_delete_flags->swap(filtered_flags);
+ }
RETURN_IF_ERROR(filter_block(block, num_rows,
std::move(filter_column), duplicate_rows,
"__filter_insert_after_delete_col__"));
+ DCHECK(row_lsns == nullptr || row_lsns->size() == block->rows());
+ DCHECK(insert_after_delete_flags == nullptr ||
+ insert_after_delete_flags->size() == block->rows());
}
return Status::OK();
}
@@ -1246,7 +1318,12 @@ Status BlockAggregator::convert_seq_column(Block* block,
size_t row_pos, size_t
Status BlockAggregator::aggregate_for_flexible_partial_update(
Block* block, size_t num_rows, const std::vector<RowsetSharedPtr>&
specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches) {
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns, std::vector<uint8_t>*
insert_after_delete_flags) {
+ if (row_lsns != nullptr && row_lsns->size() != num_rows) {
+ return Status::InvalidArgument("row binlog LSN count {} does not match
row count {}",
+ row_lsns->size(), num_rows);
+ }
std::vector<IOlapColumnDataAccessor*> key_columns {};
IOlapColumnDataAccessor* seq_column {nullptr};
@@ -1260,7 +1337,7 @@ Status
BlockAggregator::aggregate_for_flexible_partial_update(
if (_tablet_schema.has_sequence_col()) {
RETURN_IF_ERROR(aggregate_for_sequence_column(block,
static_cast<int>(num_rows),
key_columns, seq_column,
specified_rowsets,
- segment_caches));
+ segment_caches,
row_lsns));
}
// 2. merge duplicate rows and handle insert after delete
@@ -1272,7 +1349,8 @@ Status
BlockAggregator::aggregate_for_flexible_partial_update(
RETURN_IF_ERROR(convert_seq_column(block, 0, num_rows, seq_column));
}
RETURN_IF_ERROR(aggregate_for_insert_after_delete(block, num_rows,
key_columns,
- specified_rowsets,
segment_caches));
+ specified_rowsets,
segment_caches, row_lsns,
+
insert_after_delete_flags));
return Status::OK();
}
diff --git a/be/src/storage/partial_update_info.h
b/be/src/storage/partial_update_info.h
index 01b4bb3f302..4b8ab99840f 100644
--- a/be/src/storage/partial_update_info.h
+++ b/be/src/storage/partial_update_info.h
@@ -217,20 +217,28 @@ public:
std::vector<IOlapColumnDataAccessor*>&
key_columns);
Status convert_seq_column(Block* block, size_t row_pos, size_t num_rows,
IOlapColumnDataAccessor*& seq_column);
+ // Optional sidecars are aggregated and filtered with `block`; on return
each element is
+ // aligned with the corresponding row in the final block. A non-zero
+ // `insert_after_delete_flags` entry marks the surviving INSERT of a
same-batch
+ // DELETE-then-INSERT pair.
Status aggregate_for_flexible_partial_update(
Block* block, size_t num_rows, const std::vector<RowsetSharedPtr>&
specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches);
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns = nullptr,
+ std::vector<uint8_t>* insert_after_delete_flags = nullptr);
private:
Status aggregate_for_sequence_column(
Block* block, int num_rows, const
std::vector<IOlapColumnDataAccessor*>& key_columns,
IOlapColumnDataAccessor* seq_column,
const std::vector<RowsetSharedPtr>& specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches);
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns);
Status aggregate_for_insert_after_delete(
Block* block, size_t num_rows, const
std::vector<IOlapColumnDataAccessor*>& key_columns,
const std::vector<RowsetSharedPtr>& specified_rowsets,
- std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches);
+ std::vector<std::unique_ptr<SegmentCacheHandle>>& segment_caches,
+ std::vector<int64_t>* row_lsns, std::vector<uint8_t>*
insert_after_delete_flags);
Status filter_block(Block* block, size_t num_rows, MutableColumnPtr
filter_column,
int duplicate_rows, std::string col_name);
@@ -244,6 +252,10 @@ private:
void append_one_row(MutableBlock& dst_block, Block* src_block, int rid);
void remove_last_n_rows(MutableBlock& dst_block, int n);
+ void append_row_lsn(int rid);
+ void merge_row_lsn(int rid);
+ void remove_last_row_lsns(int n);
+
// aggregate rows with same keys in range [start, end) from block to
output_block
Status aggregate_rows(MutableBlock& output_block, Block* block, int start,
int end,
std::string key, std::vector<BitmapValue>*
skip_bitmaps,
@@ -262,6 +274,12 @@ private:
const segment_v2::MowKeyProbe& _probe;
HistoricalRowFetcher& _fetcher;
+ // Optional sidecar used by Row Binlog. Flexible aggregation can remove or
merge input rows;
+ // keep the persisted LSN aligned with the surviving row and retain the
largest LSN of all
+ // merged changes.
+ const std::vector<int64_t>* _input_row_lsns = nullptr;
+ std::vector<int64_t>* _output_row_lsns = nullptr;
+
// used to store state when aggregating rows in block
struct AggregateState {
int rows {0};
diff --git a/be/src/storage/segment/historical_row_retriever.cpp
b/be/src/storage/segment/historical_row_retriever.cpp
index dbe5695dea8..56c7389f876 100644
--- a/be/src/storage/segment/historical_row_retriever.cpp
+++ b/be/src/storage/segment/historical_row_retriever.cpp
@@ -143,6 +143,141 @@ Status
PrimaryKeyModelRowRetriever::retrieve_historical_row(const Int8* delete_s
return Status::OK();
}
+//
NOLINTNEXTLINE(readability-function-size,readability-function-cognitive-complexity):
Keep the
+// probe, aligned sidecar aggregation, and history-read plan in one
snapshot-consistent operation.
+Status PrimaryKeyModelRowRetriever::materialize_flexible_partial_update(
+ Block* block, std::shared_ptr<MowContext> mow_context,
+ std::vector<int64_t>* row_lsns) { //
NOLINT(readability-non-const-parameter): in/out sidecar
+ auto& tablet_schema = _context.tablet_schema;
+ if (_context.partial_update_info == nullptr ||
+ !_context.partial_update_info->is_flexible_partial_update()) {
+ return Status::InvalidArgument(
+ "flexible partial update materialization requires flexible
partial update info");
+ }
+ if (block->columns() != tablet_schema->num_columns()) {
+ return Status::InvalidArgument(
+ "illegal flexible partial update block columns: {}, schema
columns: {}",
+ block->columns(), tablet_schema->num_columns());
+ }
+
+ _mow_context = std::move(mow_context);
+ if (_mow_context == nullptr) {
+ return Status::InvalidArgument(
+ "flexible partial update materialization requires MOW
context");
+ }
+
+ std::vector<RowsetSharedPtr> specified_rowsets;
+ {
+ std::shared_lock rlock(_context.tablet->get_header_lock());
+ specified_rowsets = _mow_context->rowset_ptrs;
+ }
+ std::vector<std::unique_ptr<SegmentCacheHandle>>
segment_caches(specified_rowsets.size());
+
+ CHECK(_context.rowset_writer_ctx != nullptr);
+ const bool write_before =
+
_context.rowset_writer_ctx->write_binlog_opt().write_binlog_config().write_before;
+ TabletSchemaSPtr lookup_schema = _context.tablet->tablet_schema();
+ MowKeyProbe probe = MowKeyProbe::for_row_binlog(_context.tablet.get(),
lookup_schema.get(),
+
tablet_schema->has_sequence_col(), _mow_context,
+ write_before);
+ BlockAggregator aggregator(*tablet_schema, _context.tablet, _mow_context,
+ *_context.partial_update_info, *_key_encoder,
probe, *_row_fetcher);
+
+ size_t num_rows = block->rows();
+ std::vector<uint8_t> insert_after_delete_flags;
+ RETURN_IF_ERROR(aggregator.aggregate_for_flexible_partial_update(
+ block, num_rows, specified_rowsets, segment_caches, row_lsns,
+ &insert_after_delete_flags));
+ num_rows = block->rows();
+ if (num_rows == 0) {
+ return Status::OK();
+ }
+ DCHECK_EQ(insert_after_delete_flags.size(), num_rows);
+
+ std::vector<IOlapColumnDataAccessor*> key_columns;
+ RETURN_IF_ERROR(aggregator.convert_pk_columns(block, 0, num_rows,
key_columns));
+ IOlapColumnDataAccessor* seq_column = nullptr;
+ RETURN_IF_ERROR(aggregator.convert_seq_column(block, 0, num_rows,
seq_column));
+
+ auto* skip_bitmaps =
+ &get_mutable_skip_bitmap_column(block,
tablet_schema->skip_bitmap_col_idx())
+ ->get_data();
+ const auto* delete_signs = BaseTablet::get_delete_sign_column_data(*block,
num_rows);
+ DCHECK(delete_signs != nullptr);
+
+ const bool schema_has_seq = tablet_schema->has_sequence_col();
+ const int32_t seq_col_unique_id =
+ schema_has_seq ?
tablet_schema->column(tablet_schema->sequence_col_idx()).unique_id()
+ : -1;
+ const int32_t delete_sign_col_unique_id =
+
tablet_schema->column(tablet_schema->delete_sign_idx()).unique_id();
+ Block full_block = tablet_schema->create_storage_block();
+ for (size_t cid = 0; cid < tablet_schema->num_key_columns(); ++cid) {
+ const auto& input_column = block->get_by_position(cid);
+ auto& full_column = full_block.get_by_position(cid);
+ full_column.column = input_column.column;
+ full_column.type = input_column.type;
+ }
+
+ bool has_default_or_nullable = false;
+ std::vector<bool> use_default_or_null_flag;
+ use_default_or_null_flag.reserve(num_rows);
+ PartialUpdateStats discarded_stats;
+ for (size_t pos = 0; pos < num_rows; ++pos) {
+ const bool row_has_seq =
+ schema_has_seq &&
!skip_bitmaps->at(pos).contains(seq_col_unique_id);
+ std::string key = _key_encoder->full_encode_primary_keys(key_columns,
pos);
+ if (row_has_seq) {
+ _key_encoder->append_seq_suffix(&key, seq_column, pos);
+ }
+ const bool have_delete_sign =
!skip_bitmaps->at(pos).contains(delete_sign_col_unique_id) &&
+ delete_signs[pos] != 0;
+ ProbeOutcome outcome =
+ DORIS_TRY(probe.probe(key, /*segment_pos=*/pos, row_has_seq,
have_delete_sign,
+ specified_rowsets, segment_caches,
discarded_stats));
+ const bool insert_after_delete = insert_after_delete_flags[pos] != 0;
+ DCHECK(!insert_after_delete || !have_delete_sign);
+ if (outcome.result == KeyProbeResult::NOT_FOUND && !have_delete_sign) {
+ RETURN_IF_ERROR(_context.partial_update_info->handle_new_key(
+ *tablet_schema,
+ [&]() -> std::string {
+ return block->dump_one_line(
+ pos,
cast_set<int>(tablet_schema->num_key_columns()));
+ },
+ &skip_bitmaps->at(pos)));
+ }
+
+ const bool use_default_or_null = outcome.use_default_or_null ||
insert_after_delete;
+ has_default_or_nullable |= use_default_or_null;
+ use_default_or_null_flag.emplace_back(use_default_or_null);
+ if (!use_default_or_null) {
+ _row_fetcher->pin_rowset(outcome.rowset);
+ _row_fetcher->plan_flexible_read(outcome.loc, pos,
skip_bitmaps->at(pos));
+ // The same location supplies the optional BEFORE image and the
old delete sign used
+ // to distinguish an append after a tombstone from an update.
+ _row_fetcher->plan_fixed_read(outcome.loc, pos);
+ } else if (insert_after_delete && outcome.result !=
KeyProbeResult::NOT_FOUND) {
+ // The base writer treats this row as a new row for missing-column
fill, but the net
+ // CDC change still replaces the live row that existed before this
load. Keep its
+ // BEFORE image while preventing old values from leaking into
AFTER.
+ _row_fetcher->pin_rowset(outcome.rowset);
+ _row_fetcher->plan_fixed_read(outcome.loc, pos);
+ }
+ _operators.emplace_back(
+ outcome.result == KeyProbeResult::NOT_FOUND
+ ? (have_delete_sign ? ROW_BINLOG_DELETE :
ROW_BINLOG_APPEND)
+ : (have_delete_sign ? ROW_BINLOG_DELETE :
ROW_BINLOG_UPDATE));
+ }
+
+ RETURN_IF_ERROR(_row_fetcher->fill_non_primary_key_columns(
+ *tablet_schema, full_block, use_default_or_null_flag,
has_default_or_nullable,
+ /*segment_start_pos=*/0, /*block_start_pos=*/0, block,
skip_bitmaps));
+ block->swap(full_block);
+ DCHECK_EQ(_operators.size(), block->rows());
+ DCHECK(row_lsns == nullptr || row_lsns->size() == block->rows());
+ return Status::OK();
+}
+
Status PrimaryKeyModelRowRetriever::build_after_block(Block* block, size_t
row_pos,
size_t num_rows) {
DCHECK_EQ(_use_default_or_null_flag.size(), num_rows);
diff --git a/be/src/storage/segment/historical_row_retriever.h
b/be/src/storage/segment/historical_row_retriever.h
index 0f89d6b9dc5..d4042d0e3be 100644
--- a/be/src/storage/segment/historical_row_retriever.h
+++ b/be/src/storage/segment/historical_row_retriever.h
@@ -81,6 +81,14 @@ public:
Status retrieve_historical_row(const Int8* delete_sign_column_data, size_t
row_pos,
size_t num_rows) override;
+ // Row Binlog receives the original flexible-partial-update block, while
the base writer fills
+ // its own copy. Reuse the common flexible aggregator/read plans with a
read-only MOW probe to
+ // build the same full AFTER rows without changing the base tablet's
delete bitmap. The optional
+ // LSN sidecar is merged and filtered together with the rows.
+ Status materialize_flexible_partial_update(Block* block,
+ std::shared_ptr<MowContext>
mow_context,
+ std::vector<int64_t>* row_lsns);
+
Status build_after_block(Block* block, size_t row_pos, size_t num_rows)
override;
Status build_before_block(Block* before_block, const
std::vector<uint32_t>& value_cids,
diff --git a/be/src/storage/transform/row_binlog_derive.cpp
b/be/src/storage/transform/row_binlog_derive.cpp
index 3d11f169459..69dde8a293f 100644
--- a/be/src/storage/transform/row_binlog_derive.cpp
+++ b/be/src/storage/transform/row_binlog_derive.cpp
@@ -301,8 +301,8 @@ bool binlog_needs_historical_lookup(const
RowsetWriterContext& context) {
const bool is_source_direct_write =
cfg.source.source_write_type == DataWriteType::TYPE_DIRECT &&
!cfg.source.is_transient_rowset_writer;
- // A direct partial update (fixed needs AFTER rebuilt; flexible is rejected
- // inside the MoW stage) or a requested BEFORE image needs the probe.
+ // A direct partial update (fixed and flexible both need AFTER rebuilt) or
a
+ // requested BEFORE image needs the probe.
const bool is_partial_update = cfg.source.partial_update_info != nullptr &&
cfg.source.partial_update_info->is_partial_update() &&
is_source_direct_write;
@@ -336,16 +336,38 @@ Status
MowRowBinlogDeriveStage::derive(TransformExecContext& ctx, Block* block,
const BinlogDeriveContext& c) const {
const auto& cfg = ctx.rowset_ctx->write_binlog_opt().write_binlog_config();
- bool is_source_direct_write = cfg.source.source_write_type ==
DataWriteType::TYPE_DIRECT &&
- !cfg.source.is_transient_rowset_writer;
- if (cfg.source.partial_update_info && is_source_direct_write &&
- cfg.source.partial_update_info->is_flexible_partial_update()) {
- return Status::NotSupported("binlog<row> does not support flexible
partial update");
+ const bool is_source_direct_write =
+ cfg.source.source_write_type == DataWriteType::TYPE_DIRECT &&
+ !cfg.source.is_transient_rowset_writer;
+ const bool is_flexible_partial_update =
+ cfg.source.partial_update_info && is_source_direct_write &&
+ cfg.source.partial_update_info->is_flexible_partial_update();
+ const bool is_fixed_partial_update = cfg.source.partial_update_info &&
is_source_direct_write &&
+
cfg.source.partial_update_info->is_fixed_partial_update();
+
+ if (is_flexible_partial_update) {
+ auto retriever = std::make_unique<PrimaryKeyModelRowRetriever>();
+ RETURN_IF_ERROR(retriever->init(HistoricalRowRetrieverContext {
+ .tablet = cfg.source.base_tablet,
+ .tablet_schema = c.source_schema,
+ .rowset_writer_ctx = ctx.rowset_ctx,
+ .partial_update_info = cfg.source.partial_update_info,
+ .is_transient_rowset_writer =
cfg.source.is_transient_rowset_writer,
+ .write_type = cfg.source.source_write_type}));
+
+ std::vector<int64_t> effective_lsn_ids(c.lsn_ids->begin(),
c.lsn_ids->begin() + c.num_rows);
+ RETURN_IF_ERROR(retriever->materialize_flexible_partial_update(
+ block, cfg.source.mow_context, &effective_lsn_ids));
+
+ BinlogDeriveContext effective_context = c;
+ effective_context.num_rows = block->rows();
+ effective_context.lsn_ids =
+ std::make_shared<const
std::vector<int64_t>>(std::move(effective_lsn_ids));
+ return emit_binlog_block(effective_context, block, retriever.get(),
+ /*plain_operators=*/nullptr, block);
}
- bool is_partial_update = cfg.source.partial_update_info &&
-
cfg.source.partial_update_info->is_fixed_partial_update() &&
- is_source_direct_write;
- if (is_partial_update) {
+
+ if (is_fixed_partial_update) {
if (block->columns() < c.source_schema->num_key_columns() ||
block->columns() >= c.source_schema->num_columns()) {
return Status::InvalidArgument(fmt::format(
@@ -360,7 +382,7 @@ Status
MowRowBinlogDeriveStage::derive(TransformExecContext& ctx, Block* block,
// move in the narrow partial-update block)
int32_t delete_sign_pos = c.source_schema->delete_sign_idx();
int32_t seq_pos = c.source_schema->sequence_col_idx();
- if (is_partial_update) {
+ if (is_fixed_partial_update) {
delete_sign_pos = -1;
seq_pos = -1;
int32_t pos = 0;
@@ -390,7 +412,7 @@ Status
MowRowBinlogDeriveStage::derive(TransformExecContext& ctx, Block* block,
// missing columns from history; upserts are already source-schema shaped.
Block full_block;
const Block* after_src = block;
- if (is_partial_update) {
+ if (is_fixed_partial_update) {
full_block = widen_partial_update_block(
*c.source_schema, cfg.source.partial_update_info->update_cids,
*block);
RETURN_IF_ERROR(retriever->build_after_block(&full_block, 0,
c.num_rows));
diff --git a/be/src/storage/transform/row_binlog_derive.h
b/be/src/storage/transform/row_binlog_derive.h
index b7e174e1a74..5e004f23ae0 100644
--- a/be/src/storage/transform/row_binlog_derive.h
+++ b/be/src/storage/transform/row_binlog_derive.h
@@ -31,8 +31,8 @@ namespace segment_v2 {
// it like any DUP_KEYS block. build_transform_chain picks Plain (no historical
// probe) or Mow (with probe) via binlog_needs_historical_lookup().
-// Whether the flush needs the historical key probe: a direct partial update
-// (flexible is rejected later) or a requested BEFORE image. Decided per flush.
+// Whether the flush needs the historical key probe: a direct partial update or
+// a requested BEFORE image. Decided per flush.
bool binlog_needs_historical_lookup(const RowsetWriterContext& context);
// Context for each flush that the base stage works out once and passes to
@@ -75,9 +75,9 @@ protected:
const BinlogDeriveContext& c) const override;
};
-// With a probe: fixed partial update (rebuild AFTER from history) and/or the
-// BEFORE image. It repeats the data chain's key probe on the same source
block,
-// but never marks the delete bitmap.
+// With a probe: fixed/flexible partial update (rebuild AFTER from history)
+// and/or the BEFORE image. It repeats the data chain's key probe on the same
+// source block, but never marks the delete bitmap.
class MowRowBinlogDeriveStage : public RowBinlogDeriveStage {
public:
std::string_view name() const override { return "MowRowBinlogDerive"; }
diff --git a/be/test/storage/rowset/segment_flusher_format_test.cpp
b/be/test/storage/rowset/segment_flusher_format_test.cpp
index a29164599cc..af530741fed 100644
--- a/be/test/storage/rowset/segment_flusher_format_test.cpp
+++ b/be/test/storage/rowset/segment_flusher_format_test.cpp
@@ -1375,6 +1375,115 @@ Result<Block> create_mow_history_block(const
TabletSchemaSPtr& schema) {
return block;
}
+Result<Block> create_flexible_row_binlog_update_block(const TabletSchemaSPtr&
schema) {
+ DORIS_CHECK_GE(schema->skip_bitmap_col_idx(), 0);
+ DORIS_CHECK(schema->has_sequence_col());
+
+ constexpr std::array<int, 9> keys {0, 0, 1, 2, 3, 3, 10, 11, 11};
+ constexpr std::array<int, 9> v1_values {7000, 0, 0, 7002, 0, 0, 0, 0, 0};
+ constexpr std::array<int64_t, 9> v2_values {0, 8000, 8001, 8002, 0, 0, 0,
0, 8011};
+ constexpr std::array<int, 9> sequence_values {6000, 0, 6001, 6002, 6003,
6004, 6010, 6011, 0};
+
+ Block block = schema->create_storage_block();
+ auto* skip_bitmap = assert_cast<ColumnBitmap*>(
+
block.get_by_position(schema->skip_bitmap_col_idx()).column->assert_mutable().get());
+ for (size_t row = 0; row < keys.size(); ++row) {
+ for (size_t column_index = 0; column_index < block.columns();
++column_index) {
+ const auto& name = block.get_by_position(column_index).name;
+ if (name == "k1") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(keys[row])));
+ } else if (name == "v1") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(v1_values[row])));
+ } else if (name == "v2") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(v2_values[row])));
+ } else if (name == SEQUENCE_COL) {
+ RETURN_IF_ERROR_RESULT(append_text_value(&block, column_index,
+
std::to_string(sequence_values[row])));
+ } else if (name == DELETE_SIGN) {
+ const bool is_delete = row == 4 || row == 6 || row == 7;
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index, is_delete ?
"1" : "0"));
+ } else if (name != SKIP_BITMAP_COL) {
+
block.get_by_position(column_index).column->assert_mutable()->insert_default();
+ }
+ }
+
+ BitmapValue skipped_columns;
+ const auto skip = [&](std::string_view name) {
+ const auto column_index = schema->field_index(std::string(name));
+ DORIS_CHECK_GE(column_index, 0);
+ skipped_columns.add(static_cast<uint64_t>(
+
schema->column(static_cast<size_t>(column_index)).unique_id()));
+ };
+ if (row == 0 || row == 4 || row == 5 || row == 6 || row == 7) {
+ skip("v2");
+ }
+ if (row == 1 || row == 2 || row == 4 || row == 5 || row == 6 || row ==
7 || row == 8) {
+ skip("v1");
+ }
+ if (row == 1 || row == 8) {
+ skip(SEQUENCE_COL);
+ }
+ if (row != 4 && row != 6 && row != 7) {
+ skip(DELETE_SIGN);
+ }
+ skip_bitmap->insert_value(std::move(skipped_columns));
+ }
+ return block;
+}
+
+struct FlexibleSequenceRow {
+ int key;
+ int v1;
+ int64_t v2;
+ int sequence;
+ bool is_delete;
+};
+
+Result<Block> create_flexible_sequence_block(const TabletSchemaSPtr& schema,
+ const
std::vector<FlexibleSequenceRow>& rows) {
+ DORIS_CHECK_GE(schema->skip_bitmap_col_idx(), 0);
+ DORIS_CHECK(schema->has_sequence_col());
+
+ Block block = schema->create_storage_block();
+ auto* skip_bitmap = assert_cast<ColumnBitmap*>(
+
block.get_by_position(schema->skip_bitmap_col_idx()).column->assert_mutable().get());
+ const auto delete_sign_unique_id =
schema->column(schema->delete_sign_idx()).unique_id();
+ for (const auto& row : rows) {
+ for (size_t column_index = 0; column_index < block.columns();
++column_index) {
+ const auto& name = block.get_by_position(column_index).name;
+ if (name == "k1") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(row.key)));
+ } else if (name == "v1") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(row.v1)));
+ } else if (name == "v2") {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(row.v2)));
+ } else if (name == SEQUENCE_COL) {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index,
std::to_string(row.sequence)));
+ } else if (name == DELETE_SIGN) {
+ RETURN_IF_ERROR_RESULT(
+ append_text_value(&block, column_index, row.is_delete
? "1" : "0"));
+ } else if (name != SKIP_BITMAP_COL) {
+
block.get_by_position(column_index).column->assert_mutable()->insert_default();
+ }
+ }
+
+ BitmapValue skipped_columns;
+ if (!row.is_delete) {
+ skipped_columns.add(static_cast<uint64_t>(delete_sign_unique_id));
+ }
+ skip_bitmap->insert_value(std::move(skipped_columns));
+ }
+ return block;
+}
+
struct ExternalInvertedIndexSignature {
std::string field;
std::map<std::string, std::vector<int32_t>> postings;
@@ -2391,6 +2500,51 @@ Status verify_row_binlog_before_segment(const
TabletSharedPtr& tablet, uint32_t
return Status::OK();
}
+Status verify_flexible_row_binlog_segment(const TabletSharedPtr& tablet,
+ std::string_view case_name) {
+ auto block_result = read_row_binlog_segment(tablet, case_name, 0);
+ if (!block_result.has_value()) {
+ return block_result.error();
+ }
+ const auto& block = block_result.value();
+ if (block.rows() != 6) {
+ return Status::InternalError("{} has {} rows, expected 6", case_name,
block.rows());
+ }
+
+ constexpr std::array<Int32, 6> keys {0, 1, 2, 3, 10, 11};
+ constexpr std::array<Int32, 6> after_v1 {7000, 3001, 7002, 0, 3010, 0};
+ constexpr std::array<Int64, 6> after_v2 {8000, 8001, 8002, 0, 4010, 8011};
+ constexpr std::array<Int64, 6> operations {ROW_BINLOG_UPDATE,
ROW_BINLOG_UPDATE,
+ ROW_BINLOG_APPEND,
ROW_BINLOG_APPEND,
+ ROW_BINLOG_DELETE,
ROW_BINLOG_UPDATE};
+ constexpr std::array<Int64, 6> lsns {1001, 1002, 1003, 1005, 1006, 1008};
+ for (size_t row = 0; row < block.rows(); ++row) {
+ RETURN_IF_ERROR(
+ verify_segment_field(block, "k1", row,
Field::create_field<TYPE_INT>(keys[row])));
+ RETURN_IF_ERROR(verify_segment_field(block, "v1", row,
+
Field::create_field<TYPE_INT>(after_v1[row])));
+ RETURN_IF_ERROR(verify_segment_field(
+ block, "v2", row,
+ row == 3 ? Field {} :
Field::create_field<TYPE_BIGINT>(after_v2[row])));
+ RETURN_IF_ERROR(verify_segment_field(block, BINLOG_OP_COL, row,
+
Field::create_field<TYPE_BIGINT>(operations[row])));
+ RETURN_IF_ERROR(verify_segment_field(block, BINLOG_LSN_COL, row,
+
Field::create_field<TYPE_BIGINT>(lsns[row])));
+ if (row == 2 || row == 3) {
+ RETURN_IF_ERROR(verify_segment_field(block, "__BEFORE__v1__", row,
Field {}));
+ RETURN_IF_ERROR(verify_segment_field(block, "__BEFORE__v2__", row,
Field {}));
+ } else {
+ RETURN_IF_ERROR(verify_segment_field(
+ block, "__BEFORE__v1__", row,
+ Field::create_field<TYPE_INT>(static_cast<Int32>(3000 +
keys[row]))));
+ RETURN_IF_ERROR(verify_segment_field(
+ block, "__BEFORE__v2__", row,
+ Field::create_field<TYPE_BIGINT>(static_cast<Int64>(4000 +
keys[row]))));
+ }
+ }
+ return Status::OK();
+}
+
Status compare_logical_segments(std::string_view case_name, uint32_t
segment_id,
const LogicalSegmentContents& current,
const LogicalSegmentContents& golden) {
@@ -2915,7 +3069,9 @@ protected:
BinlogTabletPair create_binlog_tablets(int64_t tablet_id, bool enable_mow,
bool include_before_columns = false,
- bool with_sequence = false) {
+ bool with_sequence = false,
+ bool flexible_partial_update =
false) {
+ DORIS_CHECK(!flexible_partial_update || enable_mow);
auto request = testutil::create_tablet_request(
tablet_id, 270068390, 10001, 1,
enable_mow ? TKeysType::UNIQUE_KEYS : TKeysType::DUP_KEYS,
@@ -2943,6 +3099,13 @@ protected:
request.tablet_schema.__set_sequence_col_idx(
static_cast<int32_t>(request.tablet_schema.columns.size())
- 1);
}
+ if (flexible_partial_update) {
+ auto skip_bitmap_column = testutil::create_tablet_column(
+ {SKIP_BITMAP_COL, TPrimitiveType::BITMAP, false, true,
TAggregationType::NONE});
+ skip_bitmap_column.__set_visible(false);
+ skip_bitmap_column.__set_default_value(std::string(1, '\0'));
+
request.tablet_schema.columns.push_back(std::move(skip_bitmap_column));
+ }
auto find_source_column = [&](std::string_view name) -> TColumn& {
auto& columns = request.tablet_schema.columns;
const auto it =
@@ -2951,6 +3114,9 @@ protected:
DORIS_CHECK(it != columns.end());
return *it;
};
+ if (flexible_partial_update) {
+ find_source_column("v1").__set_default_value("0");
+ }
find_source_column("v2").__set_is_allow_null(true);
if (enable_mow) {
find_source_column(DELETE_SIGN).__set_visible(false);
@@ -3021,6 +3187,16 @@ protected:
EXPECT_TRUE(_engine->create_tablet(request, &profile).ok());
auto source_tablet = _engine->tablet_manager()->get_tablet(tablet_id);
EXPECT_NE(source_tablet, nullptr);
+ if (flexible_partial_update) {
+ // TTabletSchema has no skip-bitmap ordinal. Production
flexible-update schemas are
+ // persisted as TabletSchemaPB with this field set, so make the
test-created schema
+ // match that representation before writing any rowset.
+ TabletSchemaPB schema_pb;
+ source_tablet->tablet_schema()->to_schema_pb(&schema_pb);
+ schema_pb.set_skip_bitmap_col_idx(
+
source_tablet->tablet_schema()->field_index(SKIP_BITMAP_COL));
+
source_tablet->tablet_meta()->mutable_tablet_schema()->init_from_pb(schema_pb);
+ }
EXPECT_TRUE(_engine->create_tablet(binlog_request, &profile).ok());
auto binlog_tablet =
_engine->tablet_manager()->get_tablet(binlog_request.tablet_id);
EXPECT_NE(binlog_tablet, nullptr);
@@ -3078,6 +3254,66 @@ protected:
return flusher.close();
}
+ Status flush_blocks_without_golden(
+ std::string_view case_name, const TabletSchemaSPtr& schema,
std::vector<Block> blocks,
+ bool enable_vertical_writer, bool enable_unique_key_merge_on_write,
+ const std::function<void(RowsetWriterContext&)>& configure_context,
+ std::shared_ptr<TestSegmentCollector>* output_collector) const {
+ const auto directory = fmt::format("{}/{}", kTestDir, case_name);
+
RETURN_IF_ERROR(io::global_local_filesystem()->create_directory(directory));
+ auto file_writer_creator =
+ std::make_shared<LocalSegmentFileWriterCreator>(directory,
schema);
+ auto segment_collector = std::make_shared<TestSegmentCollector>();
+ RowsetWriterContext context;
+ context.tablet_schema = schema;
+ context.tablet_path = directory;
+ context.tablet_id = 10001;
+ context.rowset_id.init(10002);
+ context.max_rows_per_segment = 1024;
+ context.write_type = DataWriteType::TYPE_DIRECT;
+ context.enable_unique_key_merge_on_write =
enable_unique_key_merge_on_write;
+ context.file_writer_creator = file_writer_creator;
+ context.segment_collector = segment_collector;
+ configure_context(context);
+
+ config::enable_vertical_segment_writer = enable_vertical_writer;
+ auto* sync_point = SyncPoint::get_instance();
+ const bool sync_point_was_enabled = sync_point->get_enable();
+ sync_point->enable_processing();
+ Defer restore_sync_point {[sync_point, sync_point_was_enabled] {
+ if (!sync_point_was_enabled) {
+ sync_point->disable_processing();
+ }
+ }};
+ std::vector<uint32_t> vertical_segment_ids;
+ SyncPoint::CallbackGuard vertical_writer_guard;
+ sync_point->set_call_back(
+ "SegmentFlusher::flush_vertical_segment_writer",
+ [&vertical_segment_ids](auto&& args) {
+
vertical_segment_ids.push_back(*try_any_cast<uint32_t*>(args[0]));
+ },
+ &vertical_writer_guard);
+
+ SegmentFileCollection segment_files;
+ InvertedIndexFileCollection index_files;
+ SegmentFlusher flusher(context, segment_files, index_files);
+ for (size_t segment_id = 0; segment_id < blocks.size(); ++segment_id) {
+ RETURN_IF_ERROR(
+ flusher.flush_single_block(&blocks[segment_id],
cast_set<int32_t>(segment_id)));
+ }
+ RETURN_IF_ERROR(flusher.close());
+ const auto expected_vertical_segment_ids =
+ enable_vertical_writer ? segment_collector->segment_ids :
std::vector<uint32_t> {};
+ if (vertical_segment_ids != expected_vertical_segment_ids) {
+ return Status::InternalError(
+ "unexpected Segment writer path for {}: expected {}
vertical flushes, "
+ "observed {}",
+ case_name, expected_vertical_segment_ids.size(),
vertical_segment_ids.size());
+ }
+ *output_collector = std::move(segment_collector);
+ return Status::OK();
+ }
+
void configure_partial_update_context(
RowsetWriterContext& context, const TabletSharedPtr& tablet,
const std::shared_ptr<PartialUpdateInfo>& partial_update_info,
@@ -3094,7 +3330,8 @@ protected:
RowsetWriterContext& context, const TabletSharedPtr& source_tablet,
const TabletSharedPtr& binlog_tablet,
const std::shared_ptr<PartialUpdateInfo>& partial_update_info =
nullptr,
- const std::vector<RowsetSharedPtr>& history = {}, bool need_before
= false) const {
+ const std::vector<RowsetSharedPtr>& history = {}, bool need_before
= false,
+ int64_t rows_per_segment = 3) const {
context.tablet_id = binlog_tablet->tablet_id();
context.tablet = binlog_tablet;
context.data_dir = binlog_tablet->data_dir();
@@ -3115,7 +3352,7 @@ protected:
options.source.source_write_type = DataWriteType::TYPE_DIRECT;
for (int64_t segment_id = 0; segment_id < 2; ++segment_id) {
auto lsn_ids = std::make_shared<std::vector<int64_t>>();
- for (int64_t row = 0; row < 3; ++row) {
+ for (int64_t row = 0; row < rows_per_segment; ++row) {
lsn_ids->push_back(1000 + segment_id * 100 + row);
}
context.insert_segment_allocated_lsns(segment_id,
std::move(lsn_ids));
@@ -3231,6 +3468,169 @@ TEST_F(SegmentFlusherTransformFormatTest,
}
}
+TEST_F(SegmentFlusherTransformFormatTest,
+ FlexiblePartialUpdateRowBinlogMaterializesFullBeforeAndAfterRows) {
+ const auto tablets = create_binlog_tablets(22008, true, true, true, true);
+ ASSERT_NE(tablets.source_tablet, nullptr);
+ ASSERT_NE(tablets.binlog_tablet, nullptr);
+
+ auto history_result =
+ write_mow_history(tablets.source_tablet,
tablets.source_tablet->tablet_schema(), 31012);
+ ASSERT_TRUE(history_result.has_value()) << history_result.error();
+ std::vector<RowsetSharedPtr> history {history_result.value()};
+
+ auto partial_update_info = std::make_shared<PartialUpdateInfo>();
+ ASSERT_TRUE(partial_update_info
+ ->init(tablets.source_tablet->tablet_id(), 1,
+ *tablets.source_tablet->tablet_schema(),
+ UniqueKeyUpdateModePB::UPDATE_FLEXIBLE_COLUMNS,
+ PartialUpdateNewRowPolicyPB::APPEND, {}, false,
0, 0, "UTC", "")
+ .ok());
+ auto block_result =
+
create_flexible_row_binlog_update_block(tablets.source_tablet->tablet_schema());
+ ASSERT_TRUE(block_result.has_value()) << block_result.error();
+
+ constexpr std::string_view case_name = "mow_flexible_partial_row_binlog";
+ auto flush_status = flush_row_binlog_block(
+ case_name, tablets.binlog_tablet->tablet_schema(),
std::move(block_result).value(),
+ [this, tablets, partial_update_info, history](RowsetWriterContext&
context) {
+ configure_row_binlog_context(context, tablets.source_tablet,
tablets.binlog_tablet,
+ partial_update_info, history,
true, 9);
+ });
+ ASSERT_TRUE(flush_status.ok()) << flush_status;
+
+ const auto verify_status =
verify_flexible_row_binlog_segment(tablets.binlog_tablet, case_name);
+ ASSERT_TRUE(verify_status.ok()) << verify_status;
+}
+
+TEST_F(SegmentFlusherTransformFormatTest,
+ FlexiblePartialUpdateRowBinlogKeepsSequenceLoserAlignedWithBaseRowId) {
+ const auto tablets = create_binlog_tablets(22009, true, false, true, true);
+ ASSERT_NE(tablets.source_tablet, nullptr);
+ ASSERT_NE(tablets.binlog_tablet, nullptr);
+
+ auto history_result =
+ write_mow_history(tablets.source_tablet,
tablets.source_tablet->tablet_schema(), 31013);
+ ASSERT_TRUE(history_result.has_value()) << history_result.error();
+ std::vector<RowsetSharedPtr> history {history_result.value()};
+
+ auto partial_update_info = std::make_shared<PartialUpdateInfo>();
+ ASSERT_TRUE(partial_update_info
+ ->init(tablets.source_tablet->tablet_id(), 1,
+ *tablets.source_tablet->tablet_schema(),
+ UniqueKeyUpdateModePB::UPDATE_FLEXIBLE_COLUMNS,
+ PartialUpdateNewRowPolicyPB::APPEND, {}, false,
0, 0, "UTC", "")
+ .ok());
+
+ const std::vector<FlexibleSequenceRow> rows {
+ {.key = 0, .v1 = 9000, .v2 = 9900, .sequence = 4999, .is_delete =
false},
+ {.key = 1, .v1 = 9001, .v2 = 9901, .sequence = 5000, .is_delete =
true},
+ {.key = 2, .v1 = 9002, .v2 = 9902, .sequence = 6002, .is_delete =
false},
+ };
+ auto base_block_result =
+
create_flexible_sequence_block(tablets.source_tablet->tablet_schema(), rows);
+ ASSERT_TRUE(base_block_result.has_value()) << base_block_result.error();
+ auto binlog_block_result =
+
create_flexible_sequence_block(tablets.source_tablet->tablet_schema(), rows);
+ ASSERT_TRUE(binlog_block_result.has_value()) <<
binlog_block_result.error();
+
+ auto mow_context = make_mow_context(tablets.source_tablet->tablet_id(),
history);
+ constexpr std::string_view base_case_name =
"mow_flexible_sequence_alignment_base";
+ std::shared_ptr<TestSegmentCollector> base_collector;
+ auto base_flush_status = flush_blocks_without_golden(
+ base_case_name, tablets.source_tablet->tablet_schema(),
+ {std::move(base_block_result).value()}, false, true,
+ [this, tablets, partial_update_info, history,
+ mow_context](RowsetWriterContext& context) {
+ configure_partial_update_context(context,
tablets.source_tablet,
+ partial_update_info, history);
+ context.mow_context = mow_context;
+ },
+ &base_collector);
+ ASSERT_TRUE(base_flush_status.ok()) << base_flush_status;
+ ASSERT_NE(base_collector, nullptr);
+ ASSERT_EQ(base_collector->segment_ids, (std::vector<uint32_t> {0}));
+ ASSERT_EQ(base_collector->segment_statistics.size(), 1);
+ ASSERT_EQ(base_collector->segment_statistics[0].row_num, 3);
+
+ RowsetId writing_rowset_id;
+ writing_rowset_id.init(10002);
+ EXPECT_TRUE(mow_context->delete_bitmap->contains(
+ {writing_rowset_id, 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0));
+ EXPECT_TRUE(mow_context->delete_bitmap->contains(
+ {writing_rowset_id, 0, DeleteBitmap::TEMP_VERSION_COMMON}, 1));
+ EXPECT_FALSE(mow_context->delete_bitmap->contains(
+ {writing_rowset_id, 0, DeleteBitmap::TEMP_VERSION_COMMON}, 2));
+
+ RowsetWriterContext base_read_context;
+ base_read_context.tablet_schema = tablets.source_tablet->tablet_schema();
+ base_read_context.tablet_id = tablets.source_tablet->tablet_id();
+ base_read_context.rowset_id = writing_rowset_id;
+ auto base_segment =
+ read_logical_segment(fmt::format("{}/{}/segment_0.dat", kTestDir,
base_case_name), 0,
+ tablets.source_tablet->tablet_schema(),
base_read_context, true);
+ ASSERT_TRUE(base_segment.has_value()) << base_segment.error();
+ ASSERT_EQ(base_segment->block.rows(), 3);
+ EXPECT_TRUE(verify_segment_field(base_segment->block, "k1", 0,
+ Field::create_field<TYPE_INT>(Int32(0)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(base_segment->block, "k1", 1,
+ Field::create_field<TYPE_INT>(Int32(1)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(base_segment->block, "k1", 2,
+ Field::create_field<TYPE_INT>(Int32(2)))
+ .ok());
+
+ constexpr std::string_view binlog_case_name =
"mow_flexible_sequence_alignment_binlog";
+ std::shared_ptr<TestSegmentCollector> binlog_collector;
+ auto binlog_flush_status = flush_blocks_without_golden(
+ binlog_case_name, tablets.binlog_tablet->tablet_schema(),
+ {std::move(binlog_block_result).value()}, false, false,
+ [this, tablets, partial_update_info, history,
+ mow_context](RowsetWriterContext& context) {
+ configure_row_binlog_context(context, tablets.source_tablet,
tablets.binlog_tablet,
+ partial_update_info, history,
false, 3);
+
context.write_binlog_opt().write_binlog_config().source.mow_context =
mow_context;
+ },
+ &binlog_collector);
+ ASSERT_TRUE(binlog_flush_status.ok()) << binlog_flush_status;
+ ASSERT_NE(binlog_collector, nullptr);
+ ASSERT_EQ(binlog_collector->segment_ids, (std::vector<uint32_t> {0}));
+ ASSERT_EQ(binlog_collector->segment_statistics.size(), 1);
+ ASSERT_EQ(binlog_collector->segment_statistics[0].row_num, 3);
+
+ auto binlog_segment = read_row_binlog_segment(tablets.binlog_tablet,
binlog_case_name, 0);
+ ASSERT_TRUE(binlog_segment.has_value()) << binlog_segment.error();
+ ASSERT_EQ(binlog_segment->rows(), 3);
+ EXPECT_TRUE(
+ verify_segment_field(*binlog_segment, "k1", 0,
Field::create_field<TYPE_INT>(Int32(0)))
+ .ok());
+ EXPECT_TRUE(
+ verify_segment_field(*binlog_segment, "k1", 1,
Field::create_field<TYPE_INT>(Int32(1)))
+ .ok());
+ EXPECT_TRUE(
+ verify_segment_field(*binlog_segment, "k1", 2,
Field::create_field<TYPE_INT>(Int32(2)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_LSN_COL, 0,
+
Field::create_field<TYPE_BIGINT>(Int64(1000)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_OP_COL, 0,
+
Field::create_field<TYPE_BIGINT>(Int64(ROW_BINLOG_UPDATE)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_LSN_COL, 1,
+
Field::create_field<TYPE_BIGINT>(Int64(1001)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_OP_COL, 1,
+
Field::create_field<TYPE_BIGINT>(Int64(ROW_BINLOG_DELETE)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_LSN_COL, 2,
+
Field::create_field<TYPE_BIGINT>(Int64(1002)))
+ .ok());
+ EXPECT_TRUE(verify_segment_field(*binlog_segment, BINLOG_OP_COL, 2,
+
Field::create_field<TYPE_BIGINT>(Int64(ROW_BINLOG_APPEND)))
+ .ok());
+}
+
TEST_F(SegmentFlusherFormatTest, VariantLogicalComparisonPreservesScalarTypes)
{
VariantMap boolean_object;
boolean_object.try_emplace(
diff --git a/be/test/storage/transform/row_binlog_derive_test.cpp
b/be/test/storage/transform/row_binlog_derive_test.cpp
index 4ea6bb7ab17..07fc267d211 100644
--- a/be/test/storage/transform/row_binlog_derive_test.cpp
+++ b/be/test/storage/transform/row_binlog_derive_test.cpp
@@ -27,8 +27,10 @@
// existing one, DELETE on a delete sign), AFTER rebuilt for a fixed
// partial update, the BEFORE image mirroring history, the
// zero-value-column no-op, and the sequence-column source path
-// Rejects* flexible partial update, a fixed partial update of the wrong
-// width, a missing source schema, and a negative segment id
+// Flexible partial update materialization through the MoW derive stage,
+// including aligned LSN and operation output
+// Rejects* a fixed partial update of the wrong width, a missing source
+// schema, and a negative segment id
//
// Oracle values are ported from the deleted RowBinlogSegmentWriter.
@@ -39,6 +41,7 @@
#include <vector>
#include "common/config.h"
+#include "core/column/column_complex.h"
#include "core/column/column_nullable.h"
#include "core/column/column_vector.h"
#include "storage/binlog.h"
@@ -1393,16 +1396,25 @@ TEST_F(RowBinlogDeriveTest,
MowSeqSourceSeqLosesStillReadsHistory) {
}
// ===========================================================================
-// §5.4 Reject / negative paths.
+// §5.4 Flexible support and reject / negative paths.
// ===========================================================================
-// N4 (B9): binlog<row> does not support flexible partial update.
-TEST_F(RowBinlogDeriveTest, MowRejectsFlexiblePartialUpdate) {
- auto binlog_tablet =
- create_binlog_tablets(/*tablet_id=*/8501, TKeysType::DUP_KEYS,
/*mow=*/false).binlog;
- ASSERT_TRUE(binlog_tablet != nullptr);
- auto binlog_schema = binlog_tablet->tablet_schema();
- auto source_schema = create_binlog_pu_source_schema();
+// A flexible partial update reaches the same MoW derive stage as a fixed
+// partial update, but materializes its full AFTER row from the skip bitmap.
+TEST_F(RowBinlogDeriveTest, MowMaterializesFlexiblePartialUpdate) {
+ TabletSchemaPB source_pb;
+ create_flexible_mow_schema()->to_schema_pb(&source_pb);
+ source_pb.mutable_column(source_pb.delete_sign_idx())->set_visible(false);
+
source_pb.mutable_column(source_pb.skip_bitmap_col_idx())->set_visible(false);
+ auto source_schema = std::make_shared<TabletSchema>();
+ source_schema->init_from_pb(source_pb);
+
+ TabletSchemaPB binlog_pb;
+ create_binlog_seq_schema()->to_schema_pb(&binlog_pb);
+ binlog_pb.mutable_column(0)->set_name("k");
+ binlog_pb.mutable_column(1)->set_name("v");
+ auto binlog_schema = std::make_shared<TabletSchema>();
+ binlog_schema->init_from_pb(binlog_pb);
auto probe_tablet = make_tablet(source_schema, /*tablet_id=*/8502);
auto mow = make_mow_context(100, {});
@@ -1425,7 +1437,7 @@ TEST_F(RowBinlogDeriveTest,
MowRejectsFlexiblePartialUpdate) {
cfg.source.is_transient_rowset_writer = false;
cfg.source.mow_context = mow;
cfg.write_before = false;
- register_segment_lsns(rwc, 2);
+ register_segment_lsns(rwc, 1);
auto chain = build_transform_chain(rwc);
EXPECT_EQ(chain.stage_names(), (std::vector<std::string_view>
{"MowRowBinlogDerive"}));
@@ -1435,19 +1447,25 @@ TEST_F(RowBinlogDeriveTest,
MowRejectsFlexiblePartialUpdate) {
ctx.mow_context = mow;
ctx.partial_update_info = pui;
- Block block = source_schema->create_storage_block({0, 1});
+ Block block = source_schema->create_storage_block();
{
auto guard = block.mutate_columns_scoped();
auto& cols = guard.mutable_columns();
int32_t k = 1, v1 = 10;
+ int8_t delete_sign = 0;
cols[0]->insert_data(reinterpret_cast<const char*>(&k),
sizeof(int32_t));
cols[1]->insert_data(reinterpret_cast<const char*>(&v1),
sizeof(int32_t));
+ cols[2]->insert_data(reinterpret_cast<const char*>(&delete_sign),
sizeof(int8_t));
+ assert_cast<ColumnBitmap*>(cols[3].get())->insert_value(BitmapValue
{});
}
- auto st = chain.apply(ctx, &block);
- EXPECT_FALSE(st.ok());
- EXPECT_NE(st.to_string().find("does not support flexible partial update"),
std::string::npos)
- << st;
+ ASSERT_TRUE(chain.apply(ctx, &block).ok());
+ ASSERT_EQ(block.columns(), binlog_schema->num_columns());
+ ASSERT_EQ(block.rows(), 1);
+ EXPECT_EQ(read_int(block, 0, 0), 1);
+ EXPECT_EQ(read_int(block, 1, 0), 10);
+ EXPECT_EQ(read_lsn(block, binlog_schema->binlog_lsn_col_idx(), 0), 1000);
+ EXPECT_EQ(read_op(block, binlog_schema->binlog_op_col_idx(), 0),
ROW_BINLOG_APPEND);
}
// N5/N6 (B10): the fixed-PU MoW derive rejects a block that is too narrow
diff --git
a/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update.out
b/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update.out
new file mode 100644
index 00000000000..faffe70fcb9
--- /dev/null
+++
b/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update.out
@@ -0,0 +1,69 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !flexible_rejected_sequence_visible --
+5 50 e 105 500
+6 60 f 106 500
+
+-- !flexible_rejected_sequence_binlog --
+5 50 e 105 500 0
+6 60 f 106 500 0
+
+-- !flexible_delete_then_insert_visible --
+7 0 default \N 160
+
+-- !flexible_delete_then_insert_binlog --
+0 7 70 g 107 100 \N \N \N \N
+1 7 0 default \N 160 70 g 107 100
+
+-- !flexible_visible --
+1 11 aa 110
+2 20 \N 120
+3 30 default 130
+5 50 e 500
+6 60 f 500
+7 0 default 160
+
+-- !flexible_raw_binlog --
+0 1 10 a 100 \N \N \N
+0 2 20 b 100 \N \N \N
+0 4 40 d 100 \N \N \N
+0 5 50 e 500 \N \N \N
+0 6 60 f 500 \N \N \N
+0 7 70 g 100 \N \N \N
+1 1 11 aa 110 10 a 100
+1 2 20 \N 120 20 b 100
+0 3 30 default 130 \N \N \N
+2 4 40 d 140 40 d 100
+1 7 0 default 160 70 g 100
+
+-- !flexible_detail --
+1 10 a 100 0
+2 20 b 100 0
+4 40 d 100 0
+5 50 e 500 0
+6 60 f 500 0
+7 70 g 100 0
+1 10 a 100 2
+1 11 aa 110 3
+2 20 b 100 2
+2 20 \N 120 3
+3 30 default 130 0
+4 40 d 100 1
+7 70 g 100 2
+7 0 default 160 3
+
+-- !flexible_min_delta --
+1 11 aa 110 0
+2 20 \N 120 0
+3 30 default 130 0
+5 50 e 500 0
+6 60 f 500 0
+7 0 default 160 0
+
+-- !flexible_append_only --
+1 10 a 100 0
+2 20 b 100 0
+4 40 d 100 0
+5 50 e 500 0
+6 60 f 500 0
+7 70 g 100 0
+3 30 default 130 0
diff --git
a/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.out
b/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.out
new file mode 100644
index 00000000000..c2d4d89d3bd
--- /dev/null
+++
b/regression-test/data/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.out
@@ -0,0 +1,44 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !no_sequence_visible --
+1 11 aa 101
+2 20 \N 102
+3 30 default \N
+5 0 default \N
+
+-- !no_sequence_raw_binlog --
+0 1 10 a 101 \N \N \N
+0 2 20 b 102 \N \N \N
+0 4 40 d 104 \N \N \N
+0 5 50 e 105 \N \N \N
+1 1 11 aa 101 10 a 101
+1 2 20 \N 102 20 b 102
+0 3 30 default \N \N \N \N
+2 4 40 d 104 40 d 104
+1 5 0 default \N 50 e 105
+
+-- !no_sequence_detail --
+1 10 a 101 0
+2 20 b 102 0
+4 40 d 104 0
+5 50 e 105 0
+1 10 a 101 2
+1 11 aa 101 3
+2 20 b 102 2
+2 20 \N 102 3
+3 30 default \N 0
+4 40 d 104 1
+5 50 e 105 2
+5 0 default \N 3
+
+-- !no_sequence_min_delta --
+1 11 aa 101 0
+2 20 \N 102 0
+3 30 default \N 0
+5 0 default \N 0
+
+-- !no_sequence_append_only --
+1 10 a 101 0
+2 20 b 102 0
+4 40 d 104 0
+5 50 e 105 0
+3 30 default \N 0
diff --git
a/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update.groovy
b/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update.groovy
new file mode 100644
index 00000000000..81a693be9d0
--- /dev/null
+++
b/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update.groovy
@@ -0,0 +1,176 @@
+// 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.
+
+suite("test_row_binlog_flexible_partial_update") {
+ def tableName = "test_row_binlog_flexible_partial_update"
+ sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+ try {
+ sql """
+ CREATE TABLE ${tableName} (
+ id INT,
+ v1 INT NOT NULL DEFAULT "0",
+ v2 VARCHAR(16) NULL DEFAULT "default",
+ v3 INT NULL,
+ seq INT
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "light_schema_change" = "true",
+ "enable_unique_key_skip_bitmap_column" = "true",
+ "function_column.sequence_col" = "seq",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+
+ sql """
+ INSERT INTO ${tableName} VALUES
+ (1, 10, 'a', 101, 100),
+ (2, 20, 'b', 102, 100),
+ (4, 40, 'd', 104, 100),
+ (5, 50, 'e', 105, 500),
+ (6, 60, 'f', 106, 500),
+ (7, 70, 'g', 107, 100)
+ """
+
+ def flexibleRows = """
+ {"id":1,"v1":11,"seq":110}
+ {"id":1,"v2":"aa"}
+ {"id":2,"v2":null,"seq":120}
+ {"id":3,"v1":30,"seq":130}
+ {"id":4,"__DORIS_DELETE_SIGN__":1,"seq":140}
+ """.stripIndent().trim()
+ streamLoad {
+ table tableName
+ set "format", "json"
+ set "read_json_by_line", "true"
+ set "strict_mode", "false"
+ set "unique_key_update_mode", "UPDATE_FLEXIBLE_COLUMNS"
+ inputStream new ByteArrayInputStream(flexibleRows.getBytes())
+ time 20000
+ }
+
+ // A single lower-sequence update loses to the visible row. It must
neither change the
+ // base table nor produce a visible Row Binlog change.
+ streamLoad {
+ table tableName
+ set "format", "json"
+ set "read_json_by_line", "true"
+ set "strict_mode", "false"
+ set "unique_key_update_mode", "UPDATE_FLEXIBLE_COLUMNS"
+ inputStream new ByteArrayInputStream(
+ '{"id":5,"v1":5000,"seq":400}'.getBytes())
+ time 20000
+ }
+
+ // The same rule applies to a lower-sequence delete.
+ streamLoad {
+ table tableName
+ set "format", "json"
+ set "read_json_by_line", "true"
+ set "strict_mode", "false"
+ set "unique_key_update_mode", "UPDATE_FLEXIBLE_COLUMNS"
+ inputStream new ByteArrayInputStream(
+ '{"id":6,"__DORIS_DELETE_SIGN__":1,"seq":400}'.getBytes())
+ time 20000
+ }
+
+ // DELETE followed by INSERT for an existing key in one load keeps the
old row as BEFORE,
+ // but omitted AFTER columns use default/null instead of resurrecting
historical values.
+ def deleteThenInsertRows = """
+ {"id":7,"__DORIS_DELETE_SIGN__":1,"seq":150}
+ {"id":7,"seq":160}
+ """.stripIndent().trim()
+ streamLoad {
+ table tableName
+ set "format", "json"
+ set "read_json_by_line", "true"
+ set "strict_mode", "false"
+ set "unique_key_update_mode", "UPDATE_FLEXIBLE_COLUMNS"
+ inputStream new
ByteArrayInputStream(deleteThenInsertRows.getBytes())
+ time 20000
+ }
+ sql "sync"
+
+ order_qt_flexible_rejected_sequence_visible """
+ SELECT id, v1, v2, v3, seq
+ FROM ${tableName}
+ WHERE id IN (5, 6)
+ ORDER BY id
+ """
+
+ qt_flexible_rejected_sequence_binlog """
+ SELECT id, v1, v2, v3, seq, __DORIS_BINLOG_OP__
+ FROM binlog("table" = "${tableName}")
+ WHERE id IN (5, 6)
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+
+ order_qt_flexible_delete_then_insert_visible """
+ SELECT id, v1, v2, v3, seq
+ FROM ${tableName}
+ WHERE id = 7
+ """
+
+ qt_flexible_delete_then_insert_binlog """
+ SELECT __DORIS_BINLOG_OP__, id, v1, v2, v3, seq,
+ __BEFORE__v1__, __BEFORE__v2__, __BEFORE__v3__,
__BEFORE__seq__
+ FROM binlog("table" = "${tableName}")
+ WHERE id = 7
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+
+ order_qt_flexible_visible """
+ SELECT id, v1, v2, seq
+ FROM ${tableName}
+ ORDER BY id
+ """
+
+ qt_flexible_raw_binlog """
+ SELECT __DORIS_BINLOG_OP__, id, v1, v2, seq,
+ __BEFORE__v1__, __BEFORE__v2__, __BEFORE__seq__
+ FROM binlog("table" = "${tableName}")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+
+ if (!isCloudMode()) {
+ qt_flexible_detail """
+ SELECT id, v1, v2, seq, __DORIS_BINLOG_OP__
+ FROM ${tableName}@incr("incrementType" = "DETAIL")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__,
__DORIS_BINLOG_OP__
+ """
+
+ order_qt_flexible_min_delta """
+ SELECT id, v1, v2, seq, __DORIS_BINLOG_OP__
+ FROM ${tableName}@incr("incrementType" = "MIN_DELTA")
+ ORDER BY id, __DORIS_BINLOG_OP__
+ """
+
+ qt_flexible_append_only """
+ SELECT id, v1, v2, seq, __DORIS_BINLOG_OP__
+ FROM ${tableName}@incr("incrementType" = "APPEND_ONLY")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+ }
+ } finally {
+ sql "DROP TABLE IF EXISTS ${tableName} FORCE"
+ }
+}
diff --git
a/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.groovy
b/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.groovy
new file mode 100644
index 00000000000..c6bf42eecd3
--- /dev/null
+++
b/regression-test/suites/row_binlog_p0/test_row_binlog_flexible_partial_update_no_sequence.groovy
@@ -0,0 +1,101 @@
+// 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.
+
+suite("test_row_binlog_flexible_partial_update_no_sequence", "nonConcurrent") {
+ sql "DROP TABLE IF EXISTS
test_row_binlog_flexible_partial_update_no_sequence FORCE"
+
+ sql """
+ CREATE TABLE test_row_binlog_flexible_partial_update_no_sequence (
+ id INT,
+ v1 INT NOT NULL DEFAULT "0",
+ v2 VARCHAR(16) NULL DEFAULT "default",
+ v3 INT NULL
+ )
+ UNIQUE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES (
+ "replication_num" = "1",
+ "enable_unique_key_merge_on_write" = "true",
+ "light_schema_change" = "true",
+ "enable_unique_key_skip_bitmap_column" = "true",
+ "binlog.enable" = "true",
+ "binlog.format" = "ROW",
+ "binlog.need_historical_value" = "true"
+ )
+ """
+
+ sql """
+ INSERT INTO test_row_binlog_flexible_partial_update_no_sequence VALUES
+ (1, 10, 'a', 101),
+ (2, 20, 'b', 102),
+ (4, 40, 'd', 104),
+ (5, 50, 'e', 105)
+ """
+
+ // Cover duplicate-key aggregation, history fill, explicit NULL, a new key,
+ // DELETE, and DELETE -> INSERT without relying on a sequence column.
+ def flexibleRows = """
+ {"id":1,"v1":11}
+ {"id":1,"v2":"aa"}
+ {"id":2,"v2":null}
+ {"id":3,"v1":30}
+ {"id":4,"__DORIS_DELETE_SIGN__":1}
+ {"id":5,"__DORIS_DELETE_SIGN__":1}
+ {"id":5}
+ """.stripIndent().trim()
+ streamLoad {
+ table "test_row_binlog_flexible_partial_update_no_sequence"
+ set "format", "json"
+ set "read_json_by_line", "true"
+ set "strict_mode", "false"
+ set "unique_key_update_mode", "UPDATE_FLEXIBLE_COLUMNS"
+ inputStream new ByteArrayInputStream(flexibleRows.getBytes())
+ time 20000
+ }
+ sql "sync"
+
+ order_qt_no_sequence_visible """
+ SELECT id, v1, v2, v3
+ FROM test_row_binlog_flexible_partial_update_no_sequence
+ ORDER BY id
+ """
+
+ qt_no_sequence_raw_binlog """
+ SELECT __DORIS_BINLOG_OP__, id, v1, v2, v3,
+ __BEFORE__v1__, __BEFORE__v2__, __BEFORE__v3__
+ FROM binlog("table" =
"test_row_binlog_flexible_partial_update_no_sequence")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+
+ qt_no_sequence_detail """
+ SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
+ FROM
test_row_binlog_flexible_partial_update_no_sequence@incr("incrementType" =
"DETAIL")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__,
__DORIS_BINLOG_OP__
+ """
+
+ order_qt_no_sequence_min_delta """
+ SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
+ FROM
test_row_binlog_flexible_partial_update_no_sequence@incr("incrementType" =
"MIN_DELTA")
+ ORDER BY id, __DORIS_BINLOG_OP__
+ """
+
+ qt_no_sequence_append_only """
+ SELECT id, v1, v2, v3, __DORIS_BINLOG_OP__
+ FROM
test_row_binlog_flexible_partial_update_no_sequence@incr("incrementType" =
"APPEND_ONLY")
+ ORDER BY __DORIS_BINLOG_TSO__, __DORIS_BINLOG_LSN__
+ """
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]