This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 7f81c9847f6 branch-4.1: [fix](compaction) Fix sparse vertical 
compaction destination offset #66306 (#66429)
7f81c9847f6 is described below

commit 7f81c9847f631f21493e3f1c6a3370322f0a1129
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Aug 4 21:41:51 2026 +0800

    branch-4.1: [fix](compaction) Fix sparse vertical compaction destination 
offset #66306 (#66429)
    
    Cherry-picked from #66306
    
    Co-authored-by: Jamie <[email protected]>
---
 be/src/storage/iterator/vertical_block_reader.cpp  |   4 +-
 .../compaction/vertical_compaction_test.cpp        | 112 ++++++++++++++-------
 2 files changed, 79 insertions(+), 37 deletions(-)

diff --git a/be/src/storage/iterator/vertical_block_reader.cpp 
b/be/src/storage/iterator/vertical_block_reader.cpp
index d761387050d..dbc4d7f55ba 100644
--- a/be/src/storage/iterator/vertical_block_reader.cpp
+++ b/be/src/storage/iterator/vertical_block_reader.cpp
@@ -575,12 +575,12 @@ Status VerticalBlockReader::_unique_key_next_block(Block* 
block, bool* eof) {
             // Step 2: Prepare columns - pre-fill NULL for fixed-width, 
reserve for others
             std::vector<ColumnNullable*> nullable_dst_cols;
             std::vector<bool> supports_replace;
+            const size_t old_rows = target_columns.empty() ? 0 : 
target_columns.front()->size();
             _prepare_sparse_columns(target_columns, actual_rows, 
nullable_dst_cols,
                                     supports_replace);
 
             // Step 3: Process each batch
-            size_t dst_offset =
-                    target_columns.empty() ? 0 : target_columns[0]->size() - 
actual_rows;
+            size_t dst_offset = old_rows;
             for (const auto& batch : batches) {
                 Block* src_block = batch.block.get();
                 DCHECK(src_block != nullptr);
diff --git a/be/test/storage/compaction/vertical_compaction_test.cpp 
b/be/test/storage/compaction/vertical_compaction_test.cpp
index 2b12e34e41e..079bbb3f9af 100644
--- a/be/test/storage/compaction/vertical_compaction_test.cpp
+++ b/be/test/storage/compaction/vertical_compaction_test.cpp
@@ -38,9 +38,11 @@
 #include <vector>
 
 #include "common/status.h"
+#include "core/assert_cast.h"
 #include "core/block/block.h"
 #include "core/block/column_with_type_and_name.h"
 #include "core/column/column.h"
+#include "core/column/column_nullable.h"
 #include "core/data_type/data_type.h"
 #include "gtest/gtest_pred_impl.h"
 #include "io/cache/block_file_cache_factory.h"
@@ -1470,29 +1472,34 @@ TEST_F(VerticalCompactionTest, TestAggKeyVerticalMerge) 
{
     }
 }
 
-// Test to cover _sample_info->null_count logic in vertical_block_reader.cpp
-// This test creates a UNIQUE_KEYS table with nullable columns and sparse data
+// Test sparse compaction when a value group starts with a reserve-only column.
 TEST_F(VerticalCompactionTest, 
TestUniqueKeyVerticalMergeWithNullableSparseColumn) {
-    // Save original threshold and set to 1 to always enable sparse 
optimization
-    double original_threshold = 
config::sparse_column_compaction_threshold_percent;
+    const auto original_threshold = 
config::sparse_column_compaction_threshold_percent;
+    const auto original_columns_per_group = 
config::vertical_compaction_num_columns_per_group;
+    Defer restore_config {[original_threshold, original_columns_per_group]() {
+        config::sparse_column_compaction_threshold_percent = 
original_threshold;
+        config::vertical_compaction_num_columns_per_group = 
original_columns_per_group;
+    }};
     config::sparse_column_compaction_threshold_percent = 1.0;
+    config::vertical_compaction_num_columns_per_group = 2;
 
     auto num_input_rowset = 2;
     auto num_segments = 1;
     auto rows_per_segment = 100;
 
-    // Create schema with nullable column (c2 is nullable)
+    // The first value column only reserves capacity, while the nullable 
BIGINT column
+    // pre-fills actual_rows slots for in-place replacement.
     TabletSchemaSPtr tablet_schema = std::make_shared<TabletSchema>();
     TabletSchemaPB tablet_schema_pb;
     tablet_schema_pb.set_keys_type(UNIQUE_KEYS);
     tablet_schema_pb.set_num_short_key_columns(1);
     tablet_schema_pb.set_num_rows_per_row_block(1024);
     tablet_schema_pb.set_compress_kind(COMPRESS_NONE);
-    tablet_schema_pb.set_next_column_unique_id(4);
+    tablet_schema_pb.set_next_column_unique_id(5);
 
     ColumnPB* column_1 = tablet_schema_pb.add_column();
     column_1->set_unique_id(1);
-    column_1->set_name("c1");
+    column_1->set_name("k1");
     column_1->set_type("INT");
     column_1->set_is_key(true);
     column_1->set_length(4);
@@ -1500,31 +1507,40 @@ TEST_F(VerticalCompactionTest, 
TestUniqueKeyVerticalMergeWithNullableSparseColum
     column_1->set_is_nullable(false);
     column_1->set_is_bf_column(false);
 
-    // c2 is nullable - this is key for testing _sample_info->null_count
     ColumnPB* column_2 = tablet_schema_pb.add_column();
     column_2->set_unique_id(2);
-    column_2->set_name("c2");
-    column_2->set_type("INT");
-    column_2->set_length(4);
-    column_2->set_index_length(4);
+    column_2->set_name("v0");
+    column_2->set_type("BOOLEAN");
+    column_2->set_length(1);
+    column_2->set_index_length(1);
     column_2->set_is_key(false);
-    column_2->set_is_nullable(true); // nullable column
+    column_2->set_is_nullable(false);
     column_2->set_is_bf_column(false);
 
-    // DELETE_SIGN column required for unique keys
     ColumnPB* column_3 = tablet_schema_pb.add_column();
     column_3->set_unique_id(3);
-    column_3->set_name(DELETE_SIGN);
-    column_3->set_type("TINYINT");
-    column_3->set_length(1);
-    column_3->set_index_length(1);
-    column_3->set_is_nullable(false);
+    column_3->set_name("v1");
+    column_3->set_type("BIGINT");
+    column_3->set_length(8);
+    column_3->set_index_length(8);
     column_3->set_is_key(false);
+    column_3->set_is_nullable(true);
     column_3->set_is_bf_column(false);
 
+    // DELETE_SIGN column required for unique keys
+    ColumnPB* column_4 = tablet_schema_pb.add_column();
+    column_4->set_unique_id(4);
+    column_4->set_name(DELETE_SIGN);
+    column_4->set_type("TINYINT");
+    column_4->set_length(1);
+    column_4->set_index_length(1);
+    column_4->set_is_nullable(false);
+    column_4->set_is_key(false);
+    column_4->set_is_bf_column(false);
+
     tablet_schema->init_from_pb(tablet_schema_pb);
 
-    // Create input rowsets with NULL values in c2
+    // Create input rowsets with mixed NULL values in v1.
     std::vector<RowsetSharedPtr> input_rowsets;
     for (auto i = 0; i < num_input_rowset; i++) {
         RowsetWriterContext rowset_writer_context;
@@ -1546,26 +1562,26 @@ TEST_F(VerticalCompactionTest, 
TestUniqueKeyVerticalMergeWithNullableSparseColum
         ASSERT_TRUE(res.has_value()) << res.error();
         auto rowset_writer = std::move(res).value();
 
-        // Create block with nullable c2 column
         Block block = tablet_schema->create_block();
         auto columns = std::move(block).mutate_columns();
 
         for (int rid = 0; rid < rows_per_segment; ++rid) {
-            int32_t c1 = i * rows_per_segment + rid;
-            columns[0]->insert_data((const char*)&c1, sizeof(c1));
+            int32_t k1 = i * rows_per_segment + rid;
+            columns[0]->insert_data((const char*)&k1, sizeof(k1));
+
+            uint8_t v0 = rid % 2;
+            columns[1]->insert_data((const char*)&v0, sizeof(v0));
 
-            // Insert NULL for most rows (sparse pattern: 90% NULL)
+            // The first row is non-NULL, so the first sparse batch must 
execute replace.
             if (rid % 10 == 0) {
-                // non-NULL value
-                int32_t c2 = c1 * 10;
-                columns[1]->insert_data((const char*)&c2, sizeof(c2));
+                int64_t v1 = static_cast<int64_t>(k1) * 10;
+                columns[2]->insert_data((const char*)&v1, sizeof(v1));
             } else {
-                // NULL value
-                columns[1]->insert_default();
+                columns[2]->insert_default();
             }
 
             uint8_t delete_sign = 0;
-            columns[2]->insert_data((const char*)&delete_sign, 
sizeof(delete_sign));
+            columns[3]->insert_data((const char*)&delete_sign, 
sizeof(delete_sign));
         }
 
         auto s = add_block_with_columns(rowset_writer.get(), &block, &columns);
@@ -1599,7 +1615,6 @@ TEST_F(VerticalCompactionTest, 
TestUniqueKeyVerticalMergeWithNullableSparseColum
     RowIdConversion rowid_conversion;
     stats.rowid_conversion = &rowid_conversion;
 
-    // This will trigger the _sample_info->null_count logic in 
vertical_block_reader.cpp
     auto s = Merger::vertical_merge_rowsets(tablet, 
ReaderType::READER_BASE_COMPACTION,
                                             *tablet_schema, input_rs_readers,
                                             output_rs_writer.get(), 10000, 
num_segments, &stats);
@@ -1608,11 +1623,38 @@ TEST_F(VerticalCompactionTest, 
TestUniqueKeyVerticalMergeWithNullableSparseColum
     RowsetSharedPtr out_rowset;
     ASSERT_EQ(Status::OK(), output_rs_writer->build(out_rowset));
 
-    // Verify output
-    EXPECT_EQ(out_rowset->rowset_meta()->num_rows(), num_input_rowset * 
rows_per_segment);
+    RowsetReaderContext reader_context;
+    reader_context.tablet_schema = tablet_schema;
+    reader_context.need_ordered_result = false;
+    std::vector<uint32_t> return_columns = {0, 1, 2};
+    reader_context.return_columns = &return_columns;
+    RowsetReaderSharedPtr output_rs_reader;
+    create_and_init_rowset_reader(out_rowset.get(), reader_context, 
&output_rs_reader);
 
-    // Restore original threshold
-    config::sparse_column_compaction_threshold_percent = original_threshold;
+    Block output_block;
+    size_t output_rows = 0;
+    do {
+        block_create(tablet_schema, &output_block);
+        s = output_rs_reader->next_batch(&output_block);
+        auto columns = output_block.get_columns_with_type_and_name();
+        ASSERT_EQ(columns.size(), 3);
+        const auto& nullable_v1 = assert_cast<const 
ColumnNullable&>(*columns[2].column);
+        for (size_t row = 0; row < output_block.rows(); ++row) {
+            int64_t k1 = columns[0].column->get_int(row);
+            EXPECT_EQ(k1, output_rows);
+            EXPECT_EQ(columns[1].column->get_bool(row), k1 % 2 != 0);
+            if (k1 % 10 == 0) {
+                EXPECT_FALSE(nullable_v1.is_null_at(row));
+                EXPECT_EQ(nullable_v1.get_nested_column().get_int(row), k1 * 
10);
+            } else {
+                EXPECT_TRUE(nullable_v1.is_null_at(row));
+            }
+            ++output_rows;
+        }
+    } while (s.ok());
+    EXPECT_TRUE(s.is<END_OF_FILE>()) << s;
+    EXPECT_EQ(output_rows, num_input_rowset * rows_per_segment);
+    EXPECT_EQ(out_rowset->rowset_meta()->num_rows(), output_rows);
 }
 
 // Test that first-time compaction (no historical sampling) uses footer 
raw_data_bytes


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to