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/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/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 00000000000000..078a0b185b2aa8 Binary files /dev/null and b/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_0.dat differ 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 00000000000000..1b4bddbc720f76 Binary files /dev/null and b/be/test/storage/test_data/segment_flusher_format/flexible_partial_horizontal/segment_1.dat differ diff --git a/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_0.dat b/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_0.dat new file mode 100644 index 00000000000000..19a787146ed5bf Binary files /dev/null and b/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_0.dat differ 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 00000000000000..094f539c6726ee Binary files /dev/null and b/be/test/storage/test_data/segment_flusher_format/flexible_partial_sequence_row_store_horizontal/segment_1.dat differ 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. diff --git a/regression-test/data/unique_with_mow_p0/flexible/test_f_segment_writer.out b/regression-test/data/unique_with_mow_p0/flexible/test_f_segment_writer.out index db009647da6988..25663fae6f55cb 100644 --- a/regression-test/data/unique_with_mow_p0/flexible/test_f_segment_writer.out +++ b/regression-test/data/unique_with_mow_p0/flexible/test_f_segment_writer.out @@ -15,3 +15,11 @@ 4 4 4 4 4 4 5 5 5 5 5 5 +-- !sql -- +0 0 0 0 0 0 +1 10 111 111 1 1 4,5,6 +2 2 20 2 222 25 1,3,6 +3 3 3 30 3 3 1,2,4,5,6 +4 43 4 99 20 4 2,5,6 +5 5 5 5 5 \N 1,2,3,4,6 +6 999 9876 777 1234 \N 2,4,5,6 diff --git a/regression-test/suites/unique_with_mow_p0/flexible/test_f_segment_writer.groovy b/regression-test/suites/unique_with_mow_p0/flexible/test_f_segment_writer.groovy index 283ee60785ccb5..1d0e80436af4b9 100644 --- a/regression-test/suites/unique_with_mow_p0/flexible/test_f_segment_writer.groovy +++ b/regression-test/suites/unique_with_mow_p0/flexible/test_f_segment_writer.groovy @@ -43,6 +43,11 @@ suite('test_f_segment_writer', 'docker') { sql """insert into ${tableName} select number, number, number, number, number, number from numbers("number" = "6"); """ qt_sql "select k,v1,v2,v3,v4,v5,BITMAP_TO_STRING(__DORIS_SKIP_BITMAP_COL__) from ${tableName} order by k;" + qt_sql "select k,v1,v2,v3,v4,v5,BITMAP_TO_STRING(__DORIS_SKIP_BITMAP_COL__) from ${tableName} order by k;" + + // Since the block transform chain fills flexible blocks to full width + // before any segment writer runs, the horizontal SegmentWriter accepts + // flexible partial update too: the load must succeed in this mode. streamLoad { table "${tableName}" set 'format', 'json' @@ -56,8 +61,9 @@ suite('test_f_segment_writer', 'docker') { throw exception } def json = parseJson(result) - assertEquals("fail", json.Status.toLowerCase()) - assertTrue(json.Message.contains("[NOT_IMPLEMENTED_ERROR]SegmentWriter doesn't support flexible partial update, please set enable_vertical_segment_writer=true in be.conf on all BEs to use VerticalSegmentWriter.")); + assertEquals("success", json.Status.toLowerCase()) + assertEquals(8, json.NumberTotalRows) + assertEquals(0, json.NumberFilteredRows) } } qt_sql "select k,v1,v2,v3,v4,v5,BITMAP_TO_STRING(__DORIS_SKIP_BITMAP_COL__) from ${tableName} order by k;"