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

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


The following commit(s) were added to refs/heads/branch-4.2 by this push:
     new 807c3a02a37 branch-4.2: [Opt](variant) Stream the VARIANT root column 
and bound segment-buffering writers (#69362)
807c3a02a37 is described below

commit 807c3a02a37d667cc632b6d5096ce2e1e22b8b3b
Author: lihangyu <[email protected]>
AuthorDate: Wed Oct 21 18:31:52 2026 +0800

    branch-4.2: [Opt](variant) Stream the VARIANT root column and bound 
segment-buffering writers (#69362)
    
    cherry-pick #69221
---
 be/src/storage/segment/column_writer.h             |  25 +
 .../segment/variant/v2/variant_column_writer.cpp   |  41 +-
 .../segment/variant/v2/variant_column_writer.h     |   9 +-
 .../segment/variant/v2/variant_path_builder.cpp    |  75 ++-
 .../segment/variant/v2/variant_path_builder.h      |  16 +-
 .../segment/variant/variant_column_writer_impl.cpp | 212 +++++--
 .../segment/variant/variant_writer_helpers.cpp     |  32 +-
 .../segment/variant/variant_writer_helpers.h       |   7 +
 .../variant/variant_column_writer_reader_test.cpp  | 628 ++++++++++++++++++++-
 9 files changed, 962 insertions(+), 83 deletions(-)

diff --git a/be/src/storage/segment/column_writer.h 
b/be/src/storage/segment/column_writer.h
index bcfbcb2c7d1..da9e96741b5 100644
--- a/be/src/storage/segment/column_writer.h
+++ b/be/src/storage/segment/column_writer.h
@@ -647,6 +647,14 @@ public:
 
     size_t get_non_null_size() const { return none_null_size; }
 
+#ifdef BE_TEST
+    // Force small chunk budgets so tests can exercise multi-chunk flushes 
cheaply.
+    static void set_chunk_bytes_for_test(size_t bytes);
+    static void set_chunk_values_for_test(size_t values);
+    size_t num_chunks_for_test() const;
+    size_t max_chunk_bytes_for_test() const;
+#endif
+
     Status finalize();
 
 private:
@@ -655,6 +663,17 @@ private:
                       std::span<const uint8_t> outer_nulls);
     Status _ensure_input_format(const VariantColumnData& column);
     Status _initialize_v2_builder();
+    Status _promote_chunks(DataTypePtr* inferred_type);
+    Status _seal_chunk();
+    // Bounded value chunks; each chunk is finalized and flushed independently 
so the writer never
+    // holds a segment-sized ColumnString.
+    struct ValueChunk {
+        std::unique_ptr<VariantPathBuilder> builder;
+        size_t row_begin = 0;
+        size_t rows = 0;
+    };
+    static constexpr size_t kChunkBytes = 256ULL << 20;
+    static constexpr size_t kChunkValues = 4ULL << 20;
     bool is_finalized() const;
     bool _is_finalized = false;
     ordinal_t _next_rowid = 0;
@@ -662,6 +681,12 @@ private:
     VariantWriterInputFormat _input_format = VariantWriterInputFormat::UNSET;
     ColumnVariant::MutablePtr _v1_column;
     std::unique_ptr<VariantPathBuilder> _v2_builder;
+    std::vector<ValueChunk> _chunks;
+    size_t _chunk_row_begin = 0;
+    size_t _chunk_rows = 0;
+    // Type carried into the next chunk. Inferred promotions also check and 
update sealed chunks
+    // so fallback follows the same sequence as one uninterrupted builder.
+    DataTypePtr _running_type;
     size_t _num_rows = 0;
     ColumnWriterOptions _opts;
     std::unique_ptr<ColumnWriter> _writer;
diff --git a/be/src/storage/segment/variant/v2/variant_column_writer.cpp 
b/be/src/storage/segment/variant/v2/variant_column_writer.cpp
index a641fae2492..e7c8da11f2e 100644
--- a/be/src/storage/segment/variant/v2/variant_column_writer.cpp
+++ b/be/src/storage/segment/variant/v2/variant_column_writer.cpp
@@ -107,7 +107,12 @@ Status VariantV2ColumnWriter::init() {
     _root_only = variant_writer_helpers::has_extracted_variant_columns(
             *_opts.rowset_ctx->tablet_schema, _tablet_column->unique_id());
     if (_root_only) {
-        _root_jsonb = ColumnString::create();
+        // Stream the root column instead of buffering a whole segment: the 
JSONB ColumnString
+        // would otherwise have to hold every row of the segment and could 
exceed the
+        // ColumnStr<uint32_t> 4 GiB limit before finalize.
+        _root_writer = std::make_unique<ScalarColumnWriter>(
+                _opts, std::make_shared<TabletColumn>(*_tablet_column), 
_opts.file_writer);
+        RETURN_IF_ERROR(_root_writer->init());
         return Status::OK();
     }
 
@@ -127,15 +132,14 @@ Status VariantV2ColumnWriter::append(const 
VariantColumnData& column, size_t num
         return Status::InternalError("Cannot append ColumnVariantV2 after 
writer finalization");
     }
     if (_root_only) {
-        DORIS_CHECK(_root_jsonb);
+        DORIS_CHECK(_root_writer);
         if (num_rows > std::numeric_limits<size_t>::max() - _num_rows) {
             return Status::InvalidArgument("Variant writer row count overflows 
size_t");
         }
         ColumnString::MutablePtr batch_root_jsonb;
         RETURN_IF_ERROR(build_root_only_batch(column, num_rows, outer_nulls, 
&batch_root_jsonb));
         DORIS_CHECK_EQ(batch_root_jsonb->size(), num_rows);
-        RETURN_IF_CATCH_EXCEPTION(
-                { _root_jsonb->insert_range_from(*batch_root_jsonb, 0, 
num_rows); });
+        RETURN_IF_ERROR(_append_root_batch(*batch_root_jsonb, outer_nulls));
     } else {
         RETURN_IF_ERROR(variant_writer_helpers::append_variant_v2_to_shredder(
                 _shredder.get(), column, num_rows, outer_nulls));
@@ -150,6 +154,25 @@ Status VariantV2ColumnWriter::append(const 
VariantColumnData& column, size_t num
     return Status::OK();
 }
 
+Status VariantV2ColumnWriter::_append_root_batch(const ColumnString& batch,
+                                                 std::span<const uint8_t> 
outer_nulls) {
+    DORIS_CHECK(_root_writer != nullptr);
+    if (batch.size() == 0) {
+        return Status::OK();
+    }
+    if (!outer_nulls.empty() && outer_nulls.size() != batch.size()) {
+        return Status::InvalidArgument("Variant root outer-null span has {} 
rows, expected {}",
+                                       outer_nulls.size(), batch.size());
+    }
+    DorisVector<Slice> slices(batch.size());
+    for (size_t row = 0; row < batch.size(); ++row) {
+        slices[row] = batch.get_data_at(row).to_slice();
+    }
+    const uint8_t* null_map =
+            _tablet_column->is_nullable() && !outer_nulls.empty() ? 
outer_nulls.data() : nullptr;
+    return _root_writer->append(null_map, slices.data(), batch.size());
+}
+
 Status VariantV2ColumnWriter::_write_root(const IColumn* root_jsonb, int& 
column_id) {
     if (!root_jsonb || root_jsonb->size() != _num_rows) {
         return Status::InvalidArgument("Variant root column has {} rows, 
expected {}",
@@ -229,10 +252,8 @@ Status VariantV2ColumnWriter::finalize() {
     }
     if (_root_only) {
         DORIS_CHECK(_shredder == nullptr);
-        DORIS_CHECK(_root_jsonb);
-        int column_id = 0;
-        RETURN_IF_ERROR(_write_root(_root_jsonb.get(), column_id));
-        _root_jsonb.reset();
+        DORIS_CHECK(_root_writer != nullptr);
+        _opts.meta->set_num_rows(_num_rows);
         _is_finalized = true;
         return Status::OK();
     }
@@ -341,8 +362,8 @@ Status VariantV2ColumnWriter::write_bloom_filter_index() {
 uint64_t VariantV2ColumnWriter::estimate_buffer_size() {
     if (!_is_finalized) {
         if (_root_only) {
-            DORIS_CHECK(_root_jsonb);
-            return _outer_nulls->byte_size() + _root_jsonb->byte_size();
+            DORIS_CHECK(_root_writer != nullptr);
+            return _outer_nulls->byte_size() + 
_root_writer->estimate_buffer_size();
         }
         DORIS_CHECK(_shredder != nullptr);
         return _outer_nulls->byte_size() + _shredder->byte_size();
diff --git a/be/src/storage/segment/variant/v2/variant_column_writer.h 
b/be/src/storage/segment/variant/v2/variant_column_writer.h
index ea982f58a87..491cdf5a964 100644
--- a/be/src/storage/segment/variant/v2/variant_column_writer.h
+++ b/be/src/storage/segment/variant/v2/variant_column_writer.h
@@ -38,9 +38,9 @@ class VariantBinaryWriter;
 class VariantShredder;
 struct VariantShreddedColumns;
 
-// Native storage writer for ColumnVariantV2. Full values are incrementally 
shredded at each append;
-// a parent with extracted children retains only its root JSONB column. The 
writer never retains a
-// segment-sized copy of the input execution column.
+// Native storage writer for ColumnVariantV2. Full values are incrementally 
shredded at each append.
+// A parent with extracted children streams its root JSONB column into the 
root column writer batch
+// by batch. The writer never retains a segment-sized copy of the input 
execution column.
 class VariantV2ColumnWriter {
 public:
     VariantV2ColumnWriter(ColumnWriterOptions opts, const TabletColumn* 
column);
@@ -65,6 +65,8 @@ public:
 
 private:
     Status _write_root(const IColumn* root_jsonb, int& column_id);
+    // Streams one batch of the root-only JSONB column into the root column 
writer.
+    Status _append_root_batch(const ColumnString& batch, std::span<const 
uint8_t> outer_nulls);
     Status _write_materialized(const VariantShreddedColumns& shredded,
                                OlapBlockDataConvertor* converter, int& 
column_id);
     Status _write_binary(const VariantShreddedColumns& shredded, 
OlapBlockDataConvertor* converter,
@@ -74,7 +76,6 @@ private:
     ColumnWriterOptions _opts;
     const TabletColumn* _tablet_column = nullptr;
     std::unique_ptr<VariantShredder> _shredder;
-    ColumnString::MutablePtr _root_jsonb;
     ColumnUInt8::MutablePtr _outer_nulls;
     size_t _num_rows = 0;
     bool _root_only = false;
diff --git a/be/src/storage/segment/variant/v2/variant_path_builder.cpp 
b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
index 9c79d604894..6ad8be228bd 100644
--- a/be/src/storage/segment/variant/v2/variant_path_builder.cpp
+++ b/be/src/storage/segment/variant/v2/variant_path_builder.cpp
@@ -371,6 +371,11 @@ DataTypePtr path_least_common_type(const DataTypePtr& 
left, const DataTypePtr& r
     if (left.get() == right.get() || left->equals(*right)) {
         return left;
     }
+    // Chunk builders expose Nullable(Array(...)). Unwrap before dispatching 
so decimal array
+    // elements use the scale-aware rules below instead of the generic 
first-primitive match.
+    if (left->is_nullable() || right->is_nullable()) {
+        return make_nullable(path_least_common_type(remove_nullable(left), 
remove_nullable(right)));
+    }
     const auto* left_array = typeid_cast<const DataTypeArray*>(left.get());
     const auto* right_array = typeid_cast<const DataTypeArray*>(right.get());
     if (left_array != nullptr || right_array != nullptr) {
@@ -1042,6 +1047,10 @@ bool cast_introduced_null(const IColumn& source, const 
IColumn& target) {
 
 } // namespace
 
+DataTypePtr variant_path_least_common_type(const DataTypePtr& left, const 
DataTypePtr& right) {
+    return path_least_common_type(left, right);
+}
+
 DataTypePtr normalize_variant_path_integer_widths(const DataTypePtr& type) {
     const DataTypePtr base = remove_nullable(type);
     if (const auto* array = typeid_cast<const DataTypeArray*>(base.get())) {
@@ -1082,39 +1091,54 @@ struct VariantPathBuilder::Impl {
         return Status::OK();
     }
 
-    Status promote(const DataTypePtr& target_type, bool filter_cast_nulls) {
-        DataTypePtr target = remove_nullable(target_type);
-        if (target->equals(*type)) {
-            return Status::OK();
-        }
-        ColumnPtr promoted;
+    Status cast_to_type(const DataTypePtr& target, ColumnPtr* promoted) const {
         if (target->get_primitive_type() == TYPE_STRING &&
             type->get_primitive_type() == TYPE_ARRAY) {
             RETURN_IF_ERROR(stringify_complex_column(
-                    type, assert_cast<const ColumnNullable&>(*column), 
&promoted));
+                    type, assert_cast<const ColumnNullable&>(*column), 
promoted));
         } else if (type->get_primitive_type() == TYPE_ARRAY &&
                    target->get_primitive_type() == TYPE_ARRAY &&
                    variant_path_type_contains_nothing(type)) {
             MutableColumnPtr materialized;
             RETURN_IF_ERROR(replace_array_nothing(nullable_type, *column, 
make_nullable(target),
                                                   &materialized));
-            promoted = std::move(materialized);
+            *promoted = std::move(materialized);
         } else {
             RETURN_IF_ERROR(
                     variant_util::cast_column({column->get_ptr(), 
nullable_type, path.get_path()},
-                                              make_nullable(target), 
&promoted));
+                                              make_nullable(target), 
promoted));
         }
+        return Status::OK();
+    }
+
+    Status promote(const DataTypePtr& target_type, bool filter_cast_nulls,
+                   const InferredPromotion& on_promotion = {}) {
+        DataTypePtr target = remove_nullable(target_type);
+        if (target->equals(*type)) {
+            return Status::OK();
+        }
+        ColumnPtr promoted;
+        RETURN_IF_ERROR(cast_to_type(target, &promoted));
         // Inferred widening is only a storage representation choice. If CAST 
loses a valid value
         // at any array depth, preserve the whole path as JSONB instead. 
Forced typed-path
         // conversion intentionally retains its existing cast-null filtering 
below.
         if (!filter_cast_nulls && target->get_primitive_type() != TYPE_JSONB &&
             cast_introduced_null(*column, *promoted)) {
             target = jsonb_type();
-            RETURN_IF_ERROR(
-                    variant_util::cast_column({column->get_ptr(), 
nullable_type, path.get_path()},
-                                              make_nullable(target), 
&promoted));
+            RETURN_IF_ERROR(cast_to_type(target, &promoted));
             DORIS_CHECK(!assert_cast<const 
ColumnNullable&>(*promoted).has_null());
         }
+        if (on_promotion) {
+            const DataTypePtr proposed = target;
+            RETURN_IF_ERROR(on_promotion(&target));
+            target = remove_nullable(target);
+            if (!target->equals(*proposed)) {
+                // For example, the active chunk's 0.1 fits scale 38, but a 
sealed large Decimal
+                // does not. Recast the original active values to JSONB, 
preserving 0.1's scale.
+                DORIS_CHECK_EQ(target->get_primitive_type(), TYPE_JSONB);
+                RETURN_IF_ERROR(cast_to_type(target, &promoted));
+            }
+        }
         const auto& nullable = assert_cast<const ColumnNullable&>(*promoted);
         if (nullable.has_null()) {
             DORIS_CHECK(filter_cast_nulls);
@@ -1164,7 +1188,8 @@ VariantPathBuilder::~VariantPathBuilder() = default;
 VariantPathBuilder::VariantPathBuilder(VariantPathBuilder&&) noexcept = 
default;
 VariantPathBuilder& VariantPathBuilder::operator=(VariantPathBuilder&&) 
noexcept = default;
 
-Status VariantPathBuilder::append(VariantRef value, size_t row) {
+Status VariantPathBuilder::append(VariantRef value, size_t row,
+                                  const InferredPromotion& on_promotion) {
     try {
         const ScalarPhysical physical = scalar_physical(value);
         if (physical.kind == ScalarPhysicalKind::NULL_VALUE) {
@@ -1193,14 +1218,14 @@ Status VariantPathBuilder::append(VariantRef value, 
size_t row) {
                     DataTypePtr common_type = 
path_least_common_type(_impl->type, inferred_type);
                     if (_impl->type.get() != common_type.get() &&
                         !_impl->type->equals(*common_type)) {
-                        RETURN_IF_ERROR(_impl->promote(common_type, false));
+                        RETURN_IF_ERROR(_impl->promote(common_type, false, 
on_promotion));
                     }
                 }
             }
         }
         const bool is_array = !stable_scalar && value_kind(value) == 
ValueKind::ARRAY;
         if (is_array && !value_is_representable(value, _impl->type)) {
-            RETURN_IF_ERROR(_impl->promote(jsonb_type(), false));
+            RETURN_IF_ERROR(_impl->promote(jsonb_type(), false, on_promotion));
         }
 
         try {
@@ -1211,7 +1236,7 @@ Status VariantPathBuilder::append(VariantRef value, 
size_t row) {
             if (is_array || _impl->type->get_primitive_type() == TYPE_JSONB) {
                 throw;
             }
-            RETURN_IF_ERROR(_impl->promote(jsonb_type(), false));
+            RETURN_IF_ERROR(_impl->promote(jsonb_type(), false, on_promotion));
             auto& nullable = assert_cast<ColumnNullable&>(*_impl->column);
             append_value(value, _impl->type, &nullable.get_nested_column());
             nullable.get_null_map_data().push_back(0);
@@ -1236,11 +1261,25 @@ Status VariantPathBuilder::complete_rows(size_t rows) {
     return Status::OK();
 }
 
-Status VariantPathBuilder::convert_to(const DataTypePtr& storage_type) {
+Status VariantPathBuilder::convert_to(const DataTypePtr& storage_type, bool 
filter_cast_nulls) {
     if (!_impl->column) {
         return _impl->initialize(storage_type);
     }
-    return _impl->promote(storage_type, true);
+    return _impl->promote(storage_type, filter_cast_nulls);
+}
+
+Status VariantPathBuilder::check_inferred_promotion(const DataTypePtr& 
target_type,
+                                                    bool* needs_jsonb) const {
+    const DataTypePtr target = remove_nullable(target_type);
+    *needs_jsonb = false;
+    if (!_impl->column || target->equals(*_impl->type) ||
+        target->get_primitive_type() == TYPE_JSONB) {
+        return Status::OK();
+    }
+    ColumnPtr promoted;
+    RETURN_IF_ERROR(_impl->cast_to_type(target, &promoted));
+    *needs_jsonb = cast_introduced_null(*_impl->column, *promoted);
+    return Status::OK();
 }
 
 const PathInData& VariantPathBuilder::path() const {
diff --git a/be/src/storage/segment/variant/v2/variant_path_builder.h 
b/be/src/storage/segment/variant/v2/variant_path_builder.h
index 7b467a17ae7..cc21594840a 100644
--- a/be/src/storage/segment/variant/v2/variant_path_builder.h
+++ b/be/src/storage/segment/variant/v2/variant_path_builder.h
@@ -19,6 +19,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <functional>
 #include <memory>
 #include <span>
 
@@ -54,6 +55,10 @@ bool variant_path_type_contains_nothing(const DataTypePtr& 
type);
 // returns.
 class VariantPathBuilder final {
 public:
+    // Resolve inferred widening against values held outside this builder, 
before either side
+    // commits its cast. The resolver may replace the proposed type with JSONB.
+    using InferredPromotion = std::function<Status(DataTypePtr*)>;
+
     explicit VariantPathBuilder(PathInData path, size_t prefix_rows = 0);
     ~VariantPathBuilder();
 
@@ -62,9 +67,13 @@ public:
     VariantPathBuilder(const VariantPathBuilder&) = delete;
     VariantPathBuilder& operator=(const VariantPathBuilder&) = delete;
 
-    Status append(VariantRef value, size_t row);
+    Status append(VariantRef value, size_t row, const InferredPromotion& 
on_promotion = {});
     Status complete_rows(size_t rows);
-    Status convert_to(const DataTypePtr& storage_type);
+    // Inferred conversions preserve values through JSONB fallback; explicit 
storage coercion
+    // retains the default cast-null filtering.
+    Status convert_to(const DataTypePtr& storage_type, bool filter_cast_nulls 
= true);
+    // Probe inferred widening without changing values, types or rowids in 
retained chunks.
+    Status check_inferred_promotion(const DataTypePtr& target_type, bool* 
needs_jsonb) const;
 
     const PathInData& path() const;
     const DataTypePtr& type() const;
@@ -107,4 +116,7 @@ VariantPathSelection select_variant_paths(std::span<const 
VariantPathSelectionCa
                                           size_t 
max_dynamic_materialized_paths,
                                           bool typed_paths_to_sparse);
 
+// Least common type for a variant path. Used to merge types inferred by 
independent value chunks.
+DataTypePtr variant_path_least_common_type(const DataTypePtr& left, const 
DataTypePtr& right);
+
 } // namespace doris::segment_v2
diff --git a/be/src/storage/segment/variant/variant_column_writer_impl.cpp 
b/be/src/storage/segment/variant/variant_column_writer_impl.cpp
index 6e044cf7ccc..b04235b60e6 100644
--- a/be/src/storage/segment/variant/variant_column_writer_impl.cpp
+++ b/be/src/storage/segment/variant/variant_column_writer_impl.cpp
@@ -1979,6 +1979,11 @@ uint64_t 
VariantColumnWriterImpl::get_total_data_pages_bytes(
     return 0;
 }
 
+#ifdef BE_TEST
+static size_t g_chunk_bytes_for_test = 0;
+static size_t g_chunk_values_for_test = 0;
+#endif
+
 VariantSubcolumnWriter::VariantSubcolumnWriter(const ColumnWriterOptions& opts,
                                                TabletColumnPtr column)
         : ColumnWriter(std::move(column), opts.meta->is_nullable(), opts.meta) 
{
@@ -2019,7 +2024,6 @@ Status VariantSubcolumnWriter::_ensure_input_format(const 
VariantColumnData& col
 
 Status VariantSubcolumnWriter::_append_v2(const VariantColumnData& column, 
size_t num_rows,
                                           std::span<const uint8_t> 
outer_nulls) {
-    DORIS_CHECK(_v2_builder != nullptr);
     DORIS_CHECK(column.column_data != nullptr);
     const auto* source = 
check_and_get_column<ColumnVariantV2>(*column.column_data);
     DORIS_CHECK(source != nullptr);
@@ -2028,7 +2032,23 @@ Status VariantSubcolumnWriter::_append_v2(const 
VariantColumnData& column, size_
                                        column.row_pos, column.row_pos + 
num_rows, source->size());
     }
     DORIS_CHECK(outer_nulls.empty() || outer_nulls.size() == num_rows);
+    if (_v2_builder == nullptr) {
+        RETURN_IF_ERROR(_initialize_v2_builder());
+        if (_running_type != nullptr) {
+            // Continue from the type inferred by the sealed chunks. Appending 
a value that the
+            // previous chunks' type cannot hold (for example a bool after 
integers) then takes
+            // the builder's own JSONB fallback inside this chunk instead of 
being merged away.
+            RETURN_IF_ERROR(_v2_builder->convert_to(_running_type));
+        }
+        _chunk_row_begin = _num_rows;
+        _chunk_rows = 0;
+    }
 
+    VariantPathBuilder::InferredPromotion on_promotion;
+    // Until a chunk is sealed, the active builder already holds every value 
to be checked.
+    if (!_chunks.empty()) {
+        on_promotion = [this](DataTypePtr* type) { return 
_promote_chunks(type); };
+    }
     const auto append_encoded = [&](const ColumnVariantV2::ReadView& view, 
size_t begin) -> Status {
         DORIS_CHECK(!view.is_typed());
         for (size_t offset = 0; offset < num_rows; ++offset) {
@@ -2038,7 +2058,8 @@ Status VariantSubcolumnWriter::_append_v2(const 
VariantColumnData& column, size_
             const size_t input_row = begin + offset;
             const VariantRef value = view.value_at(input_row);
             if (!value.is_null()) {
-                RETURN_IF_ERROR(_v2_builder->append(value, _num_rows + 
offset));
+                RETURN_IF_ERROR(_v2_builder->append(value, _num_rows + offset 
- _chunk_row_begin,
+                                                    on_promotion));
             }
         }
         return Status::OK();
@@ -2046,13 +2067,69 @@ Status VariantSubcolumnWriter::_append_v2(const 
VariantColumnData& column, size_
 
     const auto view = source->read_view();
     if (!view.is_typed()) {
-        return append_encoded(view, column.row_pos);
+        RETURN_IF_ERROR(append_encoded(view, column.row_pos));
+    } else {
+        auto encoded_batch = ColumnVariantV2::create();
+        RETURN_IF_CATCH_EXCEPTION({
+            encoded_batch->insert_range_from(*source, column.row_pos, 
num_rows);
+            encoded_batch->ensure_encoded();
+        });
+        RETURN_IF_ERROR(append_encoded(encoded_batch->read_view(), 0));
+    }
+    _chunk_rows = _num_rows + num_rows - _chunk_row_begin;
+    const size_t chunk_bytes =
+#ifdef BE_TEST
+            g_chunk_bytes_for_test != 0 ? g_chunk_bytes_for_test : kChunkBytes;
+#else
+            kChunkBytes;
+#endif
+    const size_t chunk_values =
+#ifdef BE_TEST
+            g_chunk_values_for_test != 0 ? g_chunk_values_for_test : 
kChunkValues;
+#else
+            kChunkValues;
+#endif
+    if (_v2_builder->byte_size() >= chunk_bytes || 
_v2_builder->non_null_rows() >= chunk_values) {
+        RETURN_IF_ERROR(_seal_chunk());
     }
+    return Status::OK();
+}
 
-    auto encoded_batch = ColumnVariantV2::create();
-    RETURN_IF_CATCH_EXCEPTION(
-            { encoded_batch->insert_range_from(*source, column.row_pos, 
num_rows); });
-    return append_encoded(encoded_batch->read_view(), 0);
+Status VariantSubcolumnWriter::_promote_chunks(DataTypePtr* inferred_type) {
+    // Decide against every retained value before changing any chunk. A small 
scale-1 Decimal may
+    // fit scale 38 while a later large Decimal does not; committing the first 
cast would change
+    // its JSONB representation before discovering that the whole path needs 
fallback.
+    for (const auto& chunk : _chunks) {
+        if (chunk.builder->non_null_rows() == 0) {
+            continue;
+        }
+        bool needs_jsonb = false;
+        
RETURN_IF_ERROR(chunk.builder->check_inferred_promotion(*inferred_type, 
&needs_jsonb));
+        if (needs_jsonb) {
+            *inferred_type = std::make_shared<DataTypeJsonb>();
+            break;
+        }
+    }
+    for (auto& chunk : _chunks) {
+        if (chunk.builder->non_null_rows() != 0) {
+            RETURN_IF_ERROR(
+                    chunk.builder->convert_to(*inferred_type, false /* 
filter_cast_nulls */));
+        }
+    }
+    return Status::OK();
+}
+
+Status VariantSubcolumnWriter::_seal_chunk() {
+    if (_v2_builder == nullptr) {
+        return Status::OK();
+    }
+    RETURN_IF_ERROR(_v2_builder->complete_rows(_chunk_rows));
+    _running_type = _v2_builder->type();
+    _chunks.push_back(ValueChunk {
+            .builder = std::move(_v2_builder), .row_begin = _chunk_row_begin, 
.rows = _chunk_rows});
+    _v2_builder.reset();
+    _chunk_rows = 0;
+    return Status::OK();
 }
 
 Status VariantSubcolumnWriter::_append(const uint8_t* null_map, const 
uint8_t** ptr,
@@ -2089,14 +2166,40 @@ Status VariantSubcolumnWriter::append_data(const 
uint8_t** ptr, size_t num_rows)
     return _append(nullptr, ptr, num_rows);
 }
 
+#ifdef BE_TEST
+void VariantSubcolumnWriter::set_chunk_bytes_for_test(size_t bytes) {
+    g_chunk_bytes_for_test = bytes;
+}
+
+void VariantSubcolumnWriter::set_chunk_values_for_test(size_t values) {
+    g_chunk_values_for_test = values;
+}
+
+size_t VariantSubcolumnWriter::num_chunks_for_test() const {
+    return _chunks.size() + (_v2_builder != nullptr ? 1 : 0);
+}
+
+size_t VariantSubcolumnWriter::max_chunk_bytes_for_test() const {
+    size_t max = _v2_builder != nullptr ? _v2_builder->byte_size() : 0;
+    for (const auto& chunk : _chunks) {
+        max = std::max(max, chunk.builder->byte_size());
+    }
+    return max;
+}
+#endif
+
 uint64_t VariantSubcolumnWriter::estimate_buffer_size() {
     if (is_finalized()) {
         return _writer ? _writer->estimate_buffer_size() : 0;
     }
-    if (_input_format == VariantWriterInputFormat::V2) {
-        return _v2_builder->byte_size();
+    if (_input_format != VariantWriterInputFormat::V2) {
+        return _v1_column->byte_size();
     }
-    return _v1_column->byte_size();
+    size_t bytes = _v2_builder ? _v2_builder->byte_size() : 0;
+    for (const auto& chunk : _chunks) {
+        bytes += chunk.builder->byte_size();
+    }
+    return bytes;
 }
 
 bool VariantSubcolumnWriter::is_finalized() const {
@@ -2110,14 +2213,16 @@ Status VariantSubcolumnWriter::finalize() {
     if (_input_format == VariantWriterInputFormat::UNSET) {
         _input_format = VariantWriterInputFormat::V1;
     }
+    RETURN_IF_ERROR(_seal_chunk());
+
     const auto& parent_column =
             
_opts.rowset_ctx->tablet_schema->column_by_uid(get_column()->parent_unique_id());
     DORIS_CHECK(!parent_column.variant_enable_nested_group());
 
     DataTypePtr flush_type;
     ColumnPtr flush_values;
-    DorisVector<uint32_t> flush_rowids;
-    int64_t non_null_value_size = 0;
+    size_t non_null_value_size = 0;
+    bool publish_builder = false;
     if (_input_format == VariantWriterInputFormat::V1) {
         _v1_column->finalize();
         DCHECK(_v1_column->is_finalized());
@@ -2133,41 +2238,51 @@ Status VariantSubcolumnWriter::finalize() {
         non_null_value_size =
                 
_v1_column->get_subcolumns().get_root()->data.get_non_null_value_size();
     } else {
-        DORIS_CHECK(_v2_builder != nullptr);
-        RETURN_IF_ERROR(_v2_builder->complete_rows(_num_rows));
         DataTypePtr storage_type;
         TabletSchema::SubColumnInfo subcolumn_info;
-        if (variant_util::generate_sub_column_info(
-                    *_opts.rowset_ctx->tablet_schema, 
parent_column.unique_id(),
-                    _v2_builder->path().get_path(), &subcolumn_info)) {
+        const PathInData relative_path = 
get_column()->path_info_ptr()->copy_pop_front();
+        if 
(variant_util::generate_sub_column_info(*_opts.rowset_ctx->tablet_schema,
+                                                   parent_column.unique_id(),
+                                                   relative_path.get_path(), 
&subcolumn_info)) {
             storage_type = 
DataTypeFactory::instance().create_data_type(subcolumn_info.column);
         }
 
-        bool publish_builder = _v2_builder->non_null_rows() != 0;
+        // Merge the types inferred by independent chunks; the result is what 
a single builder would
+        // have inferred for the whole segment.
+        DataTypePtr merged_type;
+        bool has_values = false;
+        for (auto& chunk : _chunks) {
+            if (chunk.builder->non_null_rows() == 0) {
+                continue;
+            }
+            has_values = true;
+            RETURN_IF_ERROR(chunk.builder->convert_to(
+                    
normalize_variant_path_integer_widths(chunk.builder->type())));
+            merged_type = merged_type == nullptr ? chunk.builder->type()
+                                                 : 
variant_path_least_common_type(
+                                                           merged_type, 
chunk.builder->type());
+        }
+
+        publish_builder = has_values;
         if (publish_builder) {
-            RETURN_IF_ERROR(_v2_builder->convert_to(
-                    
normalize_variant_path_integer_widths(_v2_builder->type())));
             publish_builder =
-                    !variant_path_type_contains_nothing(_v2_builder->type()) ||
+                    !variant_path_type_contains_nothing(merged_type) ||
                     (storage_type != nullptr && 
!variant_path_type_contains_nothing(storage_type));
         }
-        if (publish_builder && storage_type != nullptr) {
-            RETURN_IF_ERROR(_v2_builder->convert_to(storage_type));
-        }
 
         if (!publish_builder) {
             flush_type = 
DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_TINYINT,
                                                                       true /* 
is_nullable */);
-            MutableColumnPtr defaults = flush_type->create_column();
-            flush_values = std::move(defaults);
         } else {
-            flush_type = _v2_builder->type();
-            flush_values = _v2_builder->column();
-            const std::span<const uint32_t> rowids = _v2_builder->rowids();
-            flush_rowids.assign(rowids.begin(), rowids.end());
-            non_null_value_size = cast_set<int64_t>(flush_rowids.size());
+            flush_type = storage_type != nullptr ? storage_type : merged_type;
+            for (auto& chunk : _chunks) {
+                if (chunk.builder->non_null_rows() == 0) {
+                    continue;
+                }
+                RETURN_IF_ERROR(chunk.builder->convert_to(flush_type));
+                non_null_value_size += chunk.builder->rowids().size();
+            }
         }
-        _v2_builder.reset();
     }
 
     TabletColumn flush_column = variant_util::get_column_by_type(
@@ -2186,12 +2301,35 @@ Status VariantSubcolumnWriter::finalize() {
             _indexes, &opts, non_null_value_size, 
need_record_none_null_value_size));
 
     _opts = opts;
-    RETURN_IF_ERROR(write_variant_subcolumn_values(_input_format, 
flush_column, _writer.get(),
-                                                   flush_type, flush_values, 
flush_rowids,
-                                                   _num_rows));
+    // One convertor for the whole column. The ARRAY convertor carries its 
accumulated item offset
+    // across calls, so flushing chunk by chunk keeps the absolute array 
offsets correct; a fresh
+    // convertor per chunk would restart them at zero and corrupt the column.
+    OlapBlockDataConvertor converter;
+    if (_input_format == VariantWriterInputFormat::V1) {
+        RETURN_IF_ERROR(write_variant_subcolumn_values(_input_format, 
flush_column, _writer.get(),
+                                                       flush_type, 
flush_values, {}, _num_rows));
+    } else if (!publish_builder) {
+        MutableColumnPtr defaults = flush_type->create_column();
+        RETURN_IF_ERROR(variant_writer_helpers::append_sparse_converted_column(
+                flush_column, _writer.get(), &converter, 0, flush_type, 
std::move(defaults), {},
+                _num_rows));
+    } else {
+        for (auto& chunk : _chunks) {
+            if (chunk.builder->non_null_rows() == 0) {
+                MutableColumnPtr empty = flush_type->create_column();
+                
RETURN_IF_ERROR(variant_writer_helpers::append_sparse_converted_column(
+                        flush_column, _writer.get(), &converter, 0, 
flush_type, std::move(empty),
+                        {}, chunk.rows));
+                continue;
+            }
+            
RETURN_IF_ERROR(variant_writer_helpers::append_sparse_converted_column(
+                    flush_column, _writer.get(), &converter, 0, flush_type, 
chunk.builder->column(),
+                    chunk.builder->rowids(), chunk.rows));
+        }
+    }
     _opts.meta->set_num_rows(_num_rows);
-    none_null_size = cast_set<size_t>(non_null_value_size);
-
+    none_null_size = non_null_value_size;
+    _chunks.clear();
     _is_finalized = true;
     return Status::OK();
 }
diff --git a/be/src/storage/segment/variant/variant_writer_helpers.cpp 
b/be/src/storage/segment/variant/variant_writer_helpers.cpp
index d8e53e405b2..bbaaa06b377 100644
--- a/be/src/storage/segment/variant/variant_writer_helpers.cpp
+++ b/be/src/storage/segment/variant/variant_writer_helpers.cpp
@@ -18,6 +18,7 @@
 #include "storage/segment/variant/variant_writer_helpers.h"
 
 #include <algorithm>
+#include <atomic>
 #include <memory>
 #include <span>
 #include <string>
@@ -320,6 +321,20 @@ Status append_sparse_scalar_column(const TabletColumn& 
tablet_column, ColumnWrit
 
 } // namespace
 
+#ifdef BE_TEST
+namespace {
+std::atomic<uint64_t> g_array_all_null_chunks_for_test = 0;
+} // namespace
+
+uint64_t array_all_null_chunks_for_test() {
+    return g_array_all_null_chunks_for_test.load(std::memory_order_relaxed);
+}
+
+void reset_array_all_null_chunks_for_test() {
+    g_array_all_null_chunks_for_test.store(0, std::memory_order_relaxed);
+}
+#endif
+
 Status append_sparse_converted_column(const TabletColumn& tablet_column, 
ColumnWriter* writer,
                                       OlapBlockDataConvertor* converter, int 
column_id,
                                       const DataTypePtr& type, const 
ColumnPtr& values_column,
@@ -336,16 +351,23 @@ Status append_sparse_converted_column(const TabletColumn& 
tablet_column, ColumnW
     if (total_rows == 0) {
         return Status::OK();
     }
-    if (rowids.empty()) {
-        DORIS_CHECK(tablet_column.is_nullable());
-        return writer->append_nulls(total_rows);
-    }
-
     const DataTypePtr base_type = remove_nullable(type);
     if (base_type->get_primitive_type() == PrimitiveType::TYPE_ARRAY) {
+#ifdef BE_TEST
+        if (rowids.empty()) {
+            g_array_all_null_chunks_for_test.fetch_add(1, 
std::memory_order_relaxed);
+        }
+#endif
+        // ARRAY writers materialize a dense N-row column and update offsets, 
nulls and the
+        // inverted index together. An all-NULL chunk (empty rowids) must take 
this path too:
+        // append_nulls only writes column data and leaves the inverted index 
inconsistent.
         return append_sparse_array_column(tablet_column, writer, converter, 
column_id, type,
                                           values_column, rowids, total_rows);
     }
+    if (rowids.empty()) {
+        DORIS_CHECK(tablet_column.is_nullable());
+        return writer->append_nulls(total_rows);
+    }
     return append_sparse_scalar_column(tablet_column, writer, converter, 
column_id, type,
                                        values_column, rowids, total_rows);
 }
diff --git a/be/src/storage/segment/variant/variant_writer_helpers.h 
b/be/src/storage/segment/variant/variant_writer_helpers.h
index 5caea664990..98722b9f808 100644
--- a/be/src/storage/segment/variant/variant_writer_helpers.h
+++ b/be/src/storage/segment/variant/variant_writer_helpers.h
@@ -76,6 +76,13 @@ Status convert_and_write_column(OlapBlockDataConvertor* 
converter, const TabletC
 
 // Converts only present values. Missing logical rows are appended as NULL; 
ARRAY uses a one-path
 // full-column fallback because its converted representation is not 
fixed-width/strided.
+#ifdef BE_TEST
+// Number of all-NULL ARRAY chunks written through the array path. The 
dispatch must never send
+// them to the scalar append_nulls fast path, which does not maintain the 
inverted index.
+uint64_t array_all_null_chunks_for_test();
+void reset_array_all_null_chunks_for_test();
+#endif
+
 Status append_sparse_converted_column(const TabletColumn& tablet_column, 
ColumnWriter* writer,
                                       OlapBlockDataConvertor* converter, int 
column_id,
                                       const DataTypePtr& type, const 
ColumnPtr& values_column,
diff --git a/be/test/storage/variant/variant_column_writer_reader_test.cpp 
b/be/test/storage/variant/variant_column_writer_reader_test.cpp
index 8c5d89d0b1e..aaf3794ddfa 100644
--- a/be/test/storage/variant/variant_column_writer_reader_test.cpp
+++ b/be/test/storage/variant/variant_column_writer_reader_test.cpp
@@ -47,6 +47,7 @@
 #include "core/value/uuid_value.h"
 #include "core/value/variant/variant_batch_builder.h"
 #include "core/value/variant/variant_parquet_encoding.h"
+#include "exec/common/variant_util.h"
 #include "exec/rowid_fetcher.h"
 #include "exprs/function/search/function_search.h"
 #include "exprs/function/search/variant_inverted_index_search.h"
@@ -79,6 +80,7 @@
 #include "storage/segment/variant/variant_column_reader.h"
 #include "storage/segment/variant/variant_column_writer_impl.h"
 #include "storage/segment/variant/variant_doc_snpashot_compact_iterator.h"
+#include "storage/segment/variant/variant_writer_helpers.h"
 #include "storage/storage_engine.h"
 #include "storage/tablet/tablet_manager.h"
 #include "testutil/variant_util.h"
@@ -1463,6 +1465,25 @@ static Status create_variant_root_reader(const 
SegmentFooterPB& footer,
 
 class VariantColumnWriterReaderTest : public testing::Test {
 public:
+    // Parent VARIANT column with an extracted VARIANT subcolumn `v.payload`, 
i.e. the layout
+    // VariantSubcolumnWriter is responsible for.
+    void init_extracted_variant_schema(int64_t tablet_id) {
+        TabletSchemaPB schema_pb;
+        schema_pb.set_keys_type(KeysType::DUP_KEYS);
+        construct_column(schema_pb.add_column(), 1, "VARIANT", "v", 1);
+        _tablet_schema = std::make_shared<TabletSchema>();
+        _tablet_schema->init_from_pb(schema_pb);
+        const TabletColumn& parent_column = _tablet_schema->column_by_uid(1);
+        TabletColumn extracted_column;
+        extracted_column.set_name(parent_column.name_lower_case() + 
".payload");
+        extracted_column.set_type(FieldType::OLAP_FIELD_TYPE_VARIANT);
+        extracted_column.set_parent_unique_id(parent_column.unique_id());
+        
extracted_column.set_path_info(PathInData(parent_column.name_lower_case() + 
".payload"));
+        extracted_column.set_is_nullable(true);
+        _tablet_schema->append_column(extracted_column);
+        init_tablet_from_current_schema(tablet_id);
+    }
+
     void SetUp() override {
         // absolute dir
         char buffer[MAX_PATH_LEN];
@@ -1826,9 +1847,10 @@ protected:
         return Status::OK();
     }
 
-    Status write_extracted_variant_segment(const ColumnPtr& source, const 
DataTypePtr& source_type,
-                                           std::string_view rowset_id, 
SegmentFooterPB* footer,
-                                           std::string* file_path, size_t 
first_batch_rows = 0) {
+    Status write_extracted_variant_segment(
+            const ColumnPtr& source, const DataTypePtr& source_type, 
std::string_view rowset_id,
+            SegmentFooterPB* footer, std::string* file_path, size_t 
first_batch_rows = 0,
+            size_t batch_rows = 0, const std::function<void(ColumnWriter*)>& 
after_batch = {}) {
         DORIS_CHECK(source.get() != nullptr);
         DORIS_CHECK(source_type != nullptr);
         DORIS_CHECK(footer != nullptr);
@@ -1873,11 +1895,28 @@ protected:
             converter.clear_source_content(0);
             return Status::OK();
         };
-        if (first_batch_rows > 0 && first_batch_rows < source->size()) {
+        if (batch_rows > 0) {
+            for (size_t row_pos = 0; row_pos < source->size(); row_pos += 
batch_rows) {
+                const size_t rows = std::min(batch_rows, source->size() - 
row_pos);
+                RETURN_IF_ERROR(append_batch(row_pos, rows));
+                if (after_batch) {
+                    after_batch(writer.get());
+                }
+            }
+        } else if (first_batch_rows > 0 && first_batch_rows < source->size()) {
             RETURN_IF_ERROR(append_batch(0, first_batch_rows));
+            if (after_batch) {
+                after_batch(writer.get());
+            }
             RETURN_IF_ERROR(append_batch(first_batch_rows, source->size() - 
first_batch_rows));
+            if (after_batch) {
+                after_batch(writer.get());
+            }
         } else {
             RETURN_IF_ERROR(append_batch(0, source->size()));
+            if (after_batch) {
+                after_batch(writer.get());
+            }
         }
 
         RETURN_IF_ERROR(writer->finish());
@@ -1940,9 +1979,15 @@ protected:
         RETURN_IF_ERROR(ColumnReader::create(reader_options, 
footer.columns(0), footer.num_rows(),
                                              file_reader, &reader));
 
-        TabletColumn read_column = _tablet_schema->column(1);
-        read_column.set_type(static_cast<FieldType>(footer.columns(0).type()));
-        auto read_type = 
DataTypeFactory::instance().create_data_type(read_column, false);
+        // Build the read type and the matching tablet column from the footer 
meta so nested types
+        // (ARRAY/MAP/STRUCT) keep their child type information; the schema 
column only carries the
+        // top-level type and the array iterator needs the element sub-column.
+        auto read_type = 
DataTypeFactory::instance().create_data_type(footer.columns(0));
+        TabletColumn read_column = variant_util::get_column_by_type(
+                read_type, _tablet_schema->column(1).name(),
+                variant_util::ExtraInfo {.unique_id = -1,
+                                         .parent_unique_id = 1,
+                                         .path_info = 
*_tablet_schema->column(1).path_info_ptr()});
         ColumnIteratorUPtr iterator;
         RETURN_IF_ERROR(reader->new_iterator(&iterator, &read_column));
         OlapReaderStatistics stats;
@@ -2501,6 +2546,104 @@ TEST_F(VariantColumnWriterReaderTest, 
test_write_column_variant_v2_accounts_root
     EXPECT_EQ(actual[1], "1");
 }
 
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_root_writer_streams_per_batch) {
+    // A variant with an extracted subcolumn takes the root-only writer path. 
Root scalars all
+    // land in the root JSONB column, so the writer must flush them batch by 
batch instead of
+    // retaining the whole segment: buffering SegmentRows * value bytes is 
what used to overflow
+    // ColumnStr<uint32_t> at 4 GiB.
+    TabletSchemaPB schema_pb;
+    schema_pb.set_keys_type(KeysType::DUP_KEYS);
+    construct_column(schema_pb.add_column(), 1, "VARIANT", "v", 10, false, 
false, 0, false, 0, 0,
+                     false);
+    _tablet_schema = std::make_shared<TabletSchema>();
+    _tablet_schema->init_from_pb(schema_pb);
+    const TabletColumn& parent_column = _tablet_schema->column_by_uid(1);
+    TabletColumn extracted_column;
+    extracted_column.set_name(parent_column.name_lower_case() + ".payload");
+    extracted_column.set_type(FieldType::OLAP_FIELD_TYPE_VARIANT);
+    extracted_column.set_parent_unique_id(parent_column.unique_id());
+    extracted_column.set_path_info(PathInData(parent_column.name_lower_case() 
+ ".payload"));
+    extracted_column.set_is_nullable(true);
+    _tablet_schema->append_column(extracted_column);
+    init_tablet_from_current_schema(10043);
+
+    constexpr size_t kRows = 4000;
+    constexpr size_t kBatchRows = 1000;
+    constexpr size_t kValueBytes = 4096;
+    const std::string payload(kValueBytes, 'x');
+    auto source = ColumnVariantV2::create();
+    DataTypeVariantV2SerDe serde;
+    DataTypeSerDe::FormatOptions serde_options;
+    for (size_t row = 0; row < kRows; ++row) {
+        const std::string json = "\"" + payload + "\"";
+        Slice slice(json.data(), json.size());
+        ASSERT_TRUE(serde.deserialize_one_cell_from_json(*source, slice, 
serde_options).ok());
+    }
+
+    const std::string rowset_id = "v2_root_streaming";
+    SegmentFooterPB footer;
+    std::string file_path = local_segment_path(_tablet->tablet_path(), 
rowset_id, 0);
+    ASSERT_TRUE(io::global_local_filesystem()->delete_file(file_path).ok());
+    io::FileWriterPtr file_writer;
+    ASSERT_TRUE(io::global_local_filesystem()->create_file(file_path, 
&file_writer).ok());
+
+    RowsetWriterContext rowset_ctx;
+    rowset_ctx.write_type = DataWriteType::TYPE_DIRECT;
+    rowset_ctx.tablet_schema = _tablet_schema;
+    rowset_ctx.tablet = _tablet;
+    rowset_ctx.tablet_path = _tablet->tablet_path();
+
+    TabletColumn column = _tablet_schema->column(0);
+    ColumnWriterOptions opts;
+    opts.meta = footer.add_columns();
+    opts.compression_type = CompressionTypePB::LZ4;
+    opts.file_writer = file_writer.get();
+    opts.footer = &footer;
+    opts.rowset_ctx = &rowset_ctx;
+    opts.storage_format = TabletStorageFormatPB::TABLET_STORAGE_FORMAT_V2;
+    _init_column_meta(opts.meta, 0, column, opts);
+
+    std::unique_ptr<ColumnWriter> writer;
+    ASSERT_TRUE(ColumnWriter::create(opts, &column, file_writer.get(), 
&writer).ok());
+    ASSERT_TRUE(writer->init().ok());
+
+    OlapBlockDataConvertor converter;
+    converter.add_column_data_convertor(column);
+    const auto source_type = std::make_shared<DataTypeVariantV2>(2, false);
+    uint64_t max_buffered_bytes = 0;
+    for (size_t row_pos = 0; row_pos < kRows; row_pos += kBatchRows) {
+        ASSERT_TRUE(converter
+                            .set_source_content_with_specifid_column(
+                                    {source->get_ptr(), source_type, 
column.name()}, row_pos,
+                                    kBatchRows, 0)
+                            .ok());
+        auto [convert_status, accessor] = converter.convert_column_data(0);
+        ASSERT_TRUE(convert_status.ok());
+        ASSERT_TRUE(writer->append(accessor->get_nullmap(), 
accessor->get_data(), kBatchRows).ok());
+        converter.clear_source_content(0);
+        max_buffered_bytes = std::max<uint64_t>(max_buffered_bytes, 
writer->estimate_buffer_size());
+    }
+    // The buffering writer retained the whole segment (kRows * kValueBytes ~ 
16 MiB). The
+    // streaming writer flushes the root column batch by batch, so its own 
retained raw buffer is
+    // about one batch; encoded pages and the other variant column writers are 
separate.
+    EXPECT_LT(max_buffered_bytes, 2 * kBatchRows * kValueBytes);
+
+    ASSERT_TRUE(writer->finish().ok());
+    ASSERT_TRUE(writer->write_data().ok());
+    ASSERT_TRUE(writer->write_ordinal_index().ok());
+    ASSERT_TRUE(writer->write_zone_map().ok());
+    ASSERT_TRUE(file_writer->close().ok());
+    footer.set_num_rows(kRows);
+
+    std::vector<std::optional<std::string>> actual;
+    ASSERT_TRUE(read_variant_root_rows(footer, file_path, &actual).ok());
+    ASSERT_EQ(actual.size(), kRows);
+    for (const auto& row : actual) {
+        ASSERT_TRUE(row.has_value());
+        EXPECT_EQ(row->size(), kValueBytes + 2);
+    }
+}
+
 TEST_F(VariantColumnWriterReaderTest, 
test_write_column_variant_v2_typed_state) {
     init_variant_tablet(10009, 1);
 
@@ -8944,4 +9087,475 @@ TEST_F(VariantColumnWriterReaderTest, 
test_compaction_nokey_variant_uid0) {
     ASSERT_EQ(output_rowset->num_rows(), 4);
 }
 
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_large_values) {
+    // A large VARIANT subcolumn must be flushed in bounded chunks: a single 
segment-sized
+    // ColumnString is what overflows ColumnStr<uint32_t> at 4 GiB. Compare 
the chunked output with
+    // the single-chunk reference and check the observed chunk budget.
+    init_extracted_variant_schema(11401);
+    constexpr size_t kRows = 3000;
+    constexpr size_t kBatchRows = 300;
+    const std::string value(1024, 'x');
+    const std::string json = "\"" + value + "\"";
+    std::vector<std::string> jsons(kRows, json);
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, {}, &source,
+                                             &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_chunk_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+    ASSERT_EQ(ref_rows.size(), kRows);
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(250); 
}
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    size_t max_chunks = 0;
+    size_t max_chunk_bytes = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_chunk_values", 
&chunked_footer,
+                        &chunked_path, 0, kBatchRows,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                            max_chunk_bytes = std::max(max_chunk_bytes,
+                                                       
subcolumn->max_chunk_bytes_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+    // A chunk holds at most the configured budget plus the batch that crossed 
it.
+    EXPECT_LE(max_chunk_bytes, 250 * 1024 + kBatchRows * 1024);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+    EXPECT_EQ(chunked_rows.front(), ref_rows.front());
+    EXPECT_EQ(chunked_rows.back(), ref_rows.back());
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_type_promotion) {
+    // Values that widen the inferred type across a chunk boundary (int -> 
string -> JSONB) must
+    // still produce the same column as the single-chunk path.
+    init_extracted_variant_schema(11402);
+    constexpr size_t kHalf = 2000;
+    std::vector<std::string> jsons;
+    jsons.reserve(2 * kHalf);
+    for (size_t i = 0; i < kHalf; ++i) {
+        jsons.push_back(std::to_string(i));
+    }
+    for (size_t i = 0; i < kHalf; ++i) {
+        jsons.push_back("\"s" + std::to_string(i) + "\"");
+    }
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, {}, &source,
+                                             &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_promote_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(500); 
}
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    size_t max_chunks = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_promote_chunked", 
&chunked_footer,
+                        &chunked_path, 0, 300,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_sparse_rows) {
+    // Rows without the subcolumn become nulls. When chunks split a gap the 
dense materialization
+    // must still place every value on its logical row.
+    init_extracted_variant_schema(11403);
+    constexpr size_t kRows = 3000;
+    std::vector<std::string> jsons(kRows, "\"v\"");
+    std::vector<UInt8> outer_nulls(kRows, 0);
+    for (size_t row = 0; row < kRows; row += 3) {
+        outer_nulls[row] = 1;
+    }
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, outer_nulls,
+                                             &source, &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_sparse_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(200); 
}
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    size_t max_chunks = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_sparse_chunked", 
&chunked_footer,
+                        &chunked_path, 0, 300,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+    ASSERT_FALSE(chunked_rows.empty());
+    EXPECT_EQ(chunked_rows[0], "NULL");
+    EXPECT_EQ(chunked_rows[1], ref_rows[1]);
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_all_null) {
+    // A subcolumn whose every row is null must still append the right number 
of nulls per chunk.
+    init_extracted_variant_schema(11404);
+    constexpr size_t kRows = 2000;
+    std::vector<std::string> jsons(kRows, "\"v\"");
+    std::vector<UInt8> outer_nulls(kRows, 1);
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, outer_nulls,
+                                             &source, &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_null_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(100); 
}
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_null_chunked",
+                                                &chunked_footer, 
&chunked_path, 0, 300)
+                        .ok());
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+    for (const auto& row : chunked_rows) {
+        EXPECT_EQ(row, "NULL");
+    }
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_by_bytes) {
+    // The byte budget (kChunkBytes) seals a chunk even when the value count 
is small, e.g. few very
+    // wide strings. The chunk must still be flushed independently and read 
back unchanged.
+    init_extracted_variant_schema(11405);
+    constexpr size_t kRows = 3000;
+    constexpr size_t kBatchRows = 200;
+    const std::string value(1024, 'y');
+    const std::string json = "\"" + value + "\"";
+    std::vector<std::string> jsons(kRows, json);
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, {}, &source,
+                                             &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_bytes_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_bytes_for_test(256 * 
1024); }
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_bytes_for_test(0); }
+    } guard;
+    size_t max_chunks = 0;
+    size_t max_chunk_bytes = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_bytes_chunked", 
&chunked_footer,
+                        &chunked_path, 0, kBatchRows,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                            max_chunk_bytes = std::max(max_chunk_bytes,
+                                                       
subcolumn->max_chunk_bytes_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+    EXPECT_LE(max_chunk_bytes, 256 * 1024 + kBatchRows * 1024);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_int_then_bool) {
+    // The single builder falls back to JSONB when a bool follows integers: 
appending `true` into
+    // an Int64 column is not a representable promotion. Chunks must carry the 
inferred type across
+    // their boundary so the same value sequence keeps the JSONB fallback 
instead of coercing
+    // true/false to 1/0 through the least-common-type merge.
+    init_extracted_variant_schema(11406);
+    const std::vector<std::string> jsons {"7", "true", "false", "8"};
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, {}, &source,
+                                             &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_bool_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(1); }
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    size_t max_chunks = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_bool_chunked", 
&chunked_footer,
+                        &chunked_path, 0, 1,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+    EXPECT_EQ(chunked_rows, (std::vector<std::string> {"7", "true", "false", 
"8"}));
+    EXPECT_EQ(static_cast<FieldType>(ref_footer.columns(0).type()),
+              FieldType::OLAP_FIELD_TYPE_JSONB);
+    EXPECT_EQ(static_cast<FieldType>(chunked_footer.columns(0).type()),
+              FieldType::OLAP_FIELD_TYPE_JSONB);
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_array_all_null_tail) {
+    // An all-NULL tail chunk of an ARRAY subcolumn must be flushed through 
the array writer (which
+    // updates offsets, nulls and the inverted index together), not through 
append_nulls.
+    init_extracted_variant_schema(11407);
+    const std::vector<std::string> jsons {"[1,2]", "[\"a\",\"b\"]", "null", 
"null"};
+    ColumnPtr source;
+    DataTypePtr source_type;
+    ASSERT_TRUE(create_variant_writer_source(VariantWriterInput::V2, jsons, 0, 
false, {}, &source,
+                                             &source_type)
+                        .ok());
+
+    SegmentFooterPB ref_footer;
+    std::string ref_path;
+    ASSERT_TRUE(write_extracted_variant_segment(source, source_type, 
"subcolumn_array_ref",
+                                                &ref_footer, &ref_path)
+                        .ok());
+    std::vector<std::string> ref_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+
+    struct ChunkGuard {
+        ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(2); }
+        ~ChunkGuard() { VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+    } guard;
+    segment_v2::variant_writer_helpers::reset_array_all_null_chunks_for_test();
+    size_t max_chunks = 0;
+    SegmentFooterPB chunked_footer;
+    std::string chunked_path;
+    ASSERT_TRUE(write_extracted_variant_segment(
+                        source, source_type, "subcolumn_array_chunked", 
&chunked_footer,
+                        &chunked_path, 0, 1,
+                        [&](ColumnWriter* writer) {
+                            auto* subcolumn = 
dynamic_cast<VariantSubcolumnWriter*>(writer);
+                            ASSERT_NE(subcolumn, nullptr);
+                            max_chunks = std::max(max_chunks, 
subcolumn->num_chunks_for_test());
+                        })
+                        .ok());
+    EXPECT_GE(max_chunks, 2);
+    
EXPECT_EQ(segment_v2::variant_writer_helpers::array_all_null_chunks_for_test(), 
1u);
+
+    std::vector<std::string> chunked_rows;
+    ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+    EXPECT_EQ(chunked_rows, ref_rows);
+    ASSERT_EQ(chunked_rows.size(), 4);
+    EXPECT_EQ(chunked_rows[2], "NULL");
+    EXPECT_EQ(chunked_rows[3], "NULL");
+}
+
+TEST(VariantPathBuilderTest, MergeNullableDecimalArraysPreservesScale) {
+    VariantBatchBuilder value_builder;
+    for (size_t index = 0; index < 2; ++index) {
+        auto row = value_builder.begin_row();
+        auto array = row.start_array();
+        row.add_decimal(index == 0 ? 123 : 12345, index == 0 ? 2 : 4, 16);
+        array.finish();
+        row.finish();
+    }
+    const auto values = value_builder.finish_batch();
+    segment_v2::VariantPathBuilder first(PathInData("metric"));
+    segment_v2::VariantPathBuilder second(PathInData("metric"));
+    ASSERT_TRUE(first.append(values.value_at(0), 0).ok());
+    ASSERT_TRUE(second.append(values.value_at(1), 0).ok());
+    for (bool reverse : {false, true}) {
+        const DataTypePtr common = segment_v2::variant_path_least_common_type(
+                reverse ? second.type() : first.type(), reverse ? first.type() 
: second.type());
+        EXPECT_TRUE(common->is_nullable());
+        const auto& array = assert_cast<const 
DataTypeArray&>(*remove_nullable(common));
+        EXPECT_EQ(array.get_nested_type()->get_scale(), 4);
+    }
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_decimal_overflow) {
+    init_extracted_variant_schema(11408);
+    for (bool array_values : {false, true}) {
+        for (bool double_tail : {false, true}) {
+            SCOPED_TRACE(testing::Message()
+                         << "array=" << array_values << ", double_tail=" << 
double_tail);
+            VariantBatchBuilder value_builder;
+            for (size_t index = 0; index < (double_tail ? 5 : 4); ++index) {
+                auto row = value_builder.begin_row();
+                std::optional<VariantBatchBuilder::Row::ArrayScope> array;
+                if (array_values) {
+                    array.emplace(row.start_array());
+                }
+                if (index == 1) {
+                    
row.add_decimal(static_cast<__int128>(VARIANT_DECIMAL16_MAX), 1, 16);
+                } else if (index == 3) {
+                    row.add_decimal(1, 38, 16);
+                } else if (index == 4) {
+                    row.add_double(2.5);
+                } else {
+                    row.add_decimal(1, 1, 16);
+                }
+                if (array_values) {
+                    array->finish();
+                }
+                row.finish();
+            }
+            auto source = ColumnVariantV2::create();
+            source->insert_encoded_batch(value_builder.finish_batch());
+            const auto source_type = std::make_shared<DataTypeVariantV2>(0, 
false);
+            const auto suffix = fmt::format("{}_{}", array_values, 
double_tail);
+            SegmentFooterPB ref_footer;
+            std::string ref_path;
+            ASSERT_TRUE(write_extracted_variant_segment(source->get_ptr(), 
source_type,
+                                                        
"decimal_overflow_ref_" + suffix,
+                                                        &ref_footer, &ref_path)
+                                .ok());
+            std::vector<std::string> ref_rows;
+            ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+            ASSERT_EQ(static_cast<FieldType>(ref_footer.columns(0).type()),
+                      FieldType::OLAP_FIELD_TYPE_JSONB);
+
+            struct ChunkGuard {
+                explicit ChunkGuard(size_t values) {
+                    VariantSubcolumnWriter::set_chunk_values_for_test(values);
+                }
+                ~ChunkGuard() { 
VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+            } guard(double_tail ? 2 : 1);
+            SegmentFooterPB chunked_footer;
+            std::string chunked_path;
+            ASSERT_TRUE(write_extracted_variant_segment(source->get_ptr(), 
source_type,
+                                                        
"decimal_overflow_chunks_" + suffix,
+                                                        &chunked_footer, 
&chunked_path, 0, 1)
+                                .ok());
+            std::vector<std::string> chunked_rows;
+            ASSERT_TRUE(
+                    read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+            // The scale-38 value cannot widen the earlier large value 
losslessly. With two rows
+            // per chunk, fallback must also preserve the preceding 0.1 in the 
active chunk.
+            EXPECT_EQ(chunked_rows, ref_rows);
+            EXPECT_EQ(static_cast<FieldType>(chunked_footer.columns(0).type()),
+                      FieldType::OLAP_FIELD_TYPE_JSONB);
+        }
+    }
+}
+
+TEST_F(VariantColumnWriterReaderTest, 
test_variant_subcolumn_writer_chunks_decimal_array_scales) {
+    init_extracted_variant_schema(11409);
+    for (bool reverse : {false, true}) {
+        SCOPED_TRACE(testing::Message() << "reverse=" << reverse);
+        VariantBatchBuilder value_builder;
+        for (size_t index = 0; index < 2; ++index) {
+            auto row = value_builder.begin_row();
+            auto array = row.start_array();
+            const bool high_scale = (index == 1) != reverse;
+            row.add_decimal(high_scale ? 12345 : 123, high_scale ? 4 : 2, 16);
+            array.finish();
+            row.finish();
+        }
+        auto source = ColumnVariantV2::create();
+        source->insert_encoded_batch(value_builder.finish_batch());
+        const auto source_type = std::make_shared<DataTypeVariantV2>(0, false);
+        const auto suffix = std::to_string(reverse);
+        SegmentFooterPB ref_footer;
+        std::string ref_path;
+        ASSERT_TRUE(write_extracted_variant_segment(source->get_ptr(), 
source_type,
+                                                    "decimal_array_ref_" + 
suffix, &ref_footer,
+                                                    &ref_path)
+                            .ok());
+        std::vector<std::string> ref_rows;
+        ASSERT_TRUE(read_extracted_variant_rows(ref_footer, ref_path, 
&ref_rows).ok());
+        ASSERT_EQ(ref_footer.columns(0).children_columns(0).frac(), 4);
+
+        struct ChunkGuard {
+            ChunkGuard() { 
VariantSubcolumnWriter::set_chunk_values_for_test(1); }
+            ~ChunkGuard() { 
VariantSubcolumnWriter::set_chunk_values_for_test(0); }
+        } guard;
+        SegmentFooterPB chunked_footer;
+        std::string chunked_path;
+        ASSERT_TRUE(write_extracted_variant_segment(source->get_ptr(), 
source_type,
+                                                    "decimal_array_chunks_" + 
suffix,
+                                                    &chunked_footer, 
&chunked_path, 0, 1)
+                            .ok());
+        std::vector<std::string> chunked_rows;
+        ASSERT_TRUE(read_extracted_variant_rows(chunked_footer, chunked_path, 
&chunked_rows).ok());
+        EXPECT_EQ(chunked_rows, ref_rows);
+        EXPECT_EQ(chunked_footer.columns(0).children_columns(0).frac(), 4);
+    }
+}
+
 } // namespace doris


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

Reply via email to