From db01a5a6029ea0b8a52ce968560c937fc01a1dbf Mon Sep 17 00:00:00 2001 From: csun5285 Date: Fri, 14 Aug 2026 15:03:33 +0800 Subject: [PATCH 1/2] [refactor](storage) move flexible partial-update fill into the chain; decouple BlockAggregator; segment writers become pure storage-format sinks Co-Authored-By: Claude Fable 5 --- be/src/storage/partial_update_info.cpp | 145 +- be/src/storage/partial_update_info.h | 30 +- be/src/storage/rowset/beta_rowset_writer.cpp | 1 - be/src/storage/rowset/segment_creator.cpp | 17 +- be/src/storage/rowset/segment_creator.h | 4 +- be/src/storage/segment/segment_writer.cpp | 17 +- be/src/storage/segment/segment_writer.h | 2 - .../segment/vertical_segment_writer.cpp | 422 +---- .../storage/segment/vertical_segment_writer.h | 52 - be/src/storage/transform/block_transform.cpp | 20 +- be/src/storage/transform/block_transform.h | 4 +- .../storage/transform/partial_update_fill.cpp | 135 ++ .../storage/transform/partial_update_fill.h | 12 + be/test/storage/mow/mow_transform_test_base.h | 58 +- .../flexible_partial_update_test.cpp | 1655 +++++++++++++++++ .../storage/transform/validate_stage_test.cpp | 18 +- 16 files changed, 2013 insertions(+), 579 deletions(-) create mode 100644 be/test/storage/transform/flexible_partial_update_test.cpp diff --git a/be/src/storage/partial_update_info.cpp b/be/src/storage/partial_update_info.cpp index 1bea6e05cb75f5..7aef98adefd0d1 100644 --- a/be/src/storage/partial_update_info.cpp +++ b/be/src/storage/partial_update_info.cpp @@ -30,18 +30,18 @@ #include "core/value/bitmap_value.h" #include "storage/iterator/olap_data_convertor.h" #include "storage/key/row_key_encoder.h" +#include "storage/mow/historical_row_fetcher.h" +#include "storage/mow/key_probe.h" #include "storage/olap_common.h" #include "storage/rowset/rowset.h" #include "storage/rowset/rowset_writer_context.h" #include "storage/segment/historical_row_retriever.h" -#include "storage/segment/vertical_segment_writer.h" #include "storage/tablet/base_tablet.h" #include "storage/tablet/tablet_meta.h" #include "storage/tablet/tablet_schema.h" #include "storage/utils.h" namespace doris { -namespace { ColumnBitmap* get_mutable_skip_bitmap_column(Block* block, size_t skip_bitmap_col_idx) { auto skip_bitmap_column = @@ -51,8 +51,6 @@ ColumnBitmap* get_mutable_skip_bitmap_column(Block* block, size_t skip_bitmap_co return skip_bitmap_column_ptr; } -} // namespace - Status PartialUpdateInfo::init(int64_t tablet_id, int64_t txn_id, const TabletSchema& tablet_schema, UniqueKeyUpdateModePB unique_key_update_mode, PartialUpdateNewRowPolicyPB policy, @@ -840,8 +838,31 @@ Status FlexibleReadPlan::fill_non_primary_key_columns_for_row_store( return Status::OK(); } -BlockAggregator::BlockAggregator(segment_v2::VerticalSegmentWriter& vertical_segment_writer) - : _writer(vertical_segment_writer), _tablet_schema(*_writer._tablet_schema) {} +BlockAggregator::BlockAggregator(TabletSchema& tablet_schema, BaseTabletSPtr tablet, + std::shared_ptr mow_context, + const PartialUpdateInfo& partial_update_info, + const RowKeyEncoder& key_encoder, + const segment_v2::MowKeyProbe& probe, + HistoricalRowFetcher& fetcher) + : _tablet_schema(tablet_schema), + _tablet(std::move(tablet)), + _mow_context(std::move(mow_context)), + _partial_update_info(partial_update_info), + _key_encoder(key_encoder), + _convertor(std::make_unique()), + _probe(probe), + _fetcher(fetcher) { + _convertor->resize(tablet_schema.num_columns()); + for (uint32_t cid = 0; cid < tablet_schema.num_key_columns(); ++cid) { + _convertor->add_column_data_convertor_at(tablet_schema.column(cid), cid); + } + if (tablet_schema.has_sequence_col()) { + auto cid = cast_set(tablet_schema.sequence_col_idx()); + _convertor->add_column_data_convertor_at(tablet_schema.column(cid), cid); + } +} + +BlockAggregator::~BlockAggregator() = default; void BlockAggregator::merge_one_row(MutableBlock& dst_block, Block* src_block, int rid, BitmapValue& skip_bitmap) { @@ -919,30 +940,26 @@ Status BlockAggregator::aggregate_rows( _state.reset(); - RowLocation loc; - RowsetSharedPtr rowset; - std::string previous_encoded_seq_value {}; - Status st = _writer._tablet->lookup_row_key( - key, &_tablet_schema, false, specified_rowsets, &loc, _writer._mow_context->max_version, - segment_caches, &rowset, true, &previous_encoded_seq_value); + auto prev_seq_res = _probe.probe_previous_seq_value(key, specified_rowsets, segment_caches); int pos = start; - bool is_expected_st = (st.is() || st.ok()); - DCHECK(is_expected_st || st.is()) + DCHECK(prev_seq_res.has_value() || prev_seq_res.error().is()) << "[BlockAggregator::aggregate_rows] unexpected error status while lookup_row_key:" - << st; - if (!is_expected_st) { - return st; + << prev_seq_res.error(); + if (!prev_seq_res.has_value()) { + return prev_seq_res.error(); } + std::string previous_encoded_seq_value = std::move(prev_seq_res.value().encoded_seq_value); + segment_v2::ProbeOutcome probe_out = std::move(prev_seq_res.value().outcome); std::string cur_seq_val; - if (st.ok()) { + if (probe_out.result == segment_v2::KeyProbeResult::FOUND) { for (pos = start; pos < end; pos++) { auto& skip_bitmap = skip_bitmaps->at(pos); bool row_has_sequence_col = (!skip_bitmap.contains(seq_col_unique_id)); // Discard all the rows whose seq value is smaller than previous_encoded_seq_value. if (row_has_sequence_col) { std::string seq_val {}; - _writer._key_encoder.append_seq_suffix(&seq_val, seq_column, pos); + _key_encoder.append_seq_suffix(&seq_val, seq_column, pos); if (Slice {seq_val}.compare(Slice {previous_encoded_seq_value}) < 0) { continue; } @@ -959,12 +976,11 @@ Status BlockAggregator::aggregate_rows( if (row_has_sequence_col) { std::string seq_val {}; // for rows that don't specify seqeunce col, seq_val will be encoded to minial value - _writer._key_encoder.append_seq_suffix(&seq_val, seq_column, pos); + _key_encoder.append_seq_suffix(&seq_val, seq_column, pos); cur_seq_val = std::move(seq_val); } else { cur_seq_val.clear(); - RETURN_IF_ERROR(_writer._generate_encoded_default_seq_value( - _tablet_schema, *_writer._opts.rowset_ctx->partial_update_info, &cur_seq_val)); + RETURN_IF_ERROR(_generate_encoded_default_seq_value(&cur_seq_val)); } } @@ -977,7 +993,7 @@ Status BlockAggregator::aggregate_rows( append_or_merge_row(output_block, block, rid, skip_bitmap, have_delete_sign); } else { std::string seq_val {}; - _writer._key_encoder.append_seq_suffix(&seq_val, seq_column, rid); + _key_encoder.append_seq_suffix(&seq_val, seq_column, rid); if (Slice {seq_val}.compare(Slice {cur_seq_val}) >= 0) { append_or_merge_row(output_block, block, rid, skip_bitmap, have_delete_sign); cur_seq_val = std::move(seq_val); @@ -990,6 +1006,33 @@ Status BlockAggregator::aggregate_rows( return Status::OK(); }; +Status BlockAggregator::_generate_encoded_default_seq_value(std::string* encoded_value) { + const auto& seq_column = _tablet_schema.column(_tablet_schema.sequence_col_idx()); + auto block = _tablet_schema.create_block_by_cids( + {cast_set(_tablet_schema.sequence_col_idx())}); + if (seq_column.has_default_value()) { + auto idx = _tablet_schema.sequence_col_idx() - _tablet_schema.num_key_columns(); + const auto& default_value = _partial_update_info.default_values[idx]; + StringRef str {default_value}; + RETURN_IF_ERROR(block.get_by_position(0).type->get_serde()->default_from_string( + str, *block.get_by_position(0).column->assert_mutable().get())); + + } else { + block.get_by_position(0).column->assert_mutable()->insert_default(); + } + DCHECK_EQ(block.rows(), 1); + auto olap_data_convertor = std::make_unique(); + olap_data_convertor->add_column_data_convertor(seq_column); + olap_data_convertor->set_source_content(&block, 0, 1); + auto [status, column] = olap_data_convertor->convert_column_data(0); + if (!status.ok()) { + return status; + } + // include marker + _key_encoder.append_seq_suffix(encoded_value, column, 0); + return Status::OK(); +} + Status BlockAggregator::aggregate_for_sequence_column( Block* block, int num_rows, const std::vector& key_columns, IOlapColumnDataAccessor* seq_column, const std::vector& specified_rowsets, @@ -1008,7 +1051,7 @@ Status BlockAggregator::aggregate_for_sequence_column( int same_key_rows {0}; std::string previous_key {}; for (int block_pos {0}; block_pos < num_rows; block_pos++) { - std::string key = _writer._key_encoder.full_encode(key_columns, block_pos); + std::string key = _key_encoder.full_encode(key_columns, block_pos); if (block_pos > 0 && previous_key == key) { same_key_rows++; } else { @@ -1042,7 +1085,7 @@ Status BlockAggregator::fill_sequence_column(Block* block, size_t num_rows, auto seq_col_block = _tablet_schema.create_block_by_cids(cids); auto tmp_block = _tablet_schema.create_block_by_cids(cids); std::map read_index; - RETURN_IF_ERROR(read_plan.read_columns_by_plan(_tablet_schema, cids, _writer._rsid_to_rowset, + RETURN_IF_ERROR(read_plan.read_columns_by_plan(_tablet_schema, cids, _fetcher.pinned_rowsets(), seq_col_block, &read_index, false)); auto new_seq_col_ptr = tmp_block.get_by_position(0).column->assert_mutable(); @@ -1085,10 +1128,20 @@ Status BlockAggregator::aggregate_for_insert_after_delete( ? _tablet_schema.column(_tablet_schema.sequence_col_idx()).unique_id() : -1; 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); - std::string key = _writer._key_encoder.full_encode(key_columns, delta_pos); + std::string key = _key_encoder.full_encode(key_columns, delta_pos); bool have_delete_sign = (!skip_bitmap.contains(delete_sign_col_unique_id) && delete_signs[block_pos] != 0); if (delta_pos > 0 && previous_key == key) { @@ -1099,22 +1152,19 @@ Status BlockAggregator::aggregate_for_insert_after_delete( DCHECK(previous_has_delete_sign); DCHECK(!have_delete_sign); ++duplicate_rows; - RowLocation loc; - RowsetSharedPtr rowset; - Status st = _writer._tablet->lookup_row_key( - key, &_tablet_schema, false, specified_rowsets, &loc, - _writer._mow_context->max_version, segment_caches, &rowset, true); - bool is_expected_st = (st.is() || st.ok()); - DCHECK(is_expected_st || st.is()) + 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()) << "[BlockAggregator::aggregate_for_insert_after_delete] unexpected error " "status while lookup_row_key:" - << st; - if (!is_expected_st) { - return st; + << probe_res.error(); + if (!probe_res.has_value()) { + return probe_res.error(); } + segment_v2::ProbeOutcome out = std::move(probe_res.value()); - Slice previous_seq_slice {}; - if (st.ok()) { + if (out.result == segment_v2::KeyProbeResult::FOUND) { if (_tablet_schema.has_sequence_col()) { // if the insert row doesn't specify the sequence column, we need to // read the historical's sequence column value so that we don't need @@ -1122,14 +1172,11 @@ Status BlockAggregator::aggregate_for_insert_after_delete( // for this row bool row_has_sequence_col = (!skip_bitmap.contains(seq_col_unique_id)); if (!row_has_sequence_col) { - read_plan.prepare_to_read(loc, block_pos); - _writer._rsid_to_rowset.emplace(rowset->rowset_id(), rowset); + read_plan.prepare_to_read(out.loc, block_pos); + _fetcher.pin_rowset(out.rowset); } } - // delete the existing row - _writer._mow_context->delete_bitmap->add( - {loc.rowset_id, loc.segment_id, DeleteBitmap::TEMP_VERSION_COMMON}, - loc.row_id); + // the old-row delete mark is set inside probe() } // and remove the row with delete sign from the current block filter_map[block_pos - 1] = 0; @@ -1170,9 +1217,9 @@ Status BlockAggregator::convert_pk_columns(Block* block, size_t row_pos, size_t std::vector& key_columns) { key_columns.clear(); for (uint32_t cid {0}; cid < _tablet_schema.num_key_columns(); cid++) { - RETURN_IF_ERROR(_writer._olap_data_convertor->set_source_content_with_specifid_column( + RETURN_IF_ERROR(_convertor->set_source_content_with_specifid_column( block->get_by_position(cid), row_pos, num_rows, cid)); - auto [status, column] = _writer._olap_data_convertor->convert_column_data(cid); + auto [status, column] = _convertor->convert_column_data(cid); if (!status.ok()) { return status; } @@ -1186,9 +1233,9 @@ Status BlockAggregator::convert_seq_column(Block* block, size_t row_pos, size_t seq_column = nullptr; if (_tablet_schema.has_sequence_col()) { auto seq_col_idx = _tablet_schema.sequence_col_idx(); - RETURN_IF_ERROR(_writer._olap_data_convertor->set_source_content_with_specifid_column( + RETURN_IF_ERROR(_convertor->set_source_content_with_specifid_column( block->get_by_position(seq_col_idx), row_pos, num_rows, seq_col_idx)); - auto [status, column] = _writer._olap_data_convertor->convert_column_data(seq_col_idx); + auto [status, column] = _convertor->convert_column_data(seq_col_idx); if (!status.ok()) { return status; } @@ -1220,7 +1267,7 @@ Status BlockAggregator::aggregate_for_flexible_partial_update( if (block->rows() != num_rows) { num_rows = block->rows(); // data in block has changed, should re-encode key columns, sequence column - _writer._olap_data_convertor->clear_source_content(); + _convertor->clear_source_content(); RETURN_IF_ERROR(convert_pk_columns(block, 0, num_rows, key_columns)); RETURN_IF_ERROR(convert_seq_column(block, 0, num_rows, seq_column)); } diff --git a/be/src/storage/partial_update_info.h b/be/src/storage/partial_update_info.h index 2066bf732e7a32..01b4bb3f30221a 100644 --- a/be/src/storage/partial_update_info.h +++ b/be/src/storage/partial_update_info.h @@ -21,12 +21,14 @@ #include #include #include +#include #include #include #include #include "common/status.h" #include "core/column/column.h" +#include "core/data_type/primitive_type.h" #include "storage/rowset/rowset_fwd.h" #include "storage/tablet/tablet_fwd.h" @@ -45,8 +47,12 @@ struct HistoricalRowRetrieverContext; struct RowsetWriterContext; struct RowsetId; class BitmapValue; +class HistoricalRowFetcher; +class OlapBlockDataConvertor; +class RowKeyEncoder; +struct MowContext; namespace segment_v2 { -class VerticalSegmentWriter; +class MowKeyProbe; } class SegmentCacheHandle; @@ -194,10 +200,18 @@ class FlexibleReadPlan { std::map>> row_store_plan; }; +ColumnBitmap* get_mutable_skip_bitmap_column(Block* block, size_t skip_bitmap_col_idx); + class BlockAggregator { public: - ~BlockAggregator() = default; - BlockAggregator(segment_v2::VerticalSegmentWriter& vertical_segment_writer); + ~BlockAggregator(); + // All references must live longer than the aggregator; the flexible fill + // stage builds everything as locals in one apply() scope. The aggregator + // owns its block convertor (key + sequence column slots). + BlockAggregator(TabletSchema& tablet_schema, BaseTabletSPtr tablet, + std::shared_ptr mow_context, + const PartialUpdateInfo& partial_update_info, const RowKeyEncoder& key_encoder, + const segment_v2::MowKeyProbe& probe, HistoricalRowFetcher& fetcher); Status convert_pk_columns(Block* block, size_t row_pos, size_t num_rows, std::vector& key_columns); @@ -237,8 +251,16 @@ class BlockAggregator { const std::vector& specified_rowsets, std::vector>& segment_caches); - segment_v2::VerticalSegmentWriter& _writer; + Status _generate_encoded_default_seq_value(std::string* encoded_value); + TabletSchema& _tablet_schema; + BaseTabletSPtr _tablet; + std::shared_ptr _mow_context; + const PartialUpdateInfo& _partial_update_info; + const RowKeyEncoder& _key_encoder; + std::unique_ptr _convertor; + const segment_v2::MowKeyProbe& _probe; + HistoricalRowFetcher& _fetcher; // used to store state when aggregating rows in block struct AggregateState { diff --git a/be/src/storage/rowset/beta_rowset_writer.cpp b/be/src/storage/rowset/beta_rowset_writer.cpp index 3fa097554f5dae..1369b336edb532 100644 --- a/be/src/storage/rowset/beta_rowset_writer.cpp +++ b/be/src/storage/rowset/beta_rowset_writer.cpp @@ -1209,7 +1209,6 @@ Status BetaRowsetWriter::create_segment_writer_for_segcompaction( writer_options.write_type = _context.write_type; writer_options.write_type = DataWriteType::TYPE_COMPACTION; writer_options.max_rows_per_segment = _context.max_rows_per_segment; - writer_options.mow_ctx = _context.mow_context; *writer = std::make_unique( file_writer.get(), _num_segcompacted, _context.tablet_schema, _context.tablet, diff --git a/be/src/storage/rowset/segment_creator.cpp b/be/src/storage/rowset/segment_creator.cpp index beecbb906728a7..2995765e3c6b8d 100644 --- a/be/src/storage/rowset/segment_creator.cpp +++ b/be/src/storage/rowset/segment_creator.cpp @@ -90,6 +90,7 @@ Status SegmentFlusher::flush_single_block(const Block* block, int32_t segment_id return Status::OK(); } Block flush_block(*block); + const size_t input_rows = flush_block.rows(); bool no_compression = flush_block.bytes() <= config::segment_compression_threshold_kb * 1024; segment_v2::DerivedColumn derived_column; RETURN_IF_ERROR(transform_block(&flush_block, segment_id, &derived_column)); @@ -111,6 +112,9 @@ Status SegmentFlusher::flush_single_block(const Block* block, int32_t segment_id RETURN_IF_ERROR_OR_CATCH_EXCEPTION(_add_rows(writer, &flush_block, 0, flush_block.rows())); RETURN_IF_ERROR(_flush_segment_writer(writer, flush_size)); } + // The caller's row accounting checks against what it fed in, so count the + // input rows, not what survived the chain. + _num_rows_written += input_rows; return Status::OK(); } @@ -119,8 +123,8 @@ Status SegmentFlusher::transform_block(Block* block, int32_t segment_id, auto transform_ctx = make_transform_exec_context(_context, segment_id); RETURN_IF_ERROR_OR_CATCH_EXCEPTION( segment_v2::build_transform_chain(_context).apply(transform_ctx, block)); - // fold the fill stages' probe counters into the flusher totals; the horizontal - // writer no longer sees partial-update rows + // fold the fill stages' probe counters into the flusher totals; the segment + // writers no longer see partial-update rows _num_rows_updated += transform_ctx.partial_update_stats.num_rows_updated; _num_rows_deleted += transform_ctx.partial_update_stats.num_rows_deleted; _num_rows_new_added += transform_ctx.partial_update_stats.num_rows_new_added; @@ -155,7 +159,6 @@ Status SegmentFlusher::_preload_segment_indexes_to_file_cache() { Status SegmentFlusher::_add_rows(std::unique_ptr& segment_writer, const Block* block, size_t row_pos, size_t num_rows) { RETURN_IF_ERROR(segment_writer->append_block(block, row_pos, num_rows)); - _num_rows_written += num_rows; return Status::OK(); } @@ -163,7 +166,6 @@ Status SegmentFlusher::_add_rows(std::unique_ptrbatch_block(block, row_pos, num_rows)); RETURN_IF_ERROR(segment_writer->write_batch()); - _num_rows_written += num_rows; return Status::OK(); } @@ -182,7 +184,6 @@ Status SegmentFlusher::_create_segment_writer(std::unique_ptrnum_rows_written(); - _num_rows_updated += writer->num_rows_updated(); - _num_rows_deleted += writer->num_rows_deleted(); - _num_rows_new_added += writer->num_rows_new_added(); - _num_rows_filtered += writer->num_rows_filtered(); - if (row_num == 0) { return Status::OK(); } diff --git a/be/src/storage/rowset/segment_creator.h b/be/src/storage/rowset/segment_creator.h index 63312dd47f4ae5..d2edfcd4c386ab 100644 --- a/be/src/storage/rowset/segment_creator.h +++ b/be/src/storage/rowset/segment_creator.h @@ -135,7 +135,9 @@ class SegmentFlusher { ~Writer(); Status add_rows(const Block* block, size_t row_offset, size_t input_row_num) { - return _flusher->_add_rows(_writer, block, row_offset, input_row_num); + RETURN_IF_ERROR(_flusher->_add_rows(_writer, block, row_offset, input_row_num)); + _flusher->_num_rows_written += input_row_num; + return Status::OK(); } Status flush(); diff --git a/be/src/storage/segment/segment_writer.cpp b/be/src/storage/segment/segment_writer.cpp index ed2b403c75fee1..62a8f036ed83ca 100644 --- a/be/src/storage/segment/segment_writer.cpp +++ b/be/src/storage/segment/segment_writer.cpp @@ -99,8 +99,7 @@ SegmentWriter::SegmentWriter(io::FileWriter* file_writer, uint32_t segment_id, _file_writer(file_writer), _index_file_writer(index_file_writer), _mem_tracker(std::make_unique(segment_mem_tracker_name(segment_id))), - _key_encoder(*_tablet_schema, _is_mow()), - _mow_context(std::move(opts.mow_ctx)) { + _key_encoder(*_tablet_schema, _is_mow()) { CHECK_NOTNULL(file_writer); _num_short_key_columns = _tablet_schema->num_short_key_columns(); } @@ -334,18 +333,6 @@ Status SegmentWriter::_create_writers(const TabletSchemaSPtr& tablet_schema, } Status SegmentWriter::append_block(const Block* block, size_t row_pos, size_t num_rows) { - // Fixed partial update blocks arrive full-width, already filled by the transform - // chain; only the flexible mode still needs the vertical writer. - if (_opts.rowset_ctx->partial_update_info && - _opts.rowset_ctx->partial_update_info->is_partial_update() && - _opts.write_type == DataWriteType::TYPE_DIRECT && - !_opts.rowset_ctx->is_transient_rowset_writer && - !_opts.rowset_ctx->partial_update_info->is_fixed_partial_update()) { - return Status::NotSupported( - "SegmentWriter doesn't support flexible partial update, please set " - "enable_vertical_segment_writer=true in be.conf on all BEs to use " - "VerticalSegmentWriter."); - } if (block->columns() < _column_writers.size()) { return Status::InternalError( "block->columns() < _column_writers.size(), block->columns()=" + @@ -357,8 +344,6 @@ Status SegmentWriter::append_block(const Block* block, size_t row_pos, size_t nu << ", block->columns()=" << block->columns() << ", _column_writers.size()=" << _column_writers.size() << ", _tablet_schema->dump_structure()=" << _tablet_schema->dump_structure(); - // Blocks from the seams arrive already transformed (variants parsed, row-store - // column materialized); compaction-family callers bring rows that are already final. _olap_data_convertor->set_source_content(block, row_pos, num_rows); // convert column data from engine format to storage layer format diff --git a/be/src/storage/segment/segment_writer.h b/be/src/storage/segment/segment_writer.h index be22adcefaa9af..7f9a18caac4a71 100644 --- a/be/src/storage/segment/segment_writer.h +++ b/be/src/storage/segment/segment_writer.h @@ -73,7 +73,6 @@ struct SegmentWriterOptions { RowsetWriterContext* rowset_ctx = nullptr; DataWriteType write_type = DataWriteType::TYPE_DEFAULT; - std::shared_ptr mow_ctx; }; using TabletSharedPtr = std::shared_ptr; @@ -210,7 +209,6 @@ class SegmentWriter { faststring _min_key; faststring _max_key; - std::shared_ptr _mow_context; std::vector _primary_keys; uint64_t _primary_keys_size = 0; // variant statistics calculator for efficient stats collection diff --git a/be/src/storage/segment/vertical_segment_writer.cpp b/be/src/storage/segment/vertical_segment_writer.cpp index abbdb857bc4815..2d95ee30f2f53c 100644 --- a/be/src/storage/segment/vertical_segment_writer.cpp +++ b/be/src/storage/segment/vertical_segment_writer.cpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #include "cloud/config.h" @@ -41,13 +40,11 @@ #include "core/block/block.h" #include "core/block/column_with_type_and_name.h" #include "core/column/column_nullable.h" -#include "core/column/column_string.h" #include "core/column/column_vector.h" #include "core/data_type/data_type.h" #include "core/data_type/data_type_factory.hpp" #include "core/data_type/data_type_number.h" // IWYU pragma: keep #include "core/types.h" -#include "exec/common/variant_util.h" #include "io/fs/file_writer.h" #include "io/fs/local_file_system.h" #include "runtime/exec_env.h" @@ -62,7 +59,6 @@ #include "storage/key_coder.h" #include "storage/mow/key_probe.h" #include "storage/olap_common.h" -#include "storage/partial_update_info.h" #include "storage/row_cursor.h" // RowCursor // IWYU pragma: keep #include "storage/rowset/rowset_fwd.h" #include "storage/rowset/rowset_writer_context.h" // RowsetWriterContext @@ -70,10 +66,8 @@ #include "storage/segment/column_writer.h" // ColumnWriter #include "storage/segment/encoding_info.h" #include "storage/segment/external_col_meta_util.h" -#include "storage/segment/historical_row_retriever.h" #include "storage/segment/page_io.h" #include "storage/segment/page_pointer.h" -#include "storage/segment/segment_loader.h" #include "storage/segment/variant/variant_ext_meta_writer.h" #include "storage/tablet/base_tablet.h" #include "storage/tablet/tablet_schema.h" @@ -83,7 +77,6 @@ #include "util/debug_points.h" #include "util/faststring.h" #include "util/json/path_in_data.h" -#include "util/jsonb/serialize.h" namespace doris::segment_v2 { using namespace ErrorCode; @@ -95,14 +88,6 @@ inline std::string vertical_segment_writer_mem_tracker_name(uint32_t segment_id) return "VerticalSegmentWriter:Segment-" + std::to_string(segment_id); } -static ColumnBitmap* get_mutable_skip_bitmap_column(Block* block, size_t skip_bitmap_col_idx) { - auto skip_bitmap_column = - IColumn::mutate(std::move(block->get_by_position(skip_bitmap_col_idx).column)); - auto* skip_bitmap_column_ptr = assert_cast(skip_bitmap_column.get()); - block->replace_by_position(skip_bitmap_col_idx, std::move(skip_bitmap_column)); - return skip_bitmap_column_ptr; -} - VerticalSegmentWriter::VerticalSegmentWriter(io::FileWriter* file_writer, uint32_t segment_id, TabletSchemaSPtr tablet_schema, BaseTabletSPtr tablet, DataDir* data_dir, @@ -117,9 +102,7 @@ VerticalSegmentWriter::VerticalSegmentWriter(io::FileWriter* file_writer, uint32 _index_file_writer(index_file_writer), _mem_tracker(std::make_unique( vertical_segment_writer_mem_tracker_name(segment_id))), - _key_encoder(*_tablet_schema, _is_mow()), - _mow_context(std::move(opts.mow_ctx)), - _block_aggregator(*this) { + _key_encoder(*_tablet_schema, _is_mow()) { CHECK_NOTNULL(file_writer); _num_short_key_columns = _tablet_schema->num_short_key_columns(); } @@ -321,45 +304,6 @@ Status VerticalSegmentWriter::init() { return Status::OK(); } -Status VerticalSegmentWriter::_append_row_store_column(const Block& block, size_t row_pos, - size_t num_rows, uint32_t cid) { - DCHECK(_tablet_schema->column(cid).is_row_store_column()); - if (num_rows == 0) { - return Status::OK(); - } - DCHECK_LE(row_pos + num_rows, block.rows()); - - auto serdes = create_data_type_serdes(block.get_data_types()); - std::unordered_set row_store_cids_set(_tablet_schema->row_columns_uids().begin(), - _tablet_schema->row_columns_uids().end()); - size_t end_pos = row_pos + num_rows; - size_t batch_rows = _opts.num_rows_per_block; - static constexpr size_t kRowStoreBatchBytes = 4 * 1024 * 1024; - DCHECK_GT(batch_rows, 0); - for (size_t pos = row_pos; pos < end_pos;) { - size_t max_rows = std::min(batch_rows, end_pos - pos); - auto row_column = ColumnString::create(); - auto* row_store_column = row_column.get(); - size_t rows = JsonbSerializeUtil::block_to_jsonb( - *_tablet_schema, block, *row_store_column, - cast_set(_tablet_schema->num_columns()), serdes, row_store_cids_set, pos, - max_rows, kRowStoreBatchBytes); - DCHECK_GT(rows, 0); - - auto typed_column = block.get_by_position(cid); - typed_column.column = std::move(row_column); - RETURN_IF_ERROR(_olap_data_convertor->set_source_content_with_specifid_column( - typed_column, 0, rows, cid)); - auto [status, column] = _olap_data_convertor->convert_column_data(cid); - RETURN_IF_ERROR(status); - RETURN_IF_ERROR( - _column_writers[cid]->append(column->get_nullmap(), column->get_data(), rows)); - _olap_data_convertor->clear_source_content(cid); - pos += rows; - } - return Status::OK(); -} - Status VerticalSegmentWriter::_append_generated_column(const DerivedColumnGenerator& generator, const Block& block, size_t row_pos, size_t num_rows, uint32_t cid) { @@ -393,36 +337,6 @@ Status VerticalSegmentWriter::_append_generated_column(const DerivedColumnGenera return Status::OK(); } -Status VerticalSegmentWriter::_probe_key_for_mow( - const MowKeyProbe& probe, std::string key, std::size_t segment_pos, - bool have_input_seq_column, bool have_delete_sign, - const std::vector& specified_rowsets, - std::vector>& segment_caches, - bool& has_default_or_nullable, std::vector& use_default_or_null_flag, - const std::function& found_cb, - const std::function& not_found_cb, PartialUpdateStats& stats) { - ProbeOutcome outcome = - DORIS_TRY(probe.probe(key, segment_pos, have_input_seq_column, have_delete_sign, - specified_rowsets, segment_caches, stats)); - if (outcome.result == KeyProbeResult::NOT_FOUND) { - if (!have_delete_sign) { - RETURN_IF_ERROR(not_found_cb()); - } - has_default_or_nullable = true; - use_default_or_null_flag.emplace_back(true); - return Status::OK(); - } - if (outcome.use_default_or_null) { - has_default_or_nullable = true; - use_default_or_null_flag.emplace_back(true); - } else { - // partial update should not contain invisible columns - use_default_or_null_flag.emplace_back(false); - found_cb(outcome.loc, outcome.rowset); - } - return Status::OK(); -} - Status VerticalSegmentWriter::_check_column_writer_disk_capacity(size_t cid) { if (_data_dir != nullptr && _data_dir->reach_capacity_limit(_column_writers[cid]->estimate_buffer_size())) { @@ -445,339 +359,19 @@ Status VerticalSegmentWriter::_finalize_column_writer_and_update_meta(size_t cid return Status::OK(); } -Status VerticalSegmentWriter::_partial_update_preconditions_check(size_t row_pos) { - if (!_is_mow()) { - auto msg = fmt::format( - "Can only do partial update on merge-on-write unique table, but found: " - "keys_type={}, _opts.enable_unique_key_merge_on_write={}, tablet_id={}", - _tablet_schema->keys_type(), _opts.enable_unique_key_merge_on_write, - _tablet->tablet_id()); - DCHECK(false) << msg; - return Status::InternalError(msg); - } - if (_opts.rowset_ctx->partial_update_info == nullptr) { - auto msg = - fmt::format("partial_update_info should not be nullptr, please check, tablet_id={}", - _tablet->tablet_id()); - DCHECK(false) << msg; - return Status::InternalError(msg); - } - if (!_opts.rowset_ctx->partial_update_info->is_flexible_partial_update()) { - auto msg = fmt::format( - "in flexible partial update code, but update_mode={}, please check, " - "tablet_id={}", - _opts.rowset_ctx->partial_update_info->update_mode(), _tablet->tablet_id()); - DCHECK(false) << msg; - return Status::InternalError(msg); - } - if (row_pos != 0) { - auto msg = fmt::format("row_pos should be 0, but found {}, tablet_id={}", row_pos, - _tablet->tablet_id()); - DCHECK(false) << msg; - return Status::InternalError(msg); - } - return Status::OK(); -} - -Status VerticalSegmentWriter::_append_block_with_flexible_partial_content(RowsInBlock& data, - Block& full_block) { - RETURN_IF_ERROR(_partial_update_preconditions_check(data.row_pos)); - - // data.block has the same schema with full_block - DCHECK(data.block->columns() == _tablet_schema->num_columns()); - - // create full block and fill with sort key columns - full_block = _tablet_schema->create_block(); - - // Use _num_rows_written instead of creating column writer 0, since all column writers - // should have the same row count, which equals _num_rows_written. - uint32_t segment_start_pos = cast_set(_num_rows_written); - - DCHECK(_tablet_schema->has_skip_bitmap_col()); - auto skip_bitmap_col_idx = _tablet_schema->skip_bitmap_col_idx(); - - bool has_default_or_nullable = false; - std::vector use_default_or_null_flag; - use_default_or_null_flag.reserve(data.num_rows); - - int32_t seq_map_col_unique_id = _opts.rowset_ctx->partial_update_info->sequence_map_col_uid(); - bool schema_has_sequence_col = _tablet_schema->has_sequence_col(); - - DBUG_EXECUTE_IF("VerticalSegmentWriter._append_block_with_flexible_partial_content.sleep", - { sleep(60); }) - const std::vector& specified_rowsets = _mow_context->rowset_ptrs; - std::vector> segment_caches(specified_rowsets.size()); - - // Ensure all primary key column writers and sequence column writer are created before - // aggregate_for_flexible_partial_update, because it internally calls convert_pk_columns - // and convert_seq_column which need the convertors in _olap_data_convertor - for (uint32_t cid = 0; cid < _tablet_schema->num_key_columns(); ++cid) { - RETURN_IF_ERROR(_create_column_writer(cid, _tablet_schema->column(cid), _tablet_schema)); - } - if (schema_has_sequence_col) { - uint32_t cid = _tablet_schema->sequence_col_idx(); - RETURN_IF_ERROR(_create_column_writer(cid, _tablet_schema->column(cid), _tablet_schema)); - } - - // 1. aggregate duplicate rows in block - RETURN_IF_ERROR(_block_aggregator.aggregate_for_flexible_partial_update( - const_cast(data.block), data.num_rows, specified_rowsets, segment_caches)); - if (data.block->rows() != data.num_rows) { - data.num_rows = data.block->rows(); - _olap_data_convertor->clear_source_content(); - } - - // 2. encode primary key columns - // we can only encode primary key columns currently becasue all non-primary columns in flexible partial update - // can have missing cells - std::vector key_columns {}; - RETURN_IF_ERROR(_block_aggregator.convert_pk_columns(const_cast(data.block), - data.row_pos, data.num_rows, key_columns)); - // 3. encode sequence column - // We encode the seguence column even thought it may have invalid values in some rows because we need to - // encode the value of sequence column in key for rows that have a valid value in sequence column during - // lookup_raw_key. We will encode the sequence column again at the end of this method. At that time, we have - // a valid sequence column to encode the key with seq col. - IOlapColumnDataAccessor* seq_column {nullptr}; - RETURN_IF_ERROR(_block_aggregator.convert_seq_column(const_cast(data.block), - data.row_pos, data.num_rows, seq_column)); - - auto* mutable_block = const_cast(data.block); - std::vector* skip_bitmaps = - &get_mutable_skip_bitmap_column(mutable_block, skip_bitmap_col_idx)->get_data(); - const auto* delete_signs = - BaseTablet::get_delete_sign_column_data(*data.block, data.row_pos + data.num_rows); - DCHECK(delete_signs != nullptr); - - for (std::size_t cid {0}; cid < _tablet_schema->num_key_columns(); cid++) { - const auto& input_column = data.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; - } - - // 4. write primary key columns data - for (std::size_t cid {0}; cid < _tablet_schema->num_key_columns(); cid++) { - const auto& column = key_columns[cid]; - DCHECK(_column_writers[cid]->get_next_rowid() == _num_rows_written); - RETURN_IF_ERROR(_column_writers[cid]->append(column->get_nullmap(), column->get_data(), - data.num_rows)); - DCHECK(_column_writers[cid]->get_next_rowid() == _num_rows_written + data.num_rows); - RETURN_IF_ERROR(_finalize_column_writer_and_update_meta(cid)); - } - - // 5. genreate read plan - FlexibleReadPlan read_plan {_tablet_schema->has_row_store_for_all_columns()}; - PartialUpdateStats stats; - RETURN_IF_ERROR(_generate_flexible_read_plan( - read_plan, data, segment_start_pos, schema_has_sequence_col, seq_map_col_unique_id, - skip_bitmaps, key_columns, seq_column, delete_signs, specified_rowsets, segment_caches, - has_default_or_nullable, use_default_or_null_flag, stats)); - CHECK_EQ(use_default_or_null_flag.size(), data.num_rows); - - if (config::enable_merge_on_write_correctness_check) { - _tablet->add_sentinel_mark_to_delete_bitmap(_mow_context->delete_bitmap.get(), - *_mow_context->rowset_ids); - } - - // 6. read according plan to fill full_block - RETURN_IF_ERROR(read_plan.fill_non_primary_key_columns( - _opts.rowset_ctx->make_historical_row_retriever_context(), _rsid_to_rowset, - *_tablet_schema, full_block, use_default_or_null_flag, has_default_or_nullable, - segment_start_pos, cast_set(data.row_pos), data.block, skip_bitmaps)); - - // TODO(bobhan1): should we replace the skip bitmap column with empty bitmaps to reduce storage occupation? - // this column is not needed in read path for merge-on-write table - - // 7. fill row store column - for (auto cid = _tablet_schema->num_key_columns(); cid < _tablet_schema->num_columns(); cid++) { - if (!_tablet_schema->column(cid).is_row_store_column()) { - continue; - } - RETURN_IF_ERROR(_create_column_writer(cast_set(cid), _tablet_schema->column(cid), - _tablet_schema)); - RETURN_IF_ERROR(_append_row_store_column(full_block, data.row_pos, data.num_rows, - cast_set(cid))); - RETURN_IF_ERROR(_finalize_column_writer_and_update_meta(cid)); - } - - std::vector column_ids; - for (uint32_t i = 0; i < _tablet_schema->num_columns(); ++i) { - column_ids.emplace_back(i); - } - if (_opts.rowset_ctx->write_type != DataWriteType::TYPE_COMPACTION && - _tablet_schema->num_variant_columns() > 0) { - RETURN_IF_ERROR(variant_util::parse_and_materialize_variant_columns( - full_block, *_tablet_schema, column_ids)); - } - - // 8. encode and write all non-primary key columns(including sequence column if exists) - for (auto cid = _tablet_schema->num_key_columns(); cid < _tablet_schema->num_columns(); cid++) { - if (_tablet_schema->column(cid).is_row_store_column()) { - continue; - } - if (cid != _tablet_schema->sequence_col_idx()) { - RETURN_IF_ERROR(_create_column_writer(cast_set(cid), - _tablet_schema->column(cid), _tablet_schema)); - } - RETURN_IF_ERROR(_olap_data_convertor->set_source_content_with_specifid_column( - full_block.get_by_position(cid), data.row_pos, data.num_rows, - cast_set(cid))); - auto [status, column] = _olap_data_convertor->convert_column_data(cid); - if (!status.ok()) { - return status; - } - if (cid == _tablet_schema->sequence_col_idx()) { - // should use the latest encoded sequence column to build the primary index - seq_column = column; - } - DCHECK(_column_writers[cid]->get_next_rowid() == _num_rows_written); - RETURN_IF_ERROR(_column_writers[cid]->append(column->get_nullmap(), column->get_data(), - data.num_rows)); - DCHECK(_column_writers[cid]->get_next_rowid() == _num_rows_written + data.num_rows); - RETURN_IF_ERROR(_finalize_column_writer_and_update_meta(cid)); - } - - _num_rows_updated += stats.num_rows_updated; - _num_rows_deleted += stats.num_rows_deleted; - _num_rows_new_added += stats.num_rows_new_added; - _num_rows_filtered += stats.num_rows_filtered; - - if (_num_rows_written != data.row_pos || - _primary_key_index_builder->num_rows() != _num_rows_written) { - return Status::InternalError( - "Correctness check failed, _num_rows_written: {}, row_pos: {}, primary key " - "index builder num rows: {}", - _num_rows_written, data.row_pos, _primary_key_index_builder->num_rows()); - } - - // 9. build primary key index - RETURN_IF_ERROR(_generate_primary_key_index(key_columns, seq_column, data.num_rows, false)); - - _num_rows_written += data.num_rows; - DCHECK_EQ(_primary_key_index_builder->num_rows(), _num_rows_written) - << "primary key index builder num rows(" << _primary_key_index_builder->num_rows() - << ") not equal to segment writer's num rows written(" << _num_rows_written << ")"; - _olap_data_convertor->clear_source_content(); - return Status::OK(); -} - -Status VerticalSegmentWriter::_generate_encoded_default_seq_value(const TabletSchema& tablet_schema, - const PartialUpdateInfo& info, - std::string* encoded_value) { - const auto& seq_column = tablet_schema.column(tablet_schema.sequence_col_idx()); - auto block = tablet_schema.create_block_by_cids( - {cast_set(tablet_schema.sequence_col_idx())}); - if (seq_column.has_default_value()) { - auto idx = tablet_schema.sequence_col_idx() - tablet_schema.num_key_columns(); - const auto& default_value = info.default_values[idx]; - StringRef str {default_value}; - RETURN_IF_ERROR(block.get_by_position(0).type->get_serde()->default_from_string( - str, *block.get_by_position(0).column->assert_mutable().get())); - - } else { - block.get_by_position(0).column->assert_mutable()->insert_default(); - } - DCHECK_EQ(block.rows(), 1); - auto olap_data_convertor = std::make_unique(); - olap_data_convertor->add_column_data_convertor(seq_column); - olap_data_convertor->set_source_content(&block, 0, 1); - auto [status, column] = olap_data_convertor->convert_column_data(0); - if (!status.ok()) { - return status; - } - // include marker - _key_encoder.append_seq_suffix(encoded_value, column, 0); - return Status::OK(); -} - -Status VerticalSegmentWriter::_generate_flexible_read_plan( - FlexibleReadPlan& read_plan, RowsInBlock& data, size_t segment_start_pos, - bool schema_has_sequence_col, int32_t seq_map_col_unique_id, - std::vector* skip_bitmaps, - const std::vector& key_columns, - IOlapColumnDataAccessor* seq_column, const signed char* delete_signs, - const std::vector& specified_rowsets, - std::vector>& segment_caches, - bool& has_default_or_nullable, std::vector& use_default_or_null_flag, - PartialUpdateStats& stats) { - int32_t delete_sign_col_unique_id = - _tablet_schema->column(_tablet_schema->delete_sign_idx()).unique_id(); - int32_t seq_col_unique_id = - (_tablet_schema->has_sequence_col() - ? _tablet_schema->column(_tablet_schema->sequence_col_idx()).unique_id() - : -1); - MowKeyProbe probe = MowKeyProbe::for_partial_update( - _tablet.get(), _tablet_schema.get(), _tablet_schema->has_sequence_col(), _mow_context, - _opts.rowset_ctx->rowset_id, _segment_id, /*flexible=*/true); - for (size_t block_pos = data.row_pos; block_pos < data.row_pos + data.num_rows; block_pos++) { - size_t delta_pos = block_pos - data.row_pos; - size_t segment_pos = segment_start_pos + delta_pos; - auto& skip_bitmap = skip_bitmaps->at(block_pos); - - bool row_has_sequence_col = - (schema_has_sequence_col && !skip_bitmap.contains(seq_col_unique_id)); - std::string key = encode_mow_key_invalidate_cache( - _key_encoder, key_columns, seq_column, delta_pos, row_has_sequence_col, - _opts.rowset_ctx->tablet_id, *_tablet_schema, _opts.write_type); - - // mark key with delete sign as deleted. - bool have_delete_sign = - (!skip_bitmap.contains(delete_sign_col_unique_id) && delete_signs[block_pos] != 0); - - auto not_found_cb = [&]() { - return _opts.rowset_ctx->partial_update_info->handle_new_key( - *_tablet_schema, - [&]() -> std::string { - return data.block->dump_one_line( - block_pos, cast_set(_key_encoder.num_sort_key_columns())); - }, - &skip_bitmap); - }; - auto update_read_plan = [&](const RowLocation& loc, const RowsetSharedPtr& rowset) { - // the flexible fill still reads through the writer's pin map, which the block - // aggregator also feeds - _rsid_to_rowset.emplace(rowset->rowset_id(), rowset); - read_plan.prepare_to_read(loc, segment_pos, skip_bitmap); - }; - - RETURN_IF_ERROR(_probe_key_for_mow(probe, std::move(key), segment_pos, row_has_sequence_col, - have_delete_sign, specified_rowsets, segment_caches, - has_default_or_nullable, use_default_or_null_flag, - update_read_plan, not_found_cb, stats)); - } - return Status::OK(); -} - Status VerticalSegmentWriter::batch_block(const Block* block, size_t row_pos, size_t num_rows) { - // Every block arrives full-width: fixed partial update blocks are widened by the - // transform chain before they reach this writer, flexible ones carry the full - // schema plus the skip bitmap by contract. - if (block->columns() != _tablet_schema->num_columns()) { - return Status::InvalidArgument( - "illegal block columns, block columns = {}, tablet_schema columns = {}", - block->dump_structure(), _tablet_schema->dump_structure()); - } + // input width is checked by the transform chain's ValidateStage + DCHECK(block->columns() == _tablet_schema->num_columns()) + << "block columns = " << block->dump_structure() + << ", tablet_schema columns = " << _tablet_schema->dump_structure(); _batched_blocks.emplace_back(block, row_pos, num_rows); return Status::OK(); } Status VerticalSegmentWriter::write_batch() { - // Only flexible partial update still fills inside this writer; fixed blocks were - // filled by the transform chain and take the regular path below. - if (_opts.rowset_ctx->partial_update_info && - _opts.rowset_ctx->partial_update_info->is_partial_update() && - _opts.write_type == DataWriteType::TYPE_DIRECT && - !_opts.rowset_ctx->is_transient_rowset_writer && - _opts.rowset_ctx->partial_update_info->is_flexible_partial_update()) { - Block full_block; - for (auto& data : _batched_blocks) { - RETURN_IF_ERROR(_append_block_with_flexible_partial_content(data, full_block)); - } - return Status::OK(); - } - // The transform chain already validated, parsed variants and decided the derived - // (row-store) column; this writer only pumps the generator in bounded batches. + // Blocks arrive fully transformed: validated, partial-update rows filled, + // variants parsed, and the derived (row-store) column decided by the chain. + // Its generator is pumped here in bounded batches. if (_derived_column.second) { const auto& [cid, generator] = _derived_column; RETURN_IF_ERROR(_create_column_writer(cid, _tablet_schema->column(cid), _tablet_schema)); diff --git a/be/src/storage/segment/vertical_segment_writer.h b/be/src/storage/segment/vertical_segment_writer.h index 1d8ff3edbdb42e..40a33b6472fd7a 100644 --- a/be/src/storage/segment/vertical_segment_writer.h +++ b/be/src/storage/segment/vertical_segment_writer.h @@ -22,7 +22,6 @@ #include #include -#include #include #include // unique_ptr #include @@ -34,7 +33,6 @@ #include "storage/index/index_file_writer.h" #include "storage/key/row_key_encoder.h" #include "storage/olap_define.h" -#include "storage/partial_update_info.h" #include "storage/segment/column_writer.h" #include "storage/segment/segment_index_file_cache_loader.h" #include "storage/tablet/tablet.h" @@ -60,7 +58,6 @@ class FileSystem; } // namespace io namespace segment_v2 { class IndexFileWriter; -class MowKeyProbe; struct VerticalSegmentWriterOptions { uint32_t num_rows_per_block = 1024; @@ -69,7 +66,6 @@ struct VerticalSegmentWriterOptions { RowsetWriterContext* rowset_ctx = nullptr; DataWriteType write_type = DataWriteType::TYPE_DEFAULT; - std::shared_ptr mow_ctx; }; class DerivedColumnGenerator; @@ -112,11 +108,6 @@ class VerticalSegmentWriter { [[nodiscard]] uint32_t num_rows_written() const { return _num_rows_written; } - // for partial update - [[nodiscard]] int64_t num_rows_updated() const { return _num_rows_updated; } - [[nodiscard]] int64_t num_rows_deleted() const { return _num_rows_deleted; } - [[nodiscard]] int64_t num_rows_new_added() const { return _num_rows_new_added; } - [[nodiscard]] int64_t num_rows_filtered() const { return _num_rows_filtered; } [[nodiscard]] uint32_t row_count() const { return _row_count; } [[nodiscard]] uint32_t segment_id() const { return _segment_id; } @@ -161,37 +152,8 @@ class VerticalSegmentWriter { void _set_min_max_key(const Slice& key); void _set_min_key(const Slice& key); void _set_max_key(const Slice& key); - Status _append_row_store_column(const Block& block, size_t row_pos, size_t num_rows, - uint32_t cid); Status _append_generated_column(const DerivedColumnGenerator& generator, const Block& block, size_t row_pos, size_t num_rows, uint32_t cid); - // Thin wrapper over MowKeyProbe that translates a ProbeOutcome back into the out-parameters the - // flexible partial update fill loop uses. `found_cb` receives the rowset that holds `loc` and - // pins it in `_rsid_to_rowset`, which the fill still reads from. - Status _probe_key_for_mow( - const MowKeyProbe& probe, std::string key, std::size_t segment_pos, - bool have_input_seq_column, bool have_delete_sign, - const std::vector& specified_rowsets, - std::vector>& segment_caches, - bool& has_default_or_nullable, std::vector& use_default_or_null_flag, - const std::function& - found_cb, - const std::function& not_found_cb, PartialUpdateStats& stats); - Status _partial_update_preconditions_check(size_t row_pos); - Status _append_block_with_flexible_partial_content(RowsInBlock& data, Block& full_block); - Status _generate_encoded_default_seq_value(const TabletSchema& tablet_schema, - const PartialUpdateInfo& info, - std::string* encoded_value); - Status _generate_flexible_read_plan( - FlexibleReadPlan& read_plan, RowsInBlock& data, size_t segment_start_pos, - bool schema_has_sequence_col, int32_t seq_map_col_unique_id, - std::vector* skip_bitmaps, - const std::vector& key_columns, - IOlapColumnDataAccessor* seq_column, const signed char* delete_signs, - const std::vector& specified_rowsets, - std::vector>& segment_caches, - bool& has_default_or_nullable, std::vector& use_default_or_null_flag, - PartialUpdateStats& stats); Status _generate_key_index(RowsInBlock& data, std::vector& key_columns, IOlapColumnDataAccessor* seq_column, @@ -212,7 +174,6 @@ class VerticalSegmentWriter { } private: - friend class ::doris::BlockAggregator; uint32_t _segment_id; TabletSchemaSPtr _tablet_schema; BaseTabletSPtr _tablet; @@ -243,13 +204,6 @@ class VerticalSegmentWriter { // _num_rows_written means row count already written in this current column group uint32_t _num_rows_written = 0; - /** for partial update stats **/ - int64_t _num_rows_updated = 0; - int64_t _num_rows_new_added = 0; - int64_t _num_rows_deleted = 0; - // number of rows filtered in strict mode partial update - int64_t _num_rows_filtered = 0; - // _row_count means total row count of this segment // In vertical compaction row count is recorded when key columns group finish // and _num_rows_written will be updated in value column group @@ -259,16 +213,10 @@ class VerticalSegmentWriter { faststring _min_key; faststring _max_key; - std::shared_ptr _mow_context; - // group every rowset-segment row id to speed up reader - std::map _rsid_to_rowset; - std::vector _batched_blocks; // the derived column the transform chain hands off to this writer's bounded pump DerivedColumn _derived_column; - - BlockAggregator _block_aggregator; }; } // namespace segment_v2 diff --git a/be/src/storage/transform/block_transform.cpp b/be/src/storage/transform/block_transform.cpp index 1eed2ae07bdd75..c4fa0b46215611 100644 --- a/be/src/storage/transform/block_transform.cpp +++ b/be/src/storage/transform/block_transform.cpp @@ -54,8 +54,9 @@ class VariantParseStage : public BlockTransform { }; // Checks schema rules and block width for every block entering a seam. The -// writers keep transitional duplicates of these checks until later changes -// remove them: non-seam callers (compaction, index change) still rely on them. +// horizontal writer keeps a transitional duplicate of the width check until a +// later change removes it: non-seam callers (compaction, index change) still +// rely on it. class ValidateStage : public BlockTransform { public: Status apply(TransformExecContext& ctx, Block* block) const override { @@ -199,18 +200,21 @@ BlockTransformChain build_transform_chain(const RowsetWriterContext& context) { const bool rebuild_row_store = context.write_type == DataWriteType::TYPE_DIRECT || context.write_type == DataWriteType::TYPE_SCHEMA_CHANGE; if (is_partial_update_load) { + // A partial update load is always TYPE_DIRECT, so the row store is + // always rebuilt. if (context.partial_update_info->is_fixed_partial_update()) { stages.push_back(std::make_shared()); // The legacy fixed path parsed both provided and missing Variant - // columns before rebuilding RowStore. A partial update load is always - // TYPE_DIRECT, so the row store is always rebuilt. + // columns before rebuilding RowStore. stages.push_back(std::make_shared()); stages.push_back(std::make_shared()); - return BlockTransformChain {std::move(stages)}; + } else { + stages.push_back(std::make_shared()); + // The legacy flexible path rebuilt RowStore before parsing the + // filled Variant columns. + stages.push_back(std::make_shared()); + stages.push_back(std::make_shared()); } - // Flexible partial update only gets validated here for now: the vertical - // writer does its own fill, parse and row-store work until that fill - // stage moves into the chain. return BlockTransformChain {std::move(stages)}; } // Direct and schema-change writers rebuilt RowStore from the raw Variant diff --git a/be/src/storage/transform/block_transform.h b/be/src/storage/transform/block_transform.h index c9d5e8685a7069..2c7ad2405725f8 100644 --- a/be/src/storage/transform/block_transform.h +++ b/be/src/storage/transform/block_transform.h @@ -123,8 +123,8 @@ class BlockTransformChain { // - binlog sub-writer: empty for now (RowBinlogSegmentWriter still derives // the binlog rows itself; a later change moves that in here) // - fixed partial update: [Validate, FixedPartialUpdateFill, VariantParse, RowStoreFill] -// - flexible partial update: [Validate] for now (the vertical writer still does -// its own fill, parse and row-store work; a later change moves that in here) +// - flexible partial update: [Validate, FlexiblePartialUpdateFill, RowStoreFill, VariantParse] +// (row store before parse, the reverse of fixed: each order mirrors its legacy path) // - direct / schema change / transient flush: [Validate, RowStoreFill, VariantParse] // RowStoreFill is omitted when the write type does not rebuild the row-store column. BlockTransformChain build_transform_chain(const RowsetWriterContext& context); diff --git a/be/src/storage/transform/partial_update_fill.cpp b/be/src/storage/transform/partial_update_fill.cpp index e13d3e9790029c..c7da32fc07969e 100644 --- a/be/src/storage/transform/partial_update_fill.cpp +++ b/be/src/storage/transform/partial_update_fill.cpp @@ -22,6 +22,7 @@ #include "common/cast_set.h" #include "common/config.h" #include "core/block/block.h" +#include "core/value/bitmap_value.h" #include "storage/iterator/olap_data_convertor.h" #include "storage/key/row_key_encoder.h" #include "storage/mow/historical_row_fetcher.h" @@ -93,6 +94,61 @@ Status probe_and_plan(TransformExecContext& ctx, RowKeyEncoder& key_encoder, Mow return Status::OK(); } +// The probe + read-plan loop of the flexible fill. Same skeleton as the fixed +// loop above, but each row's seq/delete-sign presence comes from its skip +// bitmap and the read plan is per cell instead of whole-row. +Status probe_and_plan_flexible(TransformExecContext& ctx, RowKeyEncoder& key_encoder, + MowKeyProbe& probe, HistoricalRowFetcher& fetcher, + const std::vector& specified_rowsets, + std::vector>& segment_caches, + const std::vector& key_columns, + IOlapColumnDataAccessor* seq_column, const signed char* delete_signs, + size_t num_rows, Block* block, + std::vector& skip_bitmaps, + std::vector& use_default_or_null_flag, + bool& has_default_or_nullable) { + const TabletSchema& schema = *ctx.tablet_schema; + PartialUpdateInfo& info = *ctx.partial_update_info; + const bool schema_has_seq = schema.has_sequence_col(); + const int32_t seq_col_unique_id = + schema_has_seq ? schema.column(schema.sequence_col_idx()).unique_id() : -1; + const int32_t delete_sign_col_unique_id = schema.column(schema.delete_sign_idx()).unique_id(); + + use_default_or_null_flag.reserve(num_rows); + for (size_t pos = 0; pos < num_rows; ++pos) { + // Encode without touching the row cache: the writer's key index build + // invalidates every row's cache entry under the same conditions, so the + // erase runs once there, not twice. + // one block == one fresh segment: segment_pos == block row index + const bool row_has_seq = schema_has_seq && !skip_bitmaps[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[pos].contains(delete_sign_col_unique_id) && delete_signs[pos] != 0; + ProbeOutcome out = + DORIS_TRY(probe.probe(key, /*segment_pos=*/pos, row_has_seq, have_delete_sign, + specified_rowsets, segment_caches, ctx.partial_update_stats)); + if (out.result == KeyProbeResult::NOT_FOUND && !have_delete_sign) { + RETURN_IF_ERROR(info.handle_new_key( + schema, + [&]() -> std::string { + return block->dump_one_line(pos, cast_set(schema.num_key_columns())); + }, + &skip_bitmaps[pos])); + } + has_default_or_nullable |= out.use_default_or_null; + use_default_or_null_flag.emplace_back(out.use_default_or_null); + if (!out.use_default_or_null) { + fetcher.pin_rowset(out.rowset); + fetcher.plan_flexible_read(out.loc, pos, skip_bitmaps[pos]); + } + } + CHECK_EQ(use_default_or_null_flag.size(), num_rows); + return Status::OK(); +} + } // namespace Status FixedPartialUpdateFillStage::apply(TransformExecContext& ctx, Block* block) const { @@ -159,4 +215,83 @@ Status FixedPartialUpdateFillStage::apply(TransformExecContext& ctx, Block* bloc return Status::OK(); } +Status FlexiblePartialUpdateFillStage::apply(TransformExecContext& ctx, Block* block) const { + DBUG_EXECUTE_IF("VerticalSegmentWriter._append_block_with_flexible_partial_content.sleep", + { sleep(60); }) + + const TabletSchemaSPtr& tablet_schema = ctx.tablet_schema; + TabletSchema& schema = *tablet_schema; + auto& info = *ctx.partial_update_info; + + DCHECK(block->columns() == schema.num_columns()); + DCHECK(schema.has_skip_bitmap_col()); + const auto skip_bitmap_col_idx = schema.skip_bitmap_col_idx(); + + Block full_block = schema.create_block(); + + const std::vector& specified_rowsets = ctx.mow_context->rowset_ptrs; + std::vector> segment_caches(specified_rowsets.size()); + + // encoder shared with the aggregator, which owns the conversion code + RowKeyEncoder key_encoder(schema, /*mow=*/true); + // FE forbids partial update on mow tables with cluster keys; everything + // below assumes sort keys == schema keys + DCHECK_EQ(key_encoder.num_sort_key_columns(), schema.num_key_columns()); + + MowKeyProbe probe = MowKeyProbe::for_partial_update( + ctx.tablet.get(), tablet_schema.get(), schema.has_sequence_col(), ctx.mow_context, + ctx.rowset_id, cast_set(ctx.segment_id), /*flexible=*/true); + HistoricalRowFetcher fetcher(ctx.rowset_ctx->make_historical_row_retriever_context()); + BlockAggregator aggregator(schema, ctx.tablet, ctx.mow_context, info, key_encoder, probe, + fetcher); + + // 1. aggregate duplicate keys inside the block; the row set may shrink + size_t num_rows = block->rows(); + RETURN_IF_ERROR(aggregator.aggregate_for_flexible_partial_update( + block, num_rows, specified_rowsets, segment_caches)); + num_rows = block->rows(); + + // 2. encode primary key columns + sequence column + std::vector 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)); + + std::vector* skip_bitmaps = + &get_mutable_skip_bitmap_column(block, skip_bitmap_col_idx)->get_data(); + const auto* delete_signs = BaseTablet::get_delete_sign_column_data(*block, num_rows); + DCHECK(delete_signs != nullptr); + + for (size_t cid = 0; cid < schema.num_key_columns(); ++cid) { + // Carry the input's type along with its column: a variant V2 input column + // is typed differently from the slot the schema-created block holds. + 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; + } + + // 3. probe every key against the load's rowset snapshot + bool has_default_or_nullable = false; + std::vector use_default_or_null_flag; + RETURN_IF_ERROR(probe_and_plan_flexible(ctx, key_encoder, probe, fetcher, specified_rowsets, + segment_caches, key_columns, seq_column, delete_signs, + num_rows, block, *skip_bitmaps, + use_default_or_null_flag, has_default_or_nullable)); + + maybe_add_sentinel_mark(ctx); + + // 4. fill the non-primary-key columns one cell at a time, as marked by the + // skip bitmap + RETURN_IF_ERROR(fetcher.fill_non_primary_key_columns( + schema, full_block, use_default_or_null_flag, has_default_or_nullable, + /*segment_start_pos=*/0, /*block_start_pos=*/0, block, skip_bitmaps)); + // TODO(bobhan1): should we replace the skip bitmap column with empty bitmaps to reduce + // storage occupation? this column is not needed in read path for merge-on-write table + + // 5. swap in the filled block; downstream it looks like a plain upsert + block->swap(full_block); + return Status::OK(); +} + } // namespace doris::segment_v2 diff --git a/be/src/storage/transform/partial_update_fill.h b/be/src/storage/transform/partial_update_fill.h index 4b985f3ff1fa91..ee9d210d7e8ed2 100644 --- a/be/src/storage/transform/partial_update_fill.h +++ b/be/src/storage/transform/partial_update_fill.h @@ -31,4 +31,16 @@ class FixedPartialUpdateFillStage : public BlockTransform { std::string_view name() const override { return "FixedPartialUpdateFill"; } }; +// Flexible partial update (UPDATE_FLEXIBLE_COLUMNS): the input is full-width +// with a skip bitmap marking each row's missing cells. Aggregate duplicate keys +// inside the block (the row set may shrink, even to empty), probe each key, and +// fill the missing cells from history or defaults. Delete-bitmap marks are +// written right away inside apply(); probe counters land in +// ctx.partial_update_stats. +class FlexiblePartialUpdateFillStage : public BlockTransform { +public: + Status apply(TransformExecContext& ctx, Block* block) const override; + std::string_view name() const override { return "FlexiblePartialUpdateFill"; } +}; + } // namespace doris::segment_v2 diff --git a/be/test/storage/mow/mow_transform_test_base.h b/be/test/storage/mow/mow_transform_test_base.h index 576edf8be7aefe..543be0a67158f5 100644 --- a/be/test/storage/mow/mow_transform_test_base.h +++ b/be/test/storage/mow/mow_transform_test_base.h @@ -186,7 +186,7 @@ class MowTransformTestBase : public testing::Test { const TabletSharedPtr& tablet, const std::shared_ptr& mow_context, const std::shared_ptr& partial_update_info, const std::vector& blocks, RowsetSharedPtr* rowset, - PartialUpdateStats* stats_out = nullptr) { + PartialUpdateStats* stats_out = nullptr, int64_t* writer_num_rows = nullptr) { RowsetWriterContext context; TabletSharedPtr unused_tablet; make_rowset_ctx(schema, rowset_numeric_id, version, &context, &unused_tablet); @@ -209,6 +209,11 @@ class MowTransformTestBase : public testing::Test { RETURN_IF_ERROR(writer->flush_memtable(block, segment_id++, nullptr)); } RETURN_IF_ERROR(writer->flush()); + if (writer_num_rows != nullptr) { + // the flusher-level input-row counter behind RowsetWriter::num_rows(), + // which the load-close check compares against the received rows + *writer_num_rows = writer->num_rows(); + } if (stats_out != nullptr) { stats_out->num_rows_updated = writer->num_rows_updated(); stats_out->num_rows_deleted = writer->num_rows_deleted(); @@ -223,10 +228,11 @@ class MowTransformTestBase : public testing::Test { const std::shared_ptr& mow_context, const std::shared_ptr& partial_update_info, Block* block, RowsetSharedPtr* rowset, - PartialUpdateStats* stats_out = nullptr) { + PartialUpdateStats* stats_out = nullptr, + int64_t* writer_num_rows = nullptr) { return flush_partial_rowset_segments(schema, rowset_numeric_id, version, tablet, mow_context, partial_update_info, {block}, rowset, - stats_out); + stats_out, writer_num_rows); } // Reads every row of `rowset` back into `output` in key order, all columns. @@ -312,7 +318,9 @@ class MowTransformTestBase : public testing::Test { // (k INT key, v INT, delete-sign, __DORIS_SKIP_BITMAP_COL__) flexible partial update MoW // schema: flexible loads carry a full-width block plus the per-row skip bitmap. - TabletSchemaSPtr create_flexible_mow_schema() { + // Flexible MoW schema. Without seq: k(0) v(1) delete_sign(2) skip_bitmap(3). + // With seq: k(0) v(1) seq(2) delete_sign(3) skip_bitmap(4). + TabletSchemaSPtr create_flexible_mow_schema(bool has_seq = false) { TabletSchemaPB pb; pb.set_keys_type(UNIQUE_KEYS); pb.set_num_short_key_columns(1); @@ -344,14 +352,23 @@ class MowTransformTestBase : public testing::Test { c->set_default_value(def); } }; - add_col(0, "k", "INT", true, false); - add_col(1, "v", "INT", false, true, std::to_string(0)); - add_col(2, DELETE_SIGN, "TINYINT", false, false, std::to_string(0)); - add_col(3, SKIP_BITMAP_COL, "BITMAP", false, false); + int next = 0; + add_col(next, "k", "INT", true, false); + ++next; + add_col(next, "v", "INT", false, true, std::to_string(0)); + ++next; + if (has_seq) { + add_col(next, SEQUENCE_COL, "INT", false, false, std::to_string(0)); + pb.set_sequence_col_idx(next); + ++next; + } + add_col(next, DELETE_SIGN, "TINYINT", false, false, std::to_string(0)); // init_from_pb reads these hidden-column indices straight from the PB // fields (it does not scan by name), so they must be set explicitly. - pb.set_delete_sign_idx(2); - pb.set_skip_bitmap_col_idx(3); + pb.set_delete_sign_idx(next); + ++next; + add_col(next, SKIP_BITMAP_COL, "BITMAP", false, false); + pb.set_skip_bitmap_col_idx(next); auto schema = std::make_shared(); schema->init_from_pb(pb); @@ -477,6 +494,27 @@ class MowTransformTestBase : public testing::Test { return rowset; } + // Like write_rowset, but the caller fills the block itself: for schemas whose + // column layout MowRow cannot express. + RowsetSharedPtr write_rowset_block(const TabletSchemaSPtr& schema, int64_t rowset_numeric_id, + int64_t version, const std::function& fill, + TabletSharedPtr* out_tablet) { + RowsetWriterContext ctx; + make_rowset_ctx(schema, rowset_numeric_id, version, &ctx, out_tablet); + auto rw = RowsetFactory::create_rowset_writer(*_engine, ctx, false); + EXPECT_TRUE(rw.has_value()) << rw.error(); + auto writer = std::move(rw).value(); + + Block block = schema->create_block(); + fill(block); + EXPECT_TRUE(writer->add_block(&block).ok()); + EXPECT_TRUE(writer->flush().ok()); + RowsetSharedPtr rowset; + EXPECT_TRUE(writer->build(rowset).ok()); + EXPECT_TRUE(rowset != nullptr); + return rowset; + } + std::shared_ptr make_mow_context(int64_t version, const std::vector& rowsets) { auto rsids = std::make_shared(); diff --git a/be/test/storage/transform/flexible_partial_update_test.cpp b/be/test/storage/transform/flexible_partial_update_test.cpp new file mode 100644 index 00000000000000..56c2354d25b1da --- /dev/null +++ b/be/test/storage/transform/flexible_partial_update_test.cpp @@ -0,0 +1,1655 @@ +// 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. + +// Full-branch-coverage tests for the FLEXIBLE partial-update fill stage and the +// in-block BlockAggregator it drives. The fill path under test is +// FlexiblePartialUpdateFillStage::apply -> BlockAggregator (dedup by sequence, +// insert-after-delete) -> probe_and_plan -> MowKeyProbe::probe -> +// FlexibleReadPlan::fill_non_primary_key_columns. +// +// Test groups: +// Flexible* happy paths: per-cell fill from history/defaults, delete sign, +// insert-after-delete (new and existing key), seq dedup +// Agg* BlockAggregator::aggregate_rows branch map, one test per arm: +// FOUND baseline {stale-first-row skipped / first-row-with-seq +// starts / first-row-without-seq inherits}, NOT_FOUND baseline +// {own seq starts / default seq}, main loop {no-seq direct-fed / +// accepted / out-of-order discarded}, all-rows-stale empty +// output, delete-inside-group clears state, insert-after-delete +// seq inheritance +// *Persists* end-to-end through the vertical writer and a read-back +// +// Oracle values are ported from the deleted writer implementation +// (VerticalSegmentWriter::_append_block_with_flexible_partial_content), with +// weak assertions upgraded to exact delete-bitmap membership + cardinality. + +#include + +#include +#include +#include +#include + +#include "common/config.h" +#include "core/column/column_complex.h" +#include "core/value/bitmap_value.h" +#include "storage/mow/mow_transform_test_base.h" +#include "storage/partial_update_info.h" +#include "storage/tablet/tablet_meta.h" +#include "storage/transform/block_transform.h" +#include "storage/transform/partial_update_fill.h" + +namespace doris { + +using segment_v2::FlexiblePartialUpdateFillStage; +using segment_v2::TransformExecContext; + +class FlexiblePartialUpdateTest : public MowTransformTestBase { +protected: + // The per-write RowsetWriterContext the stage reads through + // ctx.rowset_ctx->make_historical_row_retriever_context(). + void fill_rowset_ctx(RowsetWriterContext* rwc, const TabletSchemaSPtr& schema, + const TabletSharedPtr& tablet, std::shared_ptr pui, + const RowsetId& new_rsid) { + rwc->tablet_id = kTabletId; + rwc->tablet = tablet; + rwc->tablet_schema = schema; + rwc->partial_update_info = std::move(pui); + rwc->is_transient_rowset_writer = false; + rwc->write_type = DataWriteType::TYPE_DIRECT; + rwc->rowset_id = new_rsid; + } + + TransformExecContext make_exec_ctx(const TabletSchemaSPtr& schema, + const TabletSharedPtr& tablet, + const std::shared_ptr& mow, + const std::shared_ptr& pui, + RowsetWriterContext* rwc, const RowsetId& new_rsid) { + TransformExecContext ctx; + ctx.tablet_schema = schema; + ctx.write_type = DataWriteType::TYPE_DIRECT; + ctx.tablet = tablet; + ctx.mow_context = mow; + ctx.partial_update_info = pui; + ctx.rowset_ctx = rwc; + ctx.rowset_id = new_rsid; + ctx.segment_id = 0; + return ctx; + } + + std::shared_ptr make_flexible_pui( + const TabletSchemaSPtr& schema, + PartialUpdateNewRowPolicyPB policy = PartialUpdateNewRowPolicyPB::APPEND) { + auto pui = std::make_shared(); + EXPECT_TRUE(pui->init(kTabletId, 1, *schema, UniqueKeyUpdateModePB::UPDATE_FLEXIBLE_COLUMNS, + policy, {}, false, 0, 0, "UTC", "") + .ok()); + return pui; + } + + // (k INT key, v INT NOT NULL no default, delete-sign, skip-bitmap) flexible + // schema: a new key whose insert skips `v` can neither default nor null it. + TabletSchemaSPtr create_flexible_required_value_schema() { + TabletSchemaPB pb; + pb.set_keys_type(UNIQUE_KEYS); + pb.set_num_short_key_columns(1); + pb.set_num_rows_per_row_block(1024); + pb.set_compress_kind(COMPRESS_LZ4); + pb.set_next_column_unique_id(10); + + auto add_col = [&](int uid, const std::string& name, const std::string& type, bool is_key, + bool nullable, const std::string& def = "") { + ColumnPB* c = pb.add_column(); + c->set_unique_id(uid); + c->set_name(name); + c->set_type(type); + c->set_is_key(is_key); + int len = 4; + if (type == "TINYINT") { + len = 1; + } else if (type == "BITMAP") { + len = 16; + } + c->set_length(len); + c->set_index_length(len); + c->set_is_nullable(nullable); + c->set_aggregation("NONE"); + if (!def.empty()) { + c->set_default_value(def); + } + }; + add_col(0, "k", "INT", true, false); + add_col(1, "v", "INT", false, /*nullable=*/false); // NOT NULL, no default + add_col(2, DELETE_SIGN, "TINYINT", false, false, std::to_string(0)); + add_col(3, SKIP_BITMAP_COL, "BITMAP", false, false); + pb.set_delete_sign_idx(2); + pb.set_skip_bitmap_col_idx(3); + + auto schema = std::make_shared(); + schema->init_from_pb(pb); + return schema; + } + + // Runs FlexiblePartialUpdateFillStage with the MoW correctness-check sentinel + // disabled (so the delete bitmap holds only the real marks and exact + // cardinality / contains assertions are meaningful). + Status run_flexible_fill(TransformExecContext& ctx, Block* block) { + auto saved = config::enable_merge_on_write_correctness_check; + config::enable_merge_on_write_correctness_check = false; + FlexiblePartialUpdateFillStage stage; + auto st = stage.apply(ctx, block); + config::enable_merge_on_write_correctness_check = saved; + return st; + } + + // The two value columns / sequence / delete-sign / skip-bitmap flexible-seq + // Two-value-column flexible-seq schema: k(0) v1(1) v2(2) seq(3) + // delete_sign(4) skip_bitmap(5). create_flexible_mow_schema(true) (one value + // col) cannot express "insert provides v1 but skips v2", which the + // no-resurrect tests need; this local builder adds the second value column. + TabletSchemaSPtr create_flexible_seq2_schema() { + TabletSchemaPB pb; + pb.set_keys_type(UNIQUE_KEYS); + pb.set_num_short_key_columns(1); + pb.set_num_rows_per_row_block(1024); + pb.set_compress_kind(COMPRESS_LZ4); + pb.set_next_column_unique_id(10); + + auto add_col = [&](int uid, const std::string& name, const std::string& type, bool is_key, + bool nullable, const std::string& def = "") { + ColumnPB* c = pb.add_column(); + c->set_unique_id(uid); + c->set_name(name); + c->set_type(type); + c->set_is_key(is_key); + int len = 4; + if (type == "TINYINT") { + len = 1; + } else if (type == "BITMAP") { + len = 16; + } + c->set_length(len); + c->set_index_length(len); + c->set_is_nullable(nullable); + c->set_aggregation("NONE"); + if (!def.empty()) { + c->set_default_value(def); + } + }; + add_col(0, "k", "INT", true, false); + add_col(1, "v1", "INT", false, true, std::to_string(0)); + add_col(2, "v2", "INT", false, true, std::to_string(0)); + add_col(3, SEQUENCE_COL, "INT", false, false, std::to_string(0)); + add_col(4, DELETE_SIGN, "TINYINT", false, false, std::to_string(0)); + add_col(5, SKIP_BITMAP_COL, "BITMAP", false, false); + pb.set_sequence_col_idx(3); + pb.set_delete_sign_idx(4); + pb.set_skip_bitmap_col_idx(5); + + auto schema = std::make_shared(); + schema->init_from_pb(pb); + return schema; + } + + // Writes a single history row of the create_flexible_seq2_schema layout. + RowsetSharedPtr write_seq2_history(const TabletSchemaSPtr& schema, int64_t rsid_num, int32_t k, + int32_t v1, int32_t v2, int32_t seq, + TabletSharedPtr* out_tablet) { + return write_rowset_block( + schema, rsid_num, /*version=*/2, + [&](Block& b) { + auto guard = b.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&v2), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&seq), sizeof(int32_t)); + cols[4]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + cols[5]->insert_default(); // empty skip bitmap + }, + out_tablet); + } + + // One flexible-seq2 input row builder. Each give_* says the row provides that + // column; every column with give_*==false is put into the skip bitmap. + struct Seq2Row { + int32_t k; + int32_t v1; + int32_t v2; + int32_t seq; + int8_t delete_sign; + bool give_v1; + bool give_v2; + bool give_seq; + bool give_ds; + }; + + void append_seq2_row(const TabletSchemaSPtr& schema, MutableColumns& cols, const Seq2Row& r) { + cols[0]->insert_data(reinterpret_cast(&r.k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&r.v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&r.v2), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&r.seq), sizeof(int32_t)); + cols[4]->insert_data(reinterpret_cast(&r.delete_sign), sizeof(int8_t)); + BitmapValue skip; + if (!r.give_v1) { + skip.add(static_cast(schema->column(1).unique_id())); + } + if (!r.give_v2) { + skip.add(static_cast(schema->column(2).unique_id())); + } + if (!r.give_seq) { + skip.add(static_cast(schema->column(3).unique_id())); + } + if (!r.give_ds) { + skip.add(static_cast(schema->column(4).unique_id())); + } + assert_cast(cols[5].get())->insert_value(std::move(skip)); + } + + // (k INT key, v INT nullable, delete_sign, skip_bitmap, __DORIS_ROW_STORE_COL__) + // flexible MoW schema with the hidden full row-store column on, exercising + // FlexibleReadPlan::fill_non_primary_key_columns_for_row_store: a skipped cell + // is restored through fetch_value_through_row_column instead of the column store. + TabletSchemaSPtr create_flexible_row_store_schema() { + TabletSchemaPB pb; + pb.set_keys_type(UNIQUE_KEYS); + pb.set_num_short_key_columns(1); + pb.set_num_rows_per_row_block(1024); + pb.set_compress_kind(COMPRESS_LZ4); + pb.set_next_column_unique_id(10); + pb.set_store_row_column(true); + + auto add = [&](int uid, const std::string& name, const std::string& type, bool is_key, + int len, bool nullable, const std::string& def = "") { + ColumnPB* c = pb.add_column(); + c->set_unique_id(uid); + c->set_name(name); + c->set_type(type); + c->set_is_key(is_key); + c->set_length(len); + c->set_index_length(type == "STRING" ? 4 : len); + c->set_is_nullable(nullable); + c->set_is_bf_column(false); + c->set_aggregation("NONE"); + if (!def.empty()) { + c->set_default_value(def); + } + }; + // The hidden row-store / skip-bitmap / delete-sign columns are non-nullable, + // matching create_row_store_schema and create_flexible_mow_schema. + add(0, "k", "INT", true, 4, false); + add(1, "v", "INT", false, 4, true, std::to_string(0)); + add(2, DELETE_SIGN, "TINYINT", false, 1, false, std::to_string(0)); + add(3, SKIP_BITMAP_COL, "BITMAP", false, 16, false); + add(4, BeConsts::ROW_STORE_COL, "STRING", false, 2147483643, false); + pb.set_delete_sign_idx(2); + pb.set_skip_bitmap_col_idx(3); + + auto schema = std::make_shared(); + schema->init_from_pb(pb); + return schema; + } + + // Index the result block of create_flexible_seq2_schema by key. + struct Seq2Out { + int32_t v1; + int32_t v2; + int32_t seq; + int8_t ds; + }; + std::map index_seq2(const Block& block) { + std::map by_key; + for (size_t r = 0; r < block.rows(); ++r) { + by_key[read_int(block, 0, r)] = Seq2Out {.v1 = read_int(block, 1, r), + .v2 = read_int(block, 2, r), + .seq = read_int(block, 3, r), + .ds = read_tinyint(block, 4, r)}; + } + return by_key; + } +}; + +// =========================================================================== +// Happy-path fill tests. +// =========================================================================== + +// Flexible PU: a full-width input whose per-row skip bitmap marks `v` as not +// provided. An existing key takes the old `v` from history, a brand-new key +// takes the column default. +TEST_F(FlexiblePartialUpdateTest, FlexibleFillFromHistoryAndDefault) { + auto schema = create_flexible_mow_schema(); // k(0) v(1) delete_sign(2) skip_bitmap(3) + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3101, 2, {{1, 11}, {2, 22}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3102); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const int32_t v_uid = schema->column(1).unique_id(); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t ks[] = {1, 99}; + int32_t v_dummy = 999; + int8_t zero8 = 0; + for (int32_t k : ks) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v_dummy), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&zero8), sizeof(int8_t)); + BitmapValue skip; + skip.add(static_cast(v_uid)); + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.columns(), schema->num_columns()); + ASSERT_EQ(block.rows(), 2); + std::map kv; + for (size_t r = 0; r < block.rows(); ++r) { + kv[read_int(block, 0, r)] = read_int(block, 1, r); + } + EXPECT_EQ(kv[1], 11); // existing key: old v from history + EXPECT_EQ(kv[99], 0); // brand-new key: column default + EXPECT_EQ(ctx.partial_update_stats.num_rows_updated, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_new_added, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_deleted, 0); +} + +// Flexible PU per-cell fill: a provided cell is kept, a skipped cell is filled +// from history, on different rows in the same block. +TEST_F(FlexiblePartialUpdateTest, FlexibleProvidedCellKeptSkippedFilled) { + auto schema = create_flexible_mow_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3311, 2, {{1, 11}, {2, 22}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3312); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + const auto ds_uid = static_cast(schema->column(2).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t ks[] = {1, 2}; + int32_t vs[] = {88, 999}; + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&ks[i]), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + if (i == 1) { + skip.add(v_uid); + } + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 2); + std::map kv; + for (size_t r = 0; r < block.rows(); ++r) { + kv[read_int(block, 0, r)] = read_int(block, 1, r); + } + EXPECT_EQ(kv[1], 88); // provided cell kept + EXPECT_EQ(kv[2], 22); // skipped cell filled from history +} + +// Flexible PU delete: a row with a delete sign skips reading the old row (default +// fill) and marks the old row. +TEST_F(FlexiblePartialUpdateTest, FlexibleDeleteSign) { + auto schema = create_flexible_mow_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3321, 2, {{1, 11}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3322); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int32_t v_dummy = 999; + int8_t ds1 = 1; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v_dummy), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds1), sizeof(int8_t)); + BitmapValue skip; + skip.add(v_uid); + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 1, 0), 0); // v default (old row not read) + EXPECT_EQ(read_tinyint(block, 2, 0), 1); // delete sign kept + // exactly the one old row (history rowset seg0 row0) marked + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_EQ(mow->delete_bitmap->cardinality(), 1U); + // a delete on an existing key counts as an update, like the fixed path + EXPECT_EQ(ctx.partial_update_stats.num_rows_updated, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_deleted, 0); + EXPECT_EQ(ctx.partial_update_stats.num_rows_new_added, 0); +} + +// A lone delete-signed row on an absent key: the tombstone is kept and +// default-filled, nothing is marked, and handle_new_key is not consulted -- +// the ERROR policy must NOT fire for a delete. +TEST_F(FlexiblePartialUpdateTest, FlexibleDeleteSignNewKey) { + auto schema = create_flexible_mow_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3671, 2, {{7, 70}}, &tablet); // key 99 is absent + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema, PartialUpdateNewRowPolicyPB::ERROR); + RowsetId new_rsid; + new_rsid.init(3672); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 99, v_dummy = 999; + int8_t ds1 = 1; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v_dummy), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds1), sizeof(int8_t)); + BitmapValue skip; + skip.add(v_uid); + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 0, 0), 99); + EXPECT_EQ(read_int(block, 1, 0), 0); // v default (no old row) + EXPECT_EQ(read_tinyint(block, 2, 0), 1); // delete sign kept + EXPECT_EQ(mow->delete_bitmap->cardinality(), 0U); + EXPECT_EQ(ctx.partial_update_stats.num_rows_new_added, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_updated, 0); + EXPECT_EQ(ctx.partial_update_stats.num_rows_deleted, 0); +} + +// ERROR new-key policy rejects a brand-new (non-delete) flexible row. +TEST_F(FlexiblePartialUpdateTest, FlexibleNewKeyErrorPolicyRejected) { + auto schema = create_flexible_mow_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3675, 2, {{7, 70}}, &tablet); // key 99 is absent + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema, PartialUpdateNewRowPolicyPB::ERROR); + RowsetId new_rsid; + new_rsid.init(3676); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(2).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 99, v = 55; + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + + auto st = run_flexible_fill(ctx, &block); + EXPECT_FALSE(st.ok()); + EXPECT_NE(st.to_string().find("Can't append new rows in partial update"), std::string::npos) + << st; +} + +// APPEND with a new key whose insert SKIPS a NOT NULL no-default column: the +// flexible-only handle_new_key arm that reads the skip bitmap rejects the row. +TEST_F(FlexiblePartialUpdateTest, FlexibleNewKeyAppendRequiredColumnMissing) { + auto schema = create_flexible_required_value_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset_block( + schema, 3678, 2, + [&](Block& b) { + auto guard = b.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 7, v = 70; + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + cols[3]->insert_default(); + }, + &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3679); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 99, v_dummy = 0; + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v_dummy), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(v_uid); // skips the NOT NULL no-default v on a brand-new key + assert_cast(cols[3].get())->insert_value(std::move(skip)); + } + + auto st = run_flexible_fill(ctx, &block); + EXPECT_FALSE(st.ok()); + EXPECT_NE(st.to_string().find("should have default value or be nullable"), std::string::npos) + << st; +} + +// Flexible PU insert-after-delete on a brand-new key: a delete row followed by an +// insert row for the same key merges into the single insert. +TEST_F(FlexiblePartialUpdateTest, FlexibleInsertAfterDeleteNewKey) { + auto schema = create_flexible_mow_schema(); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3331, 2, {{7, 70}}, &tablet); // key 1 is new + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3332); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + const auto ds_uid = static_cast(schema->column(2).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int32_t v0 = 0; + int32_t v1 = 77; + int8_t ds_del = 1; + int8_t ds_ins = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v0), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds_del), sizeof(int8_t)); + BitmapValue skip_del; + skip_del.add(v_uid); + assert_cast(cols[3].get())->insert_value(std::move(skip_del)); + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds_ins), sizeof(int8_t)); + BitmapValue skip_ins; + skip_ins.add(ds_uid); + assert_cast(cols[3].get())->insert_value(std::move(skip_ins)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); // the delete row was merged away + EXPECT_EQ(read_int(block, 0, 0), 1); + EXPECT_EQ(read_int(block, 1, 0), 77); // the insert's provided v survives + EXPECT_EQ(read_tinyint(block, 2, 0), 0); // not a delete +} + +// Insert-after-delete on an EXISTING key must not resurrect history: with two +// value columns and an insert that provides only v1 and SKIPS v2, the surviving +// insert is treated as a brand-new row -- v2 takes the DEFAULT (0), NOT the +// history v2 (400). This is the core use_defaults_for_in_load_deleted +// semantics; a single-value-column schema could not prove it. +TEST_F(FlexiblePartialUpdateTest, FlexibleInsertAfterDeleteExistingKeyNoResurrect) { + auto schema = create_flexible_seq2_schema(); // k v1 v2 seq ds skip + TabletSharedPtr tablet; + // history k=2: v1=300, v2=400, seq=2 (distinct from defaults) + auto rowset = + write_seq2_history(schema, 3401, /*k=*/2, /*v1=*/300, /*v2=*/400, /*seq=*/2, &tablet); + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3402); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + // r0: DELETE k=2 (seq=7), provides ds + seq, skips v1/v2 + append_seq2_row(schema, cols, + Seq2Row {.k = 2, + .v1 = 0, + .v2 = 0, + .seq = 7, + .delete_sign = 1, + .give_v1 = false, + .give_v2 = false, + .give_seq = true, + .give_ds = true}); + // r1: INSERT k=2 only v1=330 (seq=8), skips v2 and ds + append_seq2_row(schema, cols, + Seq2Row {.k = 2, + .v1 = 330, + .v2 = 999, + .seq = 8, + .delete_sign = 0, + .give_v1 = true, + .give_v2 = false, + .give_seq = true, + .give_ds = false}); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); // tombstone merged away + auto out = index_seq2(block); + ASSERT_TRUE(out.contains(2)); + EXPECT_EQ(out[2].v1, 330); // provided + EXPECT_EQ(out[2].v2, 0); // DEFAULT, not the history 400 (no resurrection) + EXPECT_EQ(out[2].ds, 0); + // history k=2 old row (seg0 row0) marked; the surviving insert is not self-marked + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_EQ(mow->delete_bitmap->cardinality(), 1U); +} + +// Seq dedup with the key present in history (k=1, baseline 5), so the FOUND +// arm of aggregate_rows actually runs. Two same-key rows, both seq >= 5, merge +// into the higher-seq winner. +TEST_F(FlexiblePartialUpdateTest, FlexibleSeqColumnDedupHigherWinsFoundArm) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); // k v seq ds skip + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3301, 2, {{1, 11, 5, 0}}, &tablet); // history k=1 seq=5 + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3302); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t ks[] = {1, 1}; + int32_t vs[] = {50, 80}; + int32_t seqs[] = {6, 8}; // both >= history baseline 5 + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&ks[i]), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; // only delete sign not provided + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); // merged + EXPECT_EQ(read_int(block, 0, 0), 1); + EXPECT_EQ(read_int(block, 1, 0), 80); // higher-seq row wins + EXPECT_EQ(read_int(block, 2, 0), 8); + // FOUND in history: the old row (seg0 row0) is marked by the probe + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); +} + +// Flexible seq-loser self-mark: the one place this move changed position +// semantics (legacy marked segment_start_pos + delta_pos, the stage marks the +// block row index and relies on one-block-one-fresh-segment). A two-row group +// for k=1 shrinks to one row, then a lone stale row for k=2 (below its +// baseline; single-row groups skip aggregation) loses at the probe: the +// self-mark must land at the POST-shrink position of a NON-zero segment id. +TEST_F(FlexiblePartialUpdateTest, FlexibleSeqLoserSelfMarkAfterShrink) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + // k=1 baseline seq=5 (history row 0), k=2 baseline seq=10 (history row 1) + auto rowset = write_rowset(schema, 3651, 2, {{1, 11, 5, 0}, {2, 22, 10, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3652); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + ctx.segment_id = 3; // this block lands in segment 3 of the new rowset + + const auto v_uid = static_cast(schema->column(1).unique_id()); + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + // r0/r1: k=1 seq 6 then 8, both >= baseline 5 -> merge into one row + // r2: k=2 seq 3 < baseline 10, gives seq only -> lone seq loser + int32_t ks[] = {1, 1, 2}; + int32_t vs[] = {60, 80, 0}; + int32_t seqs[] = {6, 8, 3}; + int8_t ds0 = 0; + for (int i = 0; i < 3; ++i) { + cols[0]->insert_data(reinterpret_cast(&ks[i]), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + if (i == 2) { + skip.add(v_uid); // the loser omits v: it must stay default, not history + } + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 2); // k=1 merged away one row, k=2 kept + EXPECT_EQ(read_int(block, 0, 1), 2); + EXPECT_EQ(read_int(block, 1, 1), 0); // loser: default, NOT the history 22 + EXPECT_EQ(read_int(block, 2, 1), 3); + + // the losing row self-marks at exactly {new_rsid, segment 3, post-shrink pos 1} + EXPECT_TRUE(mow->delete_bitmap->contains({new_rsid, 3, DeleteBitmap::TEMP_VERSION_COMMON}, 1)); + EXPECT_FALSE(mow->delete_bitmap->contains({new_rsid, 0, DeleteBitmap::TEMP_VERSION_COMMON}, 1)); + EXPECT_FALSE(mow->delete_bitmap->contains({new_rsid, 3, DeleteBitmap::TEMP_VERSION_COMMON}, 2)); + // k=2's history row survives the loss; k=1's history row is a normal update mark + EXPECT_FALSE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 1)); + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_EQ(mow->delete_bitmap->cardinality(), 2U); + EXPECT_EQ(ctx.partial_update_stats.num_rows_updated, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_deleted, 1); + EXPECT_EQ(ctx.partial_update_stats.num_rows_new_added, 0); +} + +// =========================================================================== +// BlockAggregator::aggregate_rows branch coverage. All use +// create_flexible_mow_schema(true) with the key present in history so the FOUND +// arm (probe_previous_seq_value returning a baseline seq) is exercised. +// =========================================================================== + +// FOUND arm, stale first row skipped (seq < history baseline -> `continue`). +// History k=1 seq=10. Two rows: r0 seq=3 (< 10, stale, skipped as start), r1 +// seq=12 (>= 10, becomes start and the only survivor). +TEST_F(FlexiblePartialUpdateTest, AggFoundStaleFirstRowSkipped) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3501, 2, {{1, 11, 10, 0}}, &tablet); // baseline seq=10 + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3502); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int32_t vs[] = {30, 120}; + int32_t seqs[] = {3, 12}; + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 1, 0), 120); // stale seq=3 row dropped, seq=12 kept + EXPECT_EQ(read_int(block, 2, 0), 12); +} + +// FOUND arm, first-row-with-seq becomes start. History k=1 seq=5. First row +// already has seq=9 (>= 5) so it is the start; a following stale seq=4 row is +// out-of-order-discarded in the main loop. +TEST_F(FlexiblePartialUpdateTest, AggFoundFirstRowWithSeqBecomesStart) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3511, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3512); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int32_t vs[] = {90, 40}; + int32_t seqs[] = {9, 4}; + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 1, 0), 90); // seq=9 start kept, seq=4 discarded + EXPECT_EQ(read_int(block, 2, 0), 9); +} + +// FOUND arm, first-row-without-seq inherits the history baseline. History k=1 +// seq=5, v=11. r0 omits seq -> inherits baseline 5 (and is direct-fed by the +// main loop). r1 has seq=8 (>= 5) -> accepted and merged. +TEST_F(FlexiblePartialUpdateTest, AggFoundFirstRowWithoutSeqInheritsBaseline) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3521, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3522); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + const auto seq_uid = static_cast(schema->column(2).unique_id()); + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + // r0: v=70, omits seq (and ds) -> inherits history seq 5 + int32_t v0 = 70, seq0 = 0; + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v0), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq0), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip0; + skip0.add(seq_uid); + skip0.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip0)); + // r1: seq=8 (>= inherited baseline 5) -> accepted; gives seq only and + // SKIPS v, so the merge keeps r0's v=70 (proves the inherited start runs). + int32_t v1 = 80, seq1 = 8; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq1), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip1; + skip1.add(ds_uid); // gives seq, skips v + ds + skip1.add(v_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip1)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + // r1 provided seq=8 only; v cell skipped -> keeps r0's v=70 after merge + EXPECT_EQ(read_int(block, 1, 0), 70); + EXPECT_EQ(read_int(block, 2, 0), 8); // baseline inherited then advanced to 8 +} + +// NOT_FOUND arm, first-row-with-seq. Brand-new key (history has only k=7): +// the first row's own seq becomes the start; a later stale row is discarded. +TEST_F(FlexiblePartialUpdateTest, AggNotFoundFirstRowWithSeq) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3531, 2, {{7, 70, 1, 0}}, &tablet); // k=3 absent + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3532); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 3; + int32_t vs[] = {500, 490}; + int32_t seqs[] = {9, 6}; // r0 seq=9 -> start; r1 seq=6 < 9 -> discarded + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 0, 0), 3); + EXPECT_EQ(read_int(block, 1, 0), 500); // seq=9 start kept + EXPECT_EQ(read_int(block, 2, 0), 9); +} + +// NOT_FOUND arm, first-row-without-seq -> default seq. Brand-new key whose +// first row omits seq: cur_seq_val is the encoded DEFAULT seq (0 here). The +// second row gives seq=4 (>= default 0) so it is accepted and merges. +TEST_F(FlexiblePartialUpdateTest, AggNotFoundFirstRowWithoutSeqDefaultSeq) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3541, 2, {{7, 70, 1, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3542); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + const auto seq_uid = static_cast(schema->column(2).unique_id()); + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 4; + // r0: v=600, omits seq -> default seq baseline + int32_t v0 = 600, seq0 = 0; + int8_t ds0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v0), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq0), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip0; + skip0.add(seq_uid); + skip0.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip0)); + // r1: gives seq=4 only (>= default 0) -> accepted, v cell skipped keeps r0 + int32_t v1 = 0, seq1 = 4; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq1), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip1; + skip1.add(v_uid); + skip1.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip1)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 0, 0), 4); + EXPECT_EQ(read_int(block, 1, 0), 600); // r0's v kept (r1 skipped v) + EXPECT_EQ(read_int(block, 2, 0), 4); // advanced to r1's seq +} + +// Main loop: a no-seq row is direct-fed, a seq>=cur row is accepted, and a +// seq=5 +// accepted, v=70), r2 seq=6 (< 7, out-of-order discarded, v=60). +TEST_F(FlexiblePartialUpdateTest, AggMainLoopThreeArms) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3551, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3552); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto seq_uid = static_cast(schema->column(2).unique_id()); + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int8_t ds0 = 0; + // r0: v=10, no seq (direct-fed, inherits baseline 5) + int32_t v0 = 10, seq0 = 0; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v0), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq0), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue s0; + s0.add(seq_uid); + s0.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(s0)); + // r1: v=70, seq=7 (>= 5) accepted, advances cur to 7 + int32_t v1 = 70, seq1 = 7; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v1), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq1), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue s1; + s1.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(s1)); + // r2: v=60, seq=6 (< cur 7) out-of-order discarded + int32_t v2 = 60, seq2 = 6; + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&v2), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seq2), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue s2; + s2.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(s2)); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + // r0 (v=10) then r1 merges over it (gives v=70, seq=7); r2 discarded + EXPECT_EQ(read_int(block, 1, 0), 70); + EXPECT_EQ(read_int(block, 2, 0), 7); +} + +// aggregate_rows all-stale boundary: every row of a key is below the history +// baseline, so the FOUND start scan reaches `pos == end` and the main loop +// never appends -> the key is absent from the result and its history row keeps +// no delete-bitmap mark. A second key with a valid row survives alongside, so +// the block itself stays non-empty. +TEST_F(FlexiblePartialUpdateTest, AggAllRowsStaleKeyVanishes) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + // k=1 seq=20 (high baseline), k=9 seq=1 + auto rowset = write_rowset(schema, 3561, 2, {{1, 11, 20, 0}, {9, 99, 1, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3562); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + // two same-key rows for k=1, both < baseline 20 -> the whole key is + // dropped; one valid row for k=9 (seq 5 >= baseline 1) survives + int32_t ks[] = {1, 1, 9}; + int32_t vs[] = {30, 40, 90}; + int32_t seqs[] = {3, 4, 5}; + int8_t ds0 = 0; + for (int i = 0; i < 3; ++i) { + cols[0]->insert_data(reinterpret_cast(&ks[i]), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + // the whole stale key disappears; only k=9 remains + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 0, 0), 9); + EXPECT_EQ(read_int(block, 1, 0), 90); + EXPECT_EQ(read_int(block, 2, 0), 5); + // history k=1 old row (seg0 row0) must NOT be marked (never reached the + // probe); k=9's old row (seg0 row1) is marked by its surviving update + EXPECT_FALSE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 1)); + EXPECT_EQ(mow->delete_bitmap->cardinality(), 1U); +} + +// AggregateState machine: a delete inside a seq group clears the accumulated rows +// (remove_last_n_rows), leaving a pure tombstone. History k=1 seq=5. r0/r1 are +// upserts that accumulate, r2 is a delete (seq=9) that clears them -> only the +// tombstone survives, marking the history row. +TEST_F(FlexiblePartialUpdateTest, AggDeleteInsideSeqGroupClearsState) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3571, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3572); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + // r0 v=60 seq=6, r1 v=70 seq=7 (accumulate), r2 delete seq=9 (clears state) + int32_t vs[] = {60, 70, 0}; + int32_t seqs[] = {6, 7, 9}; + int8_t dss[] = {0, 0, 1}; + for (int i = 0; i < 3; ++i) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&dss[i]), sizeof(int8_t)); + BitmapValue skip; // r0/r1 give v+seq; r2 gives seq+ds, omits v + if (i == 2) { + skip.add(static_cast(schema->column(1).unique_id())); // delete omits v + } else { + skip.add(ds_uid); + } + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + // accumulated upserts cleared by the delete -> one tombstone row survives + ASSERT_EQ(block.rows(), 1); + EXPECT_EQ(read_int(block, 0, 0), 1); + EXPECT_EQ(read_tinyint(block, 3, 0), 1); // delete sign kept (pure tombstone) + // on a seq table a delete does NOT take defaults: the tombstone's skipped v + // is read back from history (11), pinning the delete_sign_skip=false branch + EXPECT_EQ(read_int(block, 1, 0), 11); + // delete marks the history old row (FOUND, delete_sign_skip false on seq table) + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_EQ(mow->delete_bitmap->cardinality(), 1U); +} + +// Insert-after-delete WITH seq inheritance: the insert row OMITS seq, so +// aggregate_for_insert_after_delete reads the old row's seq and the insert +// inherits it via fill_sequence_column. History k=2 seq=2. Delete (seq=7) then +// insert v1=330 with NO seq -> resulting seq must equal the history seq (2). +TEST_F(FlexiblePartialUpdateTest, AggInsertAfterDeleteSeqInheritance) { + auto schema = create_flexible_seq2_schema(); // k v1 v2 seq ds skip + TabletSharedPtr tablet; + auto rowset = + write_seq2_history(schema, 3601, /*k=*/2, /*v1=*/300, /*v2=*/400, /*seq=*/2, &tablet); + auto mow = make_mow_context(100, {rowset}); + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3602); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + // r0: DELETE k=2 with seq=7 (gives seq + ds; skips v1/v2). The tombstone's + // own seq must be >= history baseline 2 so step1 keeps the [D, I] pair. + append_seq2_row(schema, cols, + Seq2Row {.k = 2, + .v1 = 0, + .v2 = 0, + .seq = 7, + .delete_sign = 1, + .give_v1 = false, + .give_v2 = false, + .give_seq = true, + .give_ds = true}); + // r1: INSERT k=2 v1=330, NO seq, NO ds -> step2 must read old seq (2) and + // fill_sequence_column makes the insert inherit it. + append_seq2_row(schema, cols, + Seq2Row {.k = 2, + .v1 = 330, + .v2 = 999, + .seq = 0, + .delete_sign = 0, + .give_v1 = true, + .give_v2 = false, + .give_seq = false, + .give_ds = false}); + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.rows(), 1); + auto out = index_seq2(block); + ASSERT_TRUE(out.contains(2)); + EXPECT_EQ(out[2].v1, 330); // provided + EXPECT_EQ(out[2].v2, 0); // default, not resurrected history 400 + EXPECT_EQ(out[2].seq, 2); // inherited the history seq via fill_sequence_column + EXPECT_EQ(out[2].ds, 0); + // history old row marked + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); +} + +// FlexibleReadPlan::fill_non_primary_key_columns_for_row_store (use_row_store +// branch, previously uncovered by the flexible tests): with the hidden full +// row-store column on, a skipped cell of an existing key is restored through +// fetch_value_through_row_column (== history 11, not the default 0), while a +// provided cell is kept. Mirrors FixedRowStoreReadPath on the flexible path. +TEST_F(FlexiblePartialUpdateTest, FlexibleRowStoreReadPath) { + auto schema = create_flexible_row_store_schema(); + ASSERT_TRUE(schema->has_row_store_for_all_columns()); + TabletSharedPtr tablet; + auto rowset = write_rowset(schema, 3611, 2, {{1, 11}, {2, 22}}, &tablet); + auto mow = make_mow_context(100, {rowset}); + + auto pui = make_flexible_pui(schema); + RowsetId new_rsid; + new_rsid.init(3612); + RowsetWriterContext rwc; + fill_rowset_ctx(&rwc, schema, tablet, pui, new_rsid); + TransformExecContext ctx = make_exec_ctx(schema, tablet, mow, pui, &rwc, new_rsid); + + const auto v_uid = static_cast(schema->column(1).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t ks[] = {1, 2}; + int32_t vs[] = {999, 222}; // r0 v is a placeholder (skipped); r1 provides 222 + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&ks[i]), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + if (i == 0) { + skip.add(v_uid); // r0 skips v -> must come back from the row store + } + assert_cast(cols[3].get())->insert_value(std::move(skip)); + cols[4]->insert_default(); // row-store col placeholder (regenerated downstream) + } + } + + ASSERT_TRUE(run_flexible_fill(ctx, &block).ok()); + ASSERT_EQ(block.columns(), schema->num_columns()); + ASSERT_EQ(block.rows(), 2); + std::map kv; + for (size_t r = 0; r < block.rows(); ++r) { + kv[read_int(block, 0, r)] = read_int(block, 1, r); + } + EXPECT_EQ(kv[1], 11); // skipped cell restored through the row column (!= default 0) + EXPECT_EQ(kv[2], 222); // provided cell kept + EXPECT_FALSE(read_is_null(block, 1, 0)); + // both existing keys mark their old rows (rowid 0 and 1) + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 0)); + EXPECT_TRUE(mow->delete_bitmap->contains( + {rowset->rowset_id(), 0, DeleteBitmap::TEMP_VERSION_COMMON}, 1)); +} + +// A partial shrink (two same-key rows merge into one) flows through the same +// seam: one row lands in the segment, the aggregated-away row is accounted for +// and the flush succeeds. +TEST_F(FlexiblePartialUpdateTest, FlushCountsAggregatedAwayRows) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto history = write_rowset(schema, 3725, 2, {{7, 70, 1, 0}}, &tablet); // k=1 is new + auto mow = make_mow_context(100, {history}); + auto pui = make_flexible_pui(schema); + + const auto ds_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& cols = guard.mutable_columns(); + int32_t k = 1; + int32_t vs[] = {50, 80}; + int32_t seqs[] = {6, 8}; // same key, both valid -> merge to the seq=8 row + int8_t ds0 = 0; + for (int i = 0; i < 2; ++i) { + cols[0]->insert_data(reinterpret_cast(&k), sizeof(int32_t)); + cols[1]->insert_data(reinterpret_cast(&vs[i]), sizeof(int32_t)); + cols[2]->insert_data(reinterpret_cast(&seqs[i]), sizeof(int32_t)); + cols[3]->insert_data(reinterpret_cast(&ds0), sizeof(int8_t)); + BitmapValue skip; + skip.add(ds_uid); + assert_cast(cols[4].get())->insert_value(std::move(skip)); + } + } + + const bool saved_correctness_check = config::enable_merge_on_write_correctness_check; + config::enable_merge_on_write_correctness_check = false; + RowsetSharedPtr output; + int64_t writer_num_rows = 0; + const auto flush_status = flush_partial_rowset(schema, 3726, 3, tablet, mow, pui, &block, + &output, nullptr, &writer_num_rows); + config::enable_merge_on_write_correctness_check = saved_correctness_check; + ASSERT_TRUE(flush_status.ok()) << flush_status; + ASSERT_NE(output, nullptr); + EXPECT_EQ(output->rowset_meta()->num_rows(), 1); + // the load-close check compares this counter against the received rows: it + // must count the 2 input rows, not the 1 that survived aggregation + EXPECT_EQ(writer_num_rows, 2); + + Block persisted; + ASSERT_TRUE(read_rowset(output, schema, &persisted).ok()); + ASSERT_EQ(persisted.rows(), 1); + EXPECT_EQ(read_int(persisted, 0, 0), 1); + EXPECT_EQ(read_int(persisted, 1, 0), 80); // higher-seq row survived the merge + EXPECT_EQ(read_int(persisted, 2, 0), 8); +} + +// End-to-end writer coverage for flexible updates. The segment's physical ordering may differ +// from the legacy writer, but all filled values and sequence semantics must survive a read-back. +TEST_F(FlexiblePartialUpdateTest, VerticalWriterPersistsFilledRows) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto history = write_rowset(schema, 3711, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {history}); + auto pui = make_flexible_pui(schema); + + const auto value_uid = static_cast(schema->column(1).unique_id()); + const auto delete_sign_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& columns = guard.mutable_columns(); + const std::vector> rows {{1, 999, 10, true}, + {99, 909, 7, false}}; + for (const auto& [key, value, sequence, skip_value] : rows) { + const int8_t delete_sign = 0; + columns[0]->insert_data(reinterpret_cast(&key), sizeof(key)); + columns[1]->insert_data(reinterpret_cast(&value), sizeof(value)); + columns[2]->insert_data(reinterpret_cast(&sequence), sizeof(sequence)); + columns[3]->insert_data(reinterpret_cast(&delete_sign), + sizeof(delete_sign)); + BitmapValue skip; + if (skip_value) { + skip.add(value_uid); + } + skip.add(delete_sign_uid); + assert_cast(columns[4].get())->insert_value(std::move(skip)); + } + } + + const bool saved_vertical_writer = config::enable_vertical_segment_writer; + const bool saved_correctness_check = config::enable_merge_on_write_correctness_check; + config::enable_vertical_segment_writer = true; + config::enable_merge_on_write_correctness_check = false; + RowsetSharedPtr output; + const auto flush_status = + flush_partial_rowset(schema, 3712, 3, tablet, mow, pui, &block, &output); + config::enable_vertical_segment_writer = saved_vertical_writer; + config::enable_merge_on_write_correctness_check = saved_correctness_check; + ASSERT_TRUE(flush_status.ok()) << flush_status; + ASSERT_NE(output, nullptr); + ASSERT_EQ(output->rowset_meta()->num_rows(), 2); + + Block persisted; + ASSERT_TRUE(read_rowset(output, schema, &persisted).ok()); + ASSERT_EQ(persisted.rows(), 2); + ASSERT_EQ(persisted.columns(), schema->num_columns()); + EXPECT_EQ(read_int(persisted, 0, 0), 1); + EXPECT_EQ(read_int(persisted, 1, 0), 11); // skipped value restored from history + EXPECT_EQ(read_int(persisted, 2, 0), 10); + EXPECT_EQ(read_tinyint(persisted, 3, 0), 0); + EXPECT_EQ(read_int(persisted, 0, 1), 99); + EXPECT_EQ(read_int(persisted, 1, 1), 909); // provided value kept + EXPECT_EQ(read_int(persisted, 2, 1), 7); + EXPECT_EQ(read_tinyint(persisted, 3, 1), 0); + + const auto& persisted_skip_bitmaps = + assert_cast(*persisted.get_by_position(4).column).get_data(); + ASSERT_EQ(persisted_skip_bitmaps.size(), 2); + EXPECT_TRUE(persisted_skip_bitmaps[0].contains(value_uid)); + EXPECT_TRUE(persisted_skip_bitmaps[0].contains(delete_sign_uid)); + EXPECT_FALSE(persisted_skip_bitmaps[1].contains(value_uid)); + EXPECT_TRUE(persisted_skip_bitmaps[1].contains(delete_sign_uid)); + + auto beta_rowset = std::dynamic_pointer_cast(output); + ASSERT_NE(beta_rowset, nullptr); + std::vector segments; + ASSERT_TRUE(beta_rowset->load_segments(&segments).ok()); + ASSERT_EQ(segments.size(), 1); + RowKeyEncoder encoder(*schema, true); + for (const auto& [row_id, key, sequence] : + std::vector> {{0, 1, 10}, {1, 99, 7}}) { + std::string persisted_key; + ASSERT_TRUE(segments[0]->read_key_by_rowid(row_id, &persisted_key).ok()); + EXPECT_EQ(persisted_key, encode_key_with_seq(schema, encoder, key, sequence)); + } +} + +// The chain fills flexible blocks to full width before any writer runs, so the +// horizontal SegmentWriter accepts them now (the NotSupported rejection is +// gone). Same scenario and read-back assertions as the vertical test above. +TEST_F(FlexiblePartialUpdateTest, HorizontalWriterPersistsFilledRows) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto history = write_rowset(schema, 3771, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {history}); + auto pui = make_flexible_pui(schema); + + const auto value_uid = static_cast(schema->column(1).unique_id()); + const auto delete_sign_uid = static_cast(schema->column(3).unique_id()); + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& columns = guard.mutable_columns(); + const std::vector> rows {{1, 999, 10, true}, + {99, 909, 7, false}}; + for (const auto& [key, value, sequence, skip_value] : rows) { + const int8_t delete_sign = 0; + columns[0]->insert_data(reinterpret_cast(&key), sizeof(key)); + columns[1]->insert_data(reinterpret_cast(&value), sizeof(value)); + columns[2]->insert_data(reinterpret_cast(&sequence), sizeof(sequence)); + columns[3]->insert_data(reinterpret_cast(&delete_sign), + sizeof(delete_sign)); + BitmapValue skip; + if (skip_value) { + skip.add(value_uid); + } + skip.add(delete_sign_uid); + assert_cast(columns[4].get())->insert_value(std::move(skip)); + } + } + + const bool saved_vertical_writer = config::enable_vertical_segment_writer; + const bool saved_correctness_check = config::enable_merge_on_write_correctness_check; + config::enable_vertical_segment_writer = false; + config::enable_merge_on_write_correctness_check = false; + RowsetSharedPtr output; + const auto flush_status = + flush_partial_rowset(schema, 3772, 3, tablet, mow, pui, &block, &output); + config::enable_vertical_segment_writer = saved_vertical_writer; + config::enable_merge_on_write_correctness_check = saved_correctness_check; + ASSERT_TRUE(flush_status.ok()) << flush_status; + ASSERT_NE(output, nullptr); + ASSERT_EQ(output->rowset_meta()->num_rows(), 2); + + Block persisted; + ASSERT_TRUE(read_rowset(output, schema, &persisted).ok()); + ASSERT_EQ(persisted.rows(), 2); + ASSERT_EQ(persisted.columns(), schema->num_columns()); + EXPECT_EQ(read_int(persisted, 0, 0), 1); + EXPECT_EQ(read_int(persisted, 1, 0), 11); // skipped value restored from history + EXPECT_EQ(read_int(persisted, 2, 0), 10); + EXPECT_EQ(read_tinyint(persisted, 3, 0), 0); + EXPECT_EQ(read_int(persisted, 0, 1), 99); + EXPECT_EQ(read_int(persisted, 1, 1), 909); // provided value kept + EXPECT_EQ(read_int(persisted, 2, 1), 7); + EXPECT_EQ(read_tinyint(persisted, 3, 1), 0); + + const auto& persisted_skip_bitmaps = + assert_cast(*persisted.get_by_position(4).column).get_data(); + ASSERT_EQ(persisted_skip_bitmaps.size(), 2); + EXPECT_TRUE(persisted_skip_bitmaps[0].contains(value_uid)); + EXPECT_TRUE(persisted_skip_bitmaps[0].contains(delete_sign_uid)); + EXPECT_FALSE(persisted_skip_bitmaps[1].contains(value_uid)); + EXPECT_TRUE(persisted_skip_bitmaps[1].contains(delete_sign_uid)); + + auto beta_rowset = std::dynamic_pointer_cast(output); + ASSERT_NE(beta_rowset, nullptr); + std::vector segments; + ASSERT_TRUE(beta_rowset->load_segments(&segments).ok()); + ASSERT_EQ(segments.size(), 1); + RowKeyEncoder encoder(*schema, true); + for (const auto& [row_id, key, sequence] : + std::vector> {{0, 1, 10}, {1, 99, 7}}) { + std::string persisted_key; + ASSERT_TRUE(segments[0]->read_key_by_rowid(row_id, &persisted_key).ok()); + EXPECT_EQ(persisted_key, encode_key_with_seq(schema, encoder, key, sequence)); + } +} + +// Multiple memtable flushes use independent segment writers. Read all segments back together and +// then use that rowset as history for another partial update. +TEST_F(FlexiblePartialUpdateTest, VerticalWriterPersistsRowsAcrossSegments) { + auto schema = create_flexible_mow_schema(/*has_seq=*/true); + TabletSharedPtr tablet; + auto history = write_rowset(schema, 3741, 2, {{1, 11, 5, 0}}, &tablet); + auto mow = make_mow_context(100, {history}); + auto pui = make_flexible_pui(schema); + + const auto value_uid = static_cast(schema->column(1).unique_id()); + const auto delete_sign_uid = static_cast(schema->column(3).unique_id()); + auto make_block = [&](const std::vector>& rows, + bool skip_value = false) { + Block block = schema->create_block(); + { + auto guard = block.mutate_columns_scoped(); + auto& columns = guard.mutable_columns(); + for (const auto& [key, value, sequence] : rows) { + const int8_t delete_sign = 0; + columns[0]->insert_data(reinterpret_cast(&key), sizeof(key)); + columns[1]->insert_data(reinterpret_cast(&value), sizeof(value)); + columns[2]->insert_data(reinterpret_cast(&sequence), sizeof(sequence)); + columns[3]->insert_data(reinterpret_cast(&delete_sign), + sizeof(delete_sign)); + BitmapValue skip; + if (skip_value) { + skip.add(value_uid); + } + skip.add(delete_sign_uid); + assert_cast(columns[4].get())->insert_value(std::move(skip)); + } + } + return block; + }; + Block first_segment = make_block({{1, 999, 10}}, true); + Block second_segment = make_block({{2, 22, 7}}); + Block third_segment = make_block({{3, 33, 8}}); + + const bool saved_vertical_writer = config::enable_vertical_segment_writer; + const bool saved_correctness_check = config::enable_merge_on_write_correctness_check; + config::enable_vertical_segment_writer = true; + config::enable_merge_on_write_correctness_check = false; + RowsetSharedPtr output; + int64_t writer_num_rows = 0; + const auto flush_status = flush_partial_rowset_segments( + schema, 3742, 3, tablet, mow, pui, {&first_segment, &second_segment, &third_segment}, + &output, nullptr, &writer_num_rows); + config::enable_vertical_segment_writer = saved_vertical_writer; + config::enable_merge_on_write_correctness_check = saved_correctness_check; + ASSERT_TRUE(flush_status.ok()) << flush_status; + ASSERT_NE(output, nullptr); + EXPECT_EQ(output->rowset_meta()->num_rows(), 3); + EXPECT_EQ(writer_num_rows, 3); + + auto beta_rowset = std::dynamic_pointer_cast(output); + ASSERT_NE(beta_rowset, nullptr); + std::vector segments; + ASSERT_TRUE(beta_rowset->load_segments(&segments).ok()); + ASSERT_EQ(segments.size(), 3); + size_t persisted_segment_rows = 0; + for (const auto& segment : segments) { + persisted_segment_rows += segment->num_rows(); + } + EXPECT_EQ(persisted_segment_rows, 3); + + Block persisted; + const auto read_status = read_rowset(output, schema, &persisted); + ASSERT_TRUE(read_status.ok()) << read_status; + ASSERT_EQ(persisted.rows(), 3); + ASSERT_EQ(persisted.columns(), schema->num_columns()); + const auto& persisted_skip_bitmaps = + assert_cast(*persisted.get_by_position(4).column).get_data(); + const std::map expected_sequence {{1, 10}, {2, 7}, {3, 8}}; + for (size_t row = 0; row < persisted.rows(); ++row) { + const int32_t key = read_int(persisted, 0, row); + EXPECT_EQ(key, static_cast(row + 1)); + EXPECT_EQ(read_int(persisted, 1, row), key * 11); + ASSERT_TRUE(expected_sequence.contains(key)); + EXPECT_EQ(read_int(persisted, 2, row), expected_sequence.at(key)); + EXPECT_EQ(read_tinyint(persisted, 3, row), 0); + EXPECT_EQ(persisted_skip_bitmaps[row].contains(value_uid), row == 0); + EXPECT_TRUE(persisted_skip_bitmaps[row].contains(delete_sign_uid)); + } + + // Probe the rowset again as partial-update history. SegmentLoader opens every segment PK index + // before finding key 2 and restoring its skipped value. + auto next_mow = make_mow_context(101, {output}); + auto next_pui = make_flexible_pui(schema); + RowsetId next_rsid; + next_rsid.init(3743); + RowsetWriterContext next_rwc; + fill_rowset_ctx(&next_rwc, schema, tablet, next_pui, next_rsid); + TransformExecContext next_ctx = + make_exec_ctx(schema, tablet, next_mow, next_pui, &next_rwc, next_rsid); + Block next_update = make_block({{2, 999, 9}}, true); + ASSERT_TRUE(run_flexible_fill(next_ctx, &next_update).ok()); + ASSERT_EQ(next_update.rows(), 1); + EXPECT_EQ(read_int(next_update, 0, 0), 2); + EXPECT_EQ(read_int(next_update, 1, 0), 22); + EXPECT_EQ(read_int(next_update, 2, 0), 9); +} + +} // namespace doris diff --git a/be/test/storage/transform/validate_stage_test.cpp b/be/test/storage/transform/validate_stage_test.cpp index 903f1d3e73e390..36d14c4f9c2aa3 100644 --- a/be/test/storage/transform/validate_stage_test.cpp +++ b/be/test/storage/transform/validate_stage_test.cpp @@ -137,10 +137,10 @@ TEST_F(ValidateStageTest, CompositionFixedPartialUpdate) { (V {"Validate", "FixedPartialUpdateFill", "VariantParse", "RowStoreFill"})); } -// Flexible partial update only gets validated by the chain for now: the vertical -// writer still owns its fill, parse and row-store work. The flexible fill stage -// takes this slot when it moves into the chain. -TEST_F(ValidateStageTest, CompositionFlexiblePartialUpdateValidateOnly) { +// TYPE_DIRECT + flexible PU -> the flexible fill stage sits after Validate; the +// legacy flexible path rebuilt RowStore before parsing the filled variants, the +// reverse of the fixed order. +TEST_F(ValidateStageTest, CompositionFlexiblePartialUpdate) { using V = std::vector; auto fschema = create_flexible_mow_schema(); auto flexible = std::make_shared(); @@ -150,7 +150,8 @@ TEST_F(ValidateStageTest, CompositionFlexiblePartialUpdateValidateOnly) { .ok()); RowsetWriterContext fc = direct_rwc(fschema); fc.partial_update_info = flexible; - EXPECT_EQ(build_transform_chain(fc).stage_names(), (V {"Validate"})); + EXPECT_EQ(build_transform_chain(fc).stage_names(), + (V {"Validate", "FlexiblePartialUpdateFill", "RowStoreFill", "VariantParse"})); } // Binlog sub-writers keep deriving inside RowBinlogSegmentWriter for now, so @@ -169,10 +170,9 @@ TEST_F(ValidateStageTest, CompositionBinlogEmpty) { } // ============================================================================= -// ValidateStage branches (V1-V9, without V5's fill which is not in the chain -// yet). ValidateStage is the chain's first stage on every non-compaction, -// non-binlog path, so we build the real chain and drive it with a block that -// already fails / passes validate. +// ValidateStage branches (V1-V9). ValidateStage is the chain's first stage on +// every non-compaction, non-binlog path, so we build the real chain and drive +// it with a block that already fails / passes validate. // ============================================================================= // V1: non-PU direct, full width (columns == num_columns) -> accepted. From c05000d3075a1a73b89e1f4fc841045b09e4ec8f Mon Sep 17 00:00:00 2001 From: csun5285 Date: Fri, 14 Aug 2026 19:43:47 +0800 Subject: [PATCH 2/2] [test](storage) cover flexible partial update on the horizontal writer The horizontal SegmentWriter accepts flexible partial update now that the transform chain fills blocks before any writer runs, so make the coverage follow: - test_f_segment_writer.groovy pinned the deleted NotSupported rejection. It now requires the load to succeed and checks the updated rows and skip bitmaps; the expected values match test_flexible_partial_update.groovy, which runs the same load on the vertical path. It also reads the rows once more right before the load, so an unexpected change shows up there instead of in the post-update check. - The golden suite replays flexible partial update through both writers (kFlexiblePartialWriterModes). The vertical baselines stay the ones recorded from the legacy in-writer fill; the horizontal baselines are new, since the legacy horizontal writer rejected flexible and no legacy baseline can exist for it. - enable_vertical_segment_writer joins the fuzzy config set so pipeline runs exercise both writers. The only two suites that enable a VerticalSegmentWriter debug point either switch the config themselves or use a point that now lives in the fill stage, so neither depends on the default. Co-Authored-By: Claude Fable 5 --- be/src/common/config.cpp | 2 ++ .../rowset/segment_flusher_format_test.cpp | 11 +++++++---- .../flexible_partial_horizontal/segment_0.dat | Bin 0 -> 7415 bytes .../flexible_partial_horizontal/segment_1.dat | Bin 0 -> 7429 bytes .../segment_0.dat | Bin 0 -> 9211 bytes .../segment_1.dat | Bin 0 -> 9237 bytes .../flexible/test_f_segment_writer.out | 8 ++++++++ .../flexible/test_f_segment_writer.groovy | 10 ++++++++-- 8 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_0.dat create mode 100644 be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_1.dat create mode 100644 be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_0.dat create mode 100644 be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_1.dat diff --git a/be/src/common/config.cpp b/be/src/common/config.cpp index d27cf607a36b24..a57952ede8edb2 100644 --- a/be/src/common/config.cpp +++ b/be/src/common/config.cpp @@ -2360,6 +2360,8 @@ Status set_fuzzy_configs() { ((distribution(*generator) % 2) == 0) ? "true" : "false"; fuzzy_field_and_value["enable_packed_file"] = ((distribution(*generator) % 2) == 0) ? "true" : "false"; + fuzzy_field_and_value["enable_vertical_segment_writer"] = + ((distribution(*generator) % 2) == 0) ? "true" : "false"; fuzzy_field_and_value["max_segment_partial_column_cache_size"] = ((distribution(*generator) % 2) == 0) ? "5" : "10"; diff --git a/be/test/storage/rowset/segment_flusher_format_test.cpp b/be/test/storage/rowset/segment_flusher_format_test.cpp index bb53fe0031f4b4..b2688b5832811d 100644 --- a/be/test/storage/rowset/segment_flusher_format_test.cpp +++ b/be/test/storage/rowset/segment_flusher_format_test.cpp @@ -108,8 +108,8 @@ constexpr std::string_view kGoldenOutputDirEnv = "DORIS_SEGMENT_FLUSHER_GOLDEN_O // regenerating all golden files. constexpr int32_t kGoldenBeExecVersion = 10; constexpr int32_t kRowBinlogSystemColumnCount = 3; -constexpr size_t kExpectedGoldenCaseCount = 76; -constexpr size_t kExpectedGoldenSegmentCount = 154; +constexpr size_t kExpectedGoldenCaseCount = 78; +constexpr size_t kExpectedGoldenSegmentCount = 158; constexpr size_t kExternalIndexRows = 180; constexpr size_t kAnnDimensions = 4; constexpr std::array kGoldenProducerTests { @@ -3654,8 +3654,11 @@ TEST_F(SegmentFlusherFormatTest, RowStoreAndSegmentCreatorPathsKeepTheirSegmentB } TEST_F(SegmentFlusherTransformFormatTest, PartialUpdateAndRowBinlogPathsKeepTheirSegmentBytes) { - // Flexible partial update is a VerticalSegmentWriter-only generation path on the baseline. - constexpr std::array kFlexiblePartialWriterModes {true}; + // Both writers replay flexible partial update. The vertical baselines were + // recorded from the legacy in-writer fill; the horizontal ones pin the path + // the transform chain newly opened (the legacy horizontal writer rejected + // flexible, so no legacy baseline can exist for it). + constexpr std::array kFlexiblePartialWriterModes {false, true}; auto record = [](Result> result) { if (!result.has_value()) { return testing::AssertionFailure() << result.error(); diff --git a/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_0.dat b/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_0.dat new file mode 100644 index 0000000000000000000000000000000000000000..078a0b185b2aa80aef6c82251b9bfa7fcf0a33c8 GIT binary patch literal 7415 zcmeHLeQ;b=6~E_w?7n^Zx_S9}O%w7`(-7Kb^ER7JH<32!Hl-9x2`#ln6p|8b89}Qe z1+=grP+CMvD@9?jOr4>OBT}TI0|=Fh>`)o<2QW+-#Zmbf0h>~tqD-m3^Y$aJ$-Xv@ zGyWqdx$oU~KhF8xbIv{Yo>heq%_s(nh5{<!V?q}kTr&xx~6NSXXkk{ zTm3U{iMHR&Gh-+`lMAR(Lg+pn>(F$bt5HZ)kf<7D=`23xXVPaJSDDGVD9k{jfO;Ch z9e2I|4$oT3eYt{Ha;!lIF4Dt74KtXK8d6kn(=;qugT7TDMJEi2 zr!W*zFy>M}|5txCb2sx(K^x4A*x{JeUuteN0ny-X!opSe5yJQHj_FWNd7WYD9%1lmgQQex@5b zD*Bn}3KV9T+lYOKE5mRO=Eqp!NbEHDh7c6E51eWkywO!7)Ceo8gEce09as3peCO_( zf3TQ4a$+33LD!Cj6|i8Z9Az=g!c;zsr%#v2hs9%4F+^>AfD4|h-=VHrk7W|e-K5ynw`7HaxH0jv=I zG(803EmR@4;)9zs4Tg^A*|4v)zH)W7pG{7mO&y2|Vp)ngs3OK(#2%i{1V$6tJmJjy zEaPWZ%_kz7bVN+v16jw3LV5^HeOw{98`sFBb?!fziu-fvd&8#Fbkf z-Nf@|oj9-dU>K2ED0)}{!32Zohb~FZ^3VQYhx0bio^(=qqq7rEIH_v)+G8PwvPxRm&AD`&q|6^aY{k-GY$RIVA2u+Ho zNJ%3;;M~1dx&CG4hU1`*u5e!M*zxunG*|50iGC}(?aWOZ5G#)yIeKW1pkZ;#$a6>D z!|0cufBf=e(c>R48QHs{1pV18*6U*!_gI2t}6YRfJqa+_tUj$xaj5K>pDDf6d8k;mUU=exgsFTT0) z*B5^U(~jMGY4V0tA0Hj+>$}Cw7nC&J_@c6OPwijJ_8z?C`oCL`VpUJiJ#G4nP1DNP zY;2RS?Y&09+CEk|Ynyayq(aV-Cbq*4ZA9S!SmjPqw?U_E@l={9>w5QW;8(QpCEKjIa#b z6-7+7OO-=_az3D(Yj&Ge<4~N=cyFQy|5nvFCdZBU#=3glxq*86^HjkNWoU?P2t66n zup0`oV%&>w(&Uo=LQi*EQ#9F<3@zdC@emmi8L=CS=;?qa*8$2U(9=ccfLUKePf^d0 zVpe?-bwy!eP@vYHoB%Zx6zSGwXdP>9FBzsZ?Y1INAJaXkUjo#%=Ah{WL!Irqz1Wsc zFyQQnK-h2pr?9t^Q6fw1GN-gn_agMTvF{7X9%y(WVE37AX6dkZ!ek#-X+8<#??5=) ztFi_^M>k#$$$?WI8yhpUagIlSgPqlAbT4~aPu&m6`%3PGMkQH`wktly#M6Lyv@hI&X)?!t8>brlq~-n`J9=~Gl5 zUH;9^1TzA-)lxtSkAh91%>0{uEZY*efJ6QTEKQRYq}*MUp4Q_FU7PH^|kPJ&&_UT1z{aO~x zN`N`v>^0Ti-k5dz7eT$Jd|Va?@cI7?pbLLBs$`X2>7*t_u$`q|1akp!j#*>Uu%&U> z3Y2{$CmZ4FwNg3zl;LV5LnEV9VqI%g%j&}Ano{G@>f2>9343(_!ECddZ`W$f4ZdIR ztSwlF+>>6glNm~~dA?=T$eN;}pDdH}Av%BKR6E_6rFFS(@HP9ul%>zl(0rD@!KjtB zzI*og!v9$&`J(MlD}t|?N2e?}f6?Ax)XBP{SKS|%%lMQ9|EO-N`i^HP&Y|yY!rcK6~^Whu~+KlJWj;w1JdzqUXE<+qvDA<~M#``rPpSL5=2O)dUR#4BT@%+2OdvP>cxpbbt@+tjz#eXHq taq#E*VD9Dn1?Kt0ztF(aJu3P;Kw)v&%Et=JyRbrtp_b^?1Mc$JzX5L;+3)}W literal 0 HcmV?d00001 diff --git a/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_1.dat b/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_1.dat new file mode 100644 index 0000000000000000000000000000000000000000..1b4bddbc720f760d52088e09b40297a6cd4c4d93 GIT binary patch literal 7429 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 XT()sSVSdN(QS7SSkG1tAE>HeHsBK^b literal 0 HcmV?d00001 diff --git a/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_1.dat b/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_1.dat new file mode 100644 index 0000000000000000000000000000000000000000..094f539c6726ee612511326d71e1fff7b9e4f27d GIT binary patch literal 9237 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