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

morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new c19802899e3 [improvement](be) Import unshredded Parquet Variant values 
without rebuilding them (#68959)
c19802899e3 is described below

commit c19802899e344513d38b83427222e1d29e51939e
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Tue Oct 13 16:21:35 2026 +0800

    [improvement](be) Import unshredded Parquet Variant values without 
rebuilding them (#68959)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: #66714, #66910, #67016
    
    Problem Summary:
    
    **In short.** This ports branch-4.1 #66714 to master. When the native
    Parquet reader reads an unshredded Variant column, master decodes every
    value and encodes it again through `VariantBatchBuilder`, although the
    bytes already are Variant Encoding V1. With this PR the reader validates
    those bytes and copies them into `ColumnVariantV2`. Because the copy
    validates every metadata dictionary, a corrupt metadata key is reported
    instead of being dropped.
    
    **Background.** A Parquet Variant column without shredding stores two
    binary fields per row: `metadata` (the key dictionary) and `value`.
    `ColumnVariantV2` keeps rows the same way, as deduplicated metadata
    blobs plus one value per row. master #67016, the port of branch-4.1
    #66910, added a direct path seek for these columns. In branch-4.1,
    #66910 builds on #66714, which never reached master, so #67016
    re-encoded the values it found with `append_variant_rows` and left out
    the two #66910 tests that need #66714.
    
    **The problem, and what it cost.**
    
    | Reading an unshredded Parquet Variant | master | with this PR |
    |---|---|---|
    | The whole value (`SELECT v`) | every row decoded and re-encoded
    through `VariantBatchBuilder` (`encode_variant_range`) | metadata and
    value validated, then copied; the profile reports
    `VariantUnshreddedDirectImportTime`, `Rows` and `Bytes` |
    | One path (`SELECT v['a']`), direct seek | each found value re-encoded
    through `append_variant_rows`, which keeps one metadata dictionary per
    batch and splits the batch in halves when rows use different
    dictionaries | found values copied through the same importer |
    | A row whose metadata has a corrupt key that the value does not use
    (for example invalid UTF-8) | re-encoding drops the key, and the row
    reads as if it were valid | the read fails with a corruption error, as
    in branch-4.x |
    
    **How this PR fixes it.**
    
    - `ColumnVariantV2::EncodedRowsAppender`, as in branch-4.x: it validates
    each distinct metadata dictionary once and every value, rejects input
    that borrows bytes from the destination column (including its shredded
    materialization), and then copies the rows, interning metadata through a
    hash index that it keeps across batches. A failed append leaves a typed
    or shredded destination unchanged. `insert_encoded_rows` and
    `insert_encoded_batch` now share `_append_prevalidated_encoded_data`.
    - `variant_column_reader.cpp`: when the column is materialized, complete
    unshredded roots go through the appender in 4096-row batches, and
    `normalize_unshredded_path` uses it instead of `append_variant_rows`,
    which is removed. Three persistent profile counters report the time,
    rows and bytes.
    - The code follows master's `ColumnVariantV2`, which holds typed
    `ColumnString` members where branch-4.x cast `IColumn`s. Hunks of the
    same files that belong to other branch-4.x PRs (#66575, #66941) are not
    included.
    
    **Results.**
    
    - BE UT: the 4 new `ColumnVariantV2Test` cases, the 4 new
    `VariantColumnReaderTest` cases, the two #66910 corrupt-metadata cases
    and two new assertions pass, together with the other Variant, JNI and
    Parquet schema suites (755 tests).
    - With the production change reverted, exactly the six new reader cases
    fail; the two corrupt-metadata cases fail because the read throws
    nothing.
    
    Co-authored-by: daidai <[email protected]>
    Co-authored-by: lihangyu <[email protected]>
---
 .../core/column/variant_v2/column_variant_v2.cpp   | 331 +++++++++++++++++++--
 be/src/core/column/variant_v2/column_variant_v2.h  |  34 +++
 be/src/format_v2/parquet/parquet_profile.cpp       |   9 +
 be/src/format_v2/parquet/parquet_profile.h         |   9 +
 .../parquet/reader/variant_column_reader.cpp       | 132 ++++++--
 be/test/core/column/column_variant_v2_test.cpp     | 174 +++++++++++
 .../parquet/variant_column_reader_test.cpp         | 302 +++++++++++++++++++
 7 files changed, 927 insertions(+), 64 deletions(-)

diff --git a/be/src/core/column/variant_v2/column_variant_v2.cpp 
b/be/src/core/column/variant_v2/column_variant_v2.cpp
index 48ee53a8f9f..edab4bc6ecb 100644
--- a/be/src/core/column/variant_v2/column_variant_v2.cpp
+++ b/be/src/core/column/variant_v2/column_variant_v2.cpp
@@ -25,6 +25,7 @@
 #include <mutex>
 #include <string_view>
 #include <typeinfo>
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
@@ -130,6 +131,20 @@ void reserve_rows(ColumnString& values, MetaIdsColumn& 
metadata_ids, size_t valu
     metadata_ids.get_data().reserve(metadata_ids.size() + rows);
 }
 
+bool byte_ranges_overlap(StringRef left, StringRef right) {
+    if (left.size == 0 || right.size == 0) {
+        return false;
+    }
+    DCHECK(left.data != nullptr);
+    DCHECK(right.data != nullptr);
+    const auto left_begin = reinterpret_cast<uintptr_t>(left.data);
+    const auto right_begin = reinterpret_cast<uintptr_t>(right.data);
+    // Compare start-address distances instead of forming end pointers, which 
also avoids integer
+    // overflow for a malformed borrowed size.
+    return left_begin <= right_begin ? right_begin - left_begin < left.size
+                                     : left_begin - right_begin < right.size;
+}
+
 size_t validate_selected_indices(const uint32_t* indices_begin, const 
uint32_t* indices_end,
                                  size_t source_rows) {
     if (indices_begin == indices_end) {
@@ -979,42 +994,239 @@ void ColumnVariantV2::insert_encoded_rows( // 
NOLINT(readability-function-size)
         }
     }
 
-    if (_typed || _shredded) {
-        // Validate the incoming batch before materializing the destination so 
malformed input
-        // cannot mutate a typed or segmented Variant column as a side effect.
-        ensure_encoded();
+    _append_prevalidated_encoded_data(data);
+}
+
+struct ColumnVariantV2::EncodedRowsAppender::Impl {
+    using MetadataHashHeads =
+            std::unordered_map<size_t, uint32_t, std::hash<size_t>, 
std::equal_to<size_t>,
+                               CustomStdAllocator<std::pair<const size_t, 
uint32_t>>>;
+
+    explicit Impl(ColumnVariantV2& destination) : column(&destination) {}
+
+    ColumnVariantV2* column;
+    MetadataHashHeads metadata_hash_heads;
+    DorisVector<uint32_t> metadata_hash_next;
+    size_t expected_rows = 0;
+    bool initialized = false;
+#ifdef BE_TEST
+    size_t metadata_comparisons = 0;
+#endif
+};
+
+ColumnVariantV2::EncodedRowsAppender::EncodedRowsAppender(ColumnVariantV2& 
column)
+        : _impl(std::make_unique<Impl>(column)) {}
+
+ColumnVariantV2::EncodedRowsAppender::~EncodedRowsAppender() = default;
+
+ColumnVariantV2::EncodedRowsAppender::EncodedRowsAppender(EncodedRowsAppender&&)
 noexcept = default;
+
+#ifdef BE_TEST
+size_t ColumnVariantV2::EncodedRowsAppender::metadata_comparisons_for_test() 
const noexcept {
+    return _impl == nullptr ? 0 : _impl->metadata_comparisons;
+}
+#endif
+
+void ColumnVariantV2::EncodedRowsAppender::append( // 
NOLINT(readability-function-size)
+        std::span<const VariantRef> rows) {
+    if (rows.empty()) {
+        return;
     }
-    DORIS_CHECK(_typed_type == nullptr) << "encoded state cannot retain a 
typed data type";
-    require_exclusive(_meta_ids, "metadata ids");
-    require_exclusive(_values, "values");
-    auto& values = *_values;
-    auto& metadata_ids = *_meta_ids;
-    reserve_rows(values, metadata_ids, data.value_bytes.size, rows);
+    DORIS_CHECK(_impl != nullptr && _impl->column != nullptr)
+            << "Cannot use a moved-from Variant encoded-row appender";
+    ColumnVariantV2& column = *_impl->column;
+
+    using MetadataIdMap =
+            std::unordered_map<std::string_view, uint32_t, 
std::hash<std::string_view>,
+                               std::equal_to<std::string_view>,
+                               CustomStdAllocator<std::pair<const 
std::string_view, uint32_t>>>;
+    MetadataIdMap metadata_ids_by_value;
+    DorisVector<VariantMetadataRef> unique_metadatas;
+    DorisVector<uint32_t> source_metadata_ids;
+    DorisVector<StringRef> source_values(rows.size());
+    size_t total_value_bytes = 0;
+    for (size_t row = 0; row < rows.size(); ++row) {
+        const VariantRef value = rows[row];
+        if (value.metadata.data == nullptr && value.metadata.size != 0) {
+            throw Exception(ErrorCode::CORRUPTION,
+                            "Variant encoded metadata has a null data pointer 
for {} bytes",
+                            value.metadata.size);
+        }
+        if (value.value.data == nullptr && value.value.size != 0) {
+            throw Exception(ErrorCode::CORRUPTION,
+                            "Variant encoded value has a null data pointer for 
{} bytes",
+                            value.value.size);
+        }
 
-    if (data.meta_ids.empty()) {
-        const VariantMetadataRef metadata = metadata_at(0);
-        const uint32_t id = _find_or_insert_metadata({metadata.data, 
metadata.size});
-        values.insert_many_continuous_binary_data(data.value_bytes.data, 
data.value_offsets.data(),
-                                                  rows);
-        metadata_ids.insert_many_vals(id, rows);
+        const std::string_view metadata_key(
+                value.metadata.data == nullptr ? "" : value.metadata.data, 
value.metadata.size);
+        uint32_t source_metadata_id = 0;
+        if (unique_metadatas.empty()) {
+            validate_variant_metadata(value.metadata);
+            unique_metadatas.push_back(value.metadata);
+        } else if (unique_metadatas.size() == 1 && 
metadata_ids_by_value.empty() &&
+                   StringRef(unique_metadatas.front().data, 
unique_metadatas.front().size) ==
+                           StringRef(value.metadata.data, 
value.metadata.size)) {
+            // Iceberg files normally share one metadata dictionary across a 
batch. Avoid a hash
+            // table and per-row ids until a second distinct dictionary is 
actually observed.
+        } else {
+            if (metadata_ids_by_value.empty()) {
+                const VariantMetadataRef first = unique_metadatas.front();
+                metadata_ids_by_value.emplace(
+                        std::string_view(first.data == nullptr ? "" : 
first.data, first.size), 0);
+                source_metadata_ids.resize(rows.size());
+            }
+            auto metadata_id = metadata_ids_by_value.find(metadata_key);
+            if (metadata_id != metadata_ids_by_value.end()) {
+                source_metadata_id = metadata_id->second;
+            } else {
+                if (unique_metadatas.size() == 
std::numeric_limits<uint32_t>::max()) {
+                    throw Exception(
+                            ErrorCode::INVALID_ARGUMENT,
+                            "Variant encoded metadata dictionary exceeds the 
uint32 id limit");
+                }
+                validate_variant_metadata(value.metadata);
+                source_metadata_id = 
static_cast<uint32_t>(unique_metadatas.size());
+                unique_metadatas.push_back(value.metadata);
+                metadata_ids_by_value.emplace(metadata_key, 
source_metadata_id);
+            }
+        }
+        if (!source_metadata_ids.empty()) {
+            source_metadata_ids[row] = source_metadata_id;
+        }
+        validate_variant_payload(value);
+        source_values[row] = value.value;
+        if (value.value.size > std::numeric_limits<size_t>::max() - 
total_value_bytes) {
+            throw Exception(ErrorCode::INVALID_ARGUMENT,
+                            "Variant encoded value bytes exceed the size_t 
limit");
+        }
+        total_value_bytes += value.value.size;
+    }
+
+    // Validate the complete input before changing a typed/shredded 
destination. Failed and empty
+    // appends must preserve its representation just like the EncodedDataView 
overload does.
+    if (_impl->initialized) {
+        DORIS_CHECK(column._typed == nullptr && column._shredded == nullptr)
+                << "ColumnVariantV2 changed outside its encoded-row appender";
+        DORIS_CHECK_EQ(_impl->expected_rows, column.size())
+                << "ColumnVariantV2 changed outside its encoded-row appender";
+        const auto& current_metadatas = *static_cast<const 
ColumnString::Ptr&>(column._metadatas);
+        DORIS_CHECK_EQ(_impl->metadata_hash_next.size(), 
current_metadatas.size())
+                << "ColumnVariantV2 metadata changed outside its encoded-row 
appender";
+    }
+
+    auto aliases_encoded_storage = [&](const ColumnVariantV2& owner) {
+        DORIS_CHECK(owner._typed == nullptr && owner._shredded == nullptr)
+                << "encoded-row owner must have encoded storage";
+        const auto& metadatas = *owner._metadatas;
+        const auto& values = *owner._values;
+        const StringRef metadata_storage(metadatas.get_chars().data(),
+                                         metadatas.get_chars().size());
+        const StringRef value_storage(values.get_chars().data(), 
values.get_chars().size());
+        for (const VariantRef row : rows) {
+            if (byte_ranges_overlap({row.metadata.data, row.metadata.size}, 
metadata_storage) ||
+                byte_ranges_overlap(row.value, value_storage)) {
+                return true;
+            }
+        }
+        return false;
+    };
+    const ColumnVariantV2* encoded_owner = nullptr;
+    if (column._shredded) {
+        // get_value_ref() on a shredded destination borrows from this cached 
materialization.
+        // Check it before ensure_encoded() drops the state that owns those 
bytes.
+        encoded_owner = &column._shredded->materialized_column();
+    } else if (!column._typed) {
+        encoded_owner = &column;
+    }
+    if (encoded_owner != nullptr && aliases_encoded_storage(*encoded_owner)) {
+        throw Exception(ErrorCode::INVALID_ARGUMENT,
+                        "Variant encoded-row input must not alias its 
destination; use "
+                        "insert_range_from() for same-column rows");
+    }
+
+    if (column._typed || column._shredded) {
+        column.ensure_encoded();
+    }
+    DORIS_CHECK(column._typed_type == nullptr) << "encoded state cannot retain 
a typed data type";
+    require_exclusive(column._meta_ids, "metadata ids");
+    require_exclusive(column._values, "values");
+    auto& values = *column._values;
+    auto& metadata_ids = *column._meta_ids;
+
+    auto current_metadatas = [&]() -> const ColumnString& {
+        return *static_cast<const ColumnString::Ptr&>(column._metadatas);
+    };
+    if (!_impl->initialized) {
+        const auto& metadatas = current_metadatas();
+        _impl->metadata_hash_heads.reserve(metadatas.size());
+        _impl->metadata_hash_next.reserve(metadatas.size());
+        for (size_t index = 0; index < metadatas.size(); ++index) {
+            const auto id = static_cast<uint32_t>(index);
+            const StringRef metadata = metadatas.get_data_at(id);
+            const size_t hash = StringRefHash {}(metadata);
+            const auto existing = _impl->metadata_hash_heads.find(hash);
+            const uint32_t next = existing == _impl->metadata_hash_heads.end()
+                                          ? UNMAPPED_METADATA_ID
+                                          : existing->second;
+            _impl->metadata_hash_next.push_back(next);
+            _impl->metadata_hash_heads[hash] = id;
+        }
+        _impl->expected_rows = column.size();
+        _impl->initialized = true;
+    }
+    reserve_rows(values, metadata_ids, total_value_bytes, rows.size());
+    auto find_or_insert_metadata = [&](VariantMetadataRef metadata) {
+        const StringRef metadata_bytes(metadata.data, metadata.size);
+        const size_t hash = StringRefHash {}(metadata_bytes);
+        auto existing = _impl->metadata_hash_heads.find(hash);
+        uint32_t id = existing == _impl->metadata_hash_heads.end() ? 
UNMAPPED_METADATA_ID
+                                                                   : 
existing->second;
+        while (id != UNMAPPED_METADATA_ID) {
+#ifdef BE_TEST
+            ++_impl->metadata_comparisons;
+#endif
+            if (current_metadatas().get_data_at(id) == metadata_bytes) {
+                return id;
+            }
+            id = _impl->metadata_hash_next[id];
+        }
+
+        const uint32_t new_id = column._append_metadata(metadata_bytes);
+        const uint32_t next = existing == _impl->metadata_hash_heads.end() ? 
UNMAPPED_METADATA_ID
+                                                                           : 
existing->second;
+        _impl->metadata_hash_next.push_back(next);
+        if (existing == _impl->metadata_hash_heads.end()) {
+            _impl->metadata_hash_heads.emplace(hash, new_id);
+        } else {
+            existing->second = new_id;
+        }
+        return new_id;
+    };
+
+    if (unique_metadatas.size() == 1) {
+        const uint32_t id = find_or_insert_metadata(unique_metadatas.front());
+        values.insert_many_strings_without_reserve(source_values.data(), 
source_values.size());
+        metadata_ids.insert_many_vals(id, rows.size());
     } else {
-        DorisVector<uint32_t> remap(metadata_count, UNMAPPED_METADATA_ID);
+        DorisVector<uint32_t> 
destination_metadata_ids(unique_metadatas.size());
+        for (size_t id = 0; id < unique_metadatas.size(); ++id) {
+            destination_metadata_ids[id] = 
find_or_insert_metadata(unique_metadatas[id]);
+        }
+        values.insert_many_strings_without_reserve(source_values.data(), 
source_values.size());
         auto& destination_ids = metadata_ids.get_data();
-        for (size_t row = 0; row < rows; ++row) {
-            const uint32_t source_id = data.meta_ids[row];
-            DCHECK_LT(source_id, metadata_count);
-            const VariantMetadataRef metadata = metadata_at(source_id);
-            if (remap[source_id] == UNMAPPED_METADATA_ID) {
-                remap[source_id] = _find_or_insert_metadata({metadata.data, 
metadata.size});
-            }
-            destination_ids.push_back(remap[source_id]);
+        for (size_t row = 0; row < rows.size(); ++row) {
+            
destination_ids.push_back(destination_metadata_ids[source_metadata_ids[row]]);
         }
-        values.insert_many_continuous_binary_data(data.value_bytes.data, 
data.value_offsets.data(),
-                                                  rows);
     }
 
-    DCHECK_EQ(_meta_ids->size(), _values->size());
-    _check_invariants();
+    _impl->expected_rows = column.size();
+    DCHECK_EQ(column._meta_ids->size(), column._values->size());
+    column._check_invariants();
+}
+
+ColumnVariantV2::EncodedRowsAppender 
ColumnVariantV2::create_encoded_rows_appender() {
+    return EncodedRowsAppender(*this);
 }
 
 void ColumnVariantV2::insert_encoded_batch(const VariantBatchBuilder& block) {
@@ -1028,9 +1240,37 @@ void ColumnVariantV2::insert_encoded_batch(const 
VariantBatchBuilder& block) {
 
     const VariantMetadataRef metadata = block.metadata_ref();
     const StringRef value_bytes = block.value_bytes();
+    DORIS_CHECK_LE(metadata.size, std::numeric_limits<uint32_t>::max())
+            << "VariantBatchBuilder metadata exceeds the ColumnString uint32 
byte limit";
     DORIS_CHECK_EQ(offsets.front(), 0);
     DORIS_CHECK_EQ(static_cast<size_t>(offsets.back()), value_bytes.size);
 
+    const std::array<uint32_t, 2> metadata_offsets {0, 
static_cast<uint32_t>(metadata.size)};
+    _append_prevalidated_encoded_data({.metadata_bytes = {metadata.data, 
metadata.size},
+                                       .metadata_offsets = metadata_offsets,
+                                       .meta_ids = {},
+                                       .value_bytes = value_bytes,
+                                       .value_offsets = offsets});
+}
+
+void ColumnVariantV2::_append_prevalidated_encoded_data(const EncodedDataView& 
data) {
+    DCHECK(!data.metadata_offsets.empty());
+    DCHECK(!data.value_offsets.empty());
+    const size_t metadata_count = data.metadata_offsets.size() - 1;
+    const size_t rows = data.value_offsets.size() - 1;
+    if (rows == 0) {
+        return;
+    }
+
+    DCHECK_NE(metadata_count, 0);
+    DCHECK(data.meta_ids.empty() || data.meta_ids.size() == rows);
+    auto metadata_at = [&](uint32_t id) {
+        DCHECK_LT(id, metadata_count);
+        const uint32_t begin = data.metadata_offsets[id];
+        const uint32_t end = data.metadata_offsets[id + 1];
+        return StringRef(data.metadata_bytes.data + begin, end - begin);
+    };
+
     if (_typed || _shredded) {
         // Preserve the no-op behavior above: an empty encoded append must not 
materialize the
         // destination's lazy Variant representation.
@@ -1041,11 +1281,28 @@ void ColumnVariantV2::insert_encoded_batch(const 
VariantBatchBuilder& block) {
     require_exclusive(_values, "values");
     auto& values = *_values;
     auto& metadata_ids = *_meta_ids;
-    reserve_rows(values, metadata_ids, value_bytes.size, rows);
+    reserve_rows(values, metadata_ids, data.value_bytes.size, rows);
 
-    const uint32_t id = _find_or_insert_metadata({metadata.data, 
metadata.size});
-    values.insert_many_continuous_binary_data(value_bytes.data, 
offsets.data(), rows);
-    metadata_ids.insert_many_vals(id, rows);
+    if (data.meta_ids.empty()) {
+        DCHECK_EQ(metadata_count, 1);
+        const uint32_t id = _find_or_insert_metadata(metadata_at(0));
+        values.insert_many_continuous_binary_data(data.value_bytes.data, 
data.value_offsets.data(),
+                                                  rows);
+        metadata_ids.insert_many_vals(id, rows);
+    } else {
+        DorisVector<uint32_t> remap(metadata_count, UNMAPPED_METADATA_ID);
+        auto& destination_ids = metadata_ids.get_data();
+        for (size_t row = 0; row < rows; ++row) {
+            const uint32_t source_id = data.meta_ids[row];
+            DCHECK_LT(source_id, metadata_count);
+            if (remap[source_id] == UNMAPPED_METADATA_ID) {
+                remap[source_id] = 
_find_or_insert_metadata(metadata_at(source_id));
+            }
+            destination_ids.push_back(remap[source_id]);
+        }
+        values.insert_many_continuous_binary_data(data.value_bytes.data, 
data.value_offsets.data(),
+                                                  rows);
+    }
 
     DCHECK_EQ(_meta_ids->size(), _values->size());
     _check_invariants();
@@ -2180,6 +2437,14 @@ uint32_t 
ColumnVariantV2::_find_or_insert_metadata(StringRef metadata) {
             return id;
         }
     }
+
+    return _append_metadata(metadata);
+}
+
+uint32_t ColumnVariantV2::_append_metadata(StringRef metadata) {
+    DORIS_CHECK(metadata.data != nullptr || metadata.size == 0)
+            << "metadata bytes have a null pointer";
+    const auto& current_metadatas = *static_cast<const 
ColumnString::Ptr&>(_metadatas);
     if (current_metadatas.size() == std::numeric_limits<uint32_t>::max()) {
         throw Exception(ErrorCode::INVALID_ARGUMENT,
                         "ColumnVariantV2 metadata dictionary exceeds the 
uint32 id limit");
diff --git a/be/src/core/column/variant_v2/column_variant_v2.h 
b/be/src/core/column/variant_v2/column_variant_v2.h
index 351b6b28a50..976a4a66e31 100644
--- a/be/src/core/column/variant_v2/column_variant_v2.h
+++ b/be/src/core/column/variant_v2/column_variant_v2.h
@@ -119,6 +119,34 @@ public:
         std::span<const uint32_t> value_offsets;
     };
 
+    // Scoped adapter for repeated encoded-row appends. It builds one hash 
index over destination
+    // metadata ids and reuses it across calls, while the destination column 
continues to own all
+    // copied bytes. Input rows must not borrow metadata or value bytes from 
the destination,
+    // including its shredded materialization; use insert_range_from() for 
that case. Do not mutate
+    // or destroy the destination while this adapter is alive.
+    class EncodedRowsAppender {
+    public:
+        ~EncodedRowsAppender();
+
+        EncodedRowsAppender(const EncodedRowsAppender&) = delete;
+        EncodedRowsAppender& operator=(const EncodedRowsAppender&) = delete;
+        EncodedRowsAppender(EncodedRowsAppender&&) noexcept;
+        EncodedRowsAppender& operator=(EncodedRowsAppender&&) = delete;
+
+        void append(std::span<const VariantRef> rows);
+
+#ifdef BE_TEST
+        size_t metadata_comparisons_for_test() const noexcept;
+#endif
+
+    private:
+        friend class ColumnVariantV2;
+        explicit EncodedRowsAppender(ColumnVariantV2& column);
+
+        struct Impl;
+        std::unique_ptr<Impl> _impl;
+    };
+
     // Borrowed immutable adapter for whole-column E/T readers. The source 
column owns every
     // referenced column, type, and byte; any structural mutation invalidates 
this view. Encoded
     // bytes have already been validated at their insertion or deserialization 
boundary.
@@ -204,6 +232,10 @@ public:
     // metadata blob. Input buffers must not alias this column; use 
insert_range_from for that case.
     void insert_encoded_rows(const EncodedDataView& data);
 
+    // Reuse one appender when encoded rows arrive in bounded batches so 
destination metadata is
+    // indexed once for the full import rather than scanned again for every 
batch.
+    EncodedRowsAppender create_encoded_rows_appender();
+
     // Direct trusted codec adapter. VariantBatchBuilder already produces 
canonical metadata,
     // validated values, and ColumnString-compatible uint32 offsets, so this 
path copies its buffers
     // without validating the encoded tree a second time.
@@ -282,6 +314,8 @@ private:
     ColumnVariantV2();
     ColumnVariantV2(const ColumnVariantV2& other);
 
+    void _append_prevalidated_encoded_data(const EncodedDataView& data);
+    uint32_t _append_metadata(StringRef metadata);
     uint32_t _find_or_insert_metadata(StringRef metadata);
     void _replace_shredded_state_with(const ColumnVariantV2& replacement);
     void _ensure_serialized();
diff --git a/be/src/format_v2/parquet/parquet_profile.cpp 
b/be/src/format_v2/parquet/parquet_profile.cpp
index 8da28ac7d19..d4fff78e504 100644
--- a/be/src/format_v2/parquet/parquet_profile.cpp
+++ b/be/src/format_v2/parquet/parquet_profile.cpp
@@ -107,6 +107,12 @@ void ParquetProfile::init(RuntimeProfile* profile) {
                                                          TUnit::TIME_NS, 
parquet_profile);
     variant_reconstructed_rows = add_persistent_counter(profile, 
"VariantReconstructedRows",
                                                         TUnit::UNIT, 
parquet_profile);
+    variant_unshredded_direct_import_time = add_persistent_counter(
+            profile, "VariantUnshreddedDirectImportTime", TUnit::TIME_NS, 
parquet_profile);
+    variant_unshredded_direct_import_rows = add_persistent_counter(
+            profile, "VariantUnshreddedDirectImportRows", TUnit::UNIT, 
parquet_profile);
+    variant_unshredded_direct_import_bytes = add_persistent_counter(
+            profile, "VariantUnshreddedDirectImportBytes", TUnit::BYTES, 
parquet_profile);
     variant_unshredded_direct_seek_time = add_persistent_counter(
             profile, "VariantUnshreddedDirectSeekTime", TUnit::TIME_NS, 
parquet_profile);
     variant_unshredded_direct_seek_rows = add_persistent_counter(
@@ -355,6 +361,9 @@ ParquetColumnReaderProfile 
ParquetProfile::column_reader_profile() const {
             .materialization_time = materialization_time,
             .variant_reconstruction_time = variant_reconstruction_time,
             .variant_reconstructed_rows = variant_reconstructed_rows,
+            .variant_unshredded_direct_import_time = 
variant_unshredded_direct_import_time,
+            .variant_unshredded_direct_import_rows = 
variant_unshredded_direct_import_rows,
+            .variant_unshredded_direct_import_bytes = 
variant_unshredded_direct_import_bytes,
             .variant_unshredded_direct_seek_time = 
variant_unshredded_direct_seek_time,
             .variant_unshredded_direct_seek_rows = 
variant_unshredded_direct_seek_rows,
             .variant_unshredded_direct_seek_bytes = 
variant_unshredded_direct_seek_bytes,
diff --git a/be/src/format_v2/parquet/parquet_profile.h 
b/be/src/format_v2/parquet/parquet_profile.h
index 7ec112e0a55..4a0ef53e9bc 100644
--- a/be/src/format_v2/parquet/parquet_profile.h
+++ b/be/src/format_v2/parquet/parquet_profile.h
@@ -42,6 +42,11 @@ struct ParquetColumnReaderProfile {
     RuntimeProfile::Counter* materialization_time = nullptr; // value 
materialization time (ns)
     std::shared_ptr<RuntimeProfile::Counter> variant_reconstruction_time;
     std::shared_ptr<RuntimeProfile::Counter> variant_reconstructed_rows;
+    // Complete unshredded roots imported without rebuilding their Variant 
value tree. Bytes count
+    // the per-row metadata and value references presented to the direct 
importer.
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_time;
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_rows;
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_bytes;
     // Pure metadata+value roots can seek a requested path before constructing 
a root column.
     std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_seek_time;
     std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_seek_rows;
@@ -184,6 +189,10 @@ struct ParquetProfile {
     RuntimeProfile::Counter* materialization_time = nullptr;
     std::shared_ptr<RuntimeProfile::Counter> variant_reconstruction_time;
     std::shared_ptr<RuntimeProfile::Counter> variant_reconstructed_rows;
+    // These counters are persistent because direct import can happen after 
scanner teardown.
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_time;
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_rows;
+    std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_import_bytes;
     std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_seek_time;
     std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_seek_rows;
     std::shared_ptr<RuntimeProfile::Counter> 
variant_unshredded_direct_seek_bytes;
diff --git a/be/src/format_v2/parquet/reader/variant_column_reader.cpp 
b/be/src/format_v2/parquet/reader/variant_column_reader.cpp
index cf8912735d5..7a7d926b1a5 100644
--- a/be/src/format_v2/parquet/reader/variant_column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/variant_column_reader.cpp
@@ -479,9 +479,60 @@ std::optional<std::pair<size_t, size_t>> 
unshredded_child_indices(
     return std::pair {metadata_index, value_index};
 }
 
-ColumnVariantV2::MutablePtr encode_variant_column(const ParquetColumnSchema& 
schema,
-                                                  const IColumn& physical,
-                                                  bool require_metadata = 
true) {
+void import_unshredded_variant_range(const ParquetColumnSchema& schema, const 
ColumnStruct& wrapper,
+                                     const ColumnNullable* outer_nullable, 
size_t metadata_index,
+                                     size_t value_index, size_t begin, size_t 
end,
+                                     DorisVector<VariantRef>& encoded_rows,
+                                     ColumnVariantV2::EncodedRowsAppender& 
appender,
+                                     int64_t* imported_bytes) {
+    encoded_rows.clear();
+    if (encoded_rows.capacity() < end - begin) {
+        encoded_rows.reserve(end - begin);
+    }
+    if (imported_bytes != nullptr) {
+        *imported_bytes = 0;
+    }
+    auto count_imported_bytes = [&](size_t bytes) {
+        if (imported_bytes == nullptr) {
+            return;
+        }
+        DORIS_CHECK_LE(bytes,
+                       static_cast<size_t>(std::numeric_limits<int64_t>::max() 
- *imported_bytes));
+        *imported_bytes += static_cast<int64_t>(bytes);
+    };
+    for (size_t row = begin; row < end; ++row) {
+        if (outer_nullable != nullptr && 
outer_nullable->get_null_map_data()[row] != 0) {
+            encoded_rows.push_back(
+                    {.metadata = {.data = VARIANT_EMPTY_METADATA.data(),
+                                  .size = VARIANT_EMPTY_METADATA.size()},
+                     .value = {VARIANT_NULL_VALUE.data(), 
VARIANT_NULL_VALUE.size()}});
+            count_imported_bytes(VARIANT_EMPTY_METADATA.size());
+            count_imported_bytes(VARIANT_NULL_VALUE.size());
+            continue;
+        }
+
+        const Cell metadata = cell_at(wrapper.get_column(metadata_index), row);
+        if (metadata.is_null) {
+            throw Exception(ErrorCode::CORRUPTION, "Parquet Variant {} has 
null metadata at row {}",
+                            schema.name, row);
+        }
+        const StringRef metadata_bytes = metadata.column->get_data_at(row);
+        const Cell value = cell_at(wrapper.get_column(value_index), row);
+        const StringRef value_bytes =
+                value.is_null ? StringRef(VARIANT_NULL_VALUE.data(), 
VARIANT_NULL_VALUE.size())
+                              : value.column->get_data_at(row);
+        encoded_rows.push_back(
+                {.metadata = {.data = metadata_bytes.data, .size = 
metadata_bytes.size},
+                 .value = value_bytes});
+        count_imported_bytes(metadata_bytes.size);
+        count_imported_bytes(value_bytes.size);
+    }
+    appender.append(std::span<const VariantRef>(encoded_rows));
+}
+
+ColumnVariantV2::MutablePtr encode_variant_column(
+        const ParquetColumnSchema& schema, const IColumn& physical, bool 
require_metadata = true,
+        const ParquetColumnReaderProfile* profile = nullptr) {
     if (schema.kind != ParquetColumnSchemaKind::VARIANT) {
         throw Exception(ErrorCode::INVALID_ARGUMENT, "Parquet column {} is not 
Variant",
                         schema.name);
@@ -496,11 +547,50 @@ ColumnVariantV2::MutablePtr encode_variant_column(const 
ParquetColumnSchema& sch
     }
 
     auto variants = ColumnVariantV2::create();
-    constexpr size_t MAX_RECONSTRUCTION_BATCH_ROWS = 4096;
-    for (size_t begin = 0; begin < physical.size(); begin += 
MAX_RECONSTRUCTION_BATCH_ROWS) {
-        encode_variant_range(schema, wrapper, outer_nullable, begin,
-                             std::min(physical.size(), begin + 
MAX_RECONSTRUCTION_BATCH_ROWS),
-                             require_metadata, *variants);
+    // A complete unshredded root already contains exact Variant Encoding V1 
bytes. Keep the
+    // existing lazy materialization boundary, but validate and copy those 
bytes without rebuilding
+    // the value tree through VariantBatchBuilder.
+    const auto unshredded_indices =
+            require_metadata ? unshredded_child_indices(schema) : std::nullopt;
+    constexpr size_t MAX_MATERIALIZATION_BATCH_ROWS = 4096;
+    DorisVector<VariantRef> encoded_rows;
+    std::optional<ColumnVariantV2::EncodedRowsAppender> encoded_rows_appender;
+    if (unshredded_indices.has_value()) {
+        encoded_rows.reserve(std::min(physical.size(), 
MAX_MATERIALIZATION_BATCH_ROWS));
+        
encoded_rows_appender.emplace(variants->create_encoded_rows_appender());
+    }
+    for (size_t begin = 0; begin < physical.size(); begin += 
MAX_MATERIALIZATION_BATCH_ROWS) {
+        const size_t end = std::min(physical.size(), begin + 
MAX_MATERIALIZATION_BATCH_ROWS);
+        if (unshredded_indices.has_value()) {
+            DORIS_CHECK(encoded_rows_appender.has_value());
+            int64_t imported_bytes = 0;
+            RuntimeProfile::Counter* direct_import_time =
+                    profile == nullptr ? nullptr
+                                       : 
profile->variant_unshredded_direct_import_time.get();
+            {
+                // Update the shared timer on every exit, including validation 
failures. Rows and
+                // bytes are published only after the complete chunk has been 
appended.
+                SCOPED_TIMER(direct_import_time);
+                import_unshredded_variant_range(
+                        schema, structure, outer_nullable, 
unshredded_indices->first,
+                        unshredded_indices->second, begin, end, encoded_rows,
+                        *encoded_rows_appender, profile == nullptr ? nullptr : 
&imported_bytes);
+            }
+            if (profile != nullptr) {
+                const auto imported_rows = static_cast<int64_t>(end - begin);
+                if (profile->variant_unshredded_direct_import_rows != nullptr) 
{
+                    
COUNTER_UPDATE(profile->variant_unshredded_direct_import_rows.get(),
+                                   imported_rows);
+                }
+                if (profile->variant_unshredded_direct_import_bytes != 
nullptr) {
+                    
COUNTER_UPDATE(profile->variant_unshredded_direct_import_bytes.get(),
+                                   imported_bytes);
+                }
+            }
+        } else {
+            encode_variant_range(schema, wrapper, outer_nullable, begin, end, 
require_metadata,
+                                 *variants);
+        }
     }
     return variants;
 }
@@ -1041,27 +1131,6 @@ UnshreddedPathScan scan_unshredded_path(const 
ParquetColumnSchema& schema, const
     return std::move(typed_values).finish(std::move(path_cache));
 }
 
-void append_variant_rows(ColumnVariantV2& output, std::span<const VariantRef> 
rows) {
-    try {
-        VariantBatchBuilder builder(VariantBatchBuilder::ReserveHint {.rows = 
rows.size()});
-        for (const VariantRef value : rows) {
-            auto row = builder.begin_row();
-            row.add_value(value);
-            row.finish();
-        }
-        output.insert_encoded_batch(builder.finish_batch());
-    } catch (...) {
-        if (rows.size() <= 1) {
-            throw;
-        }
-        // A builder has one metadata dictionary. Split heterogeneous batches 
while preserving the
-        // original validation error for a malformed single row.
-        const size_t middle = rows.size() / 2;
-        append_variant_rows(output, rows.first(middle));
-        append_variant_rows(output, rows.subspan(middle));
-    }
-}
-
 ColumnPtr normalize_unshredded_path(const ParquetColumnSchema& schema, const 
IColumn& physical,
                                     std::pair<size_t, size_t> child_indices,
                                     std::span<const 
VariantShreddedPathSegment> path,
@@ -1074,6 +1143,7 @@ ColumnPtr normalize_unshredded_path(const 
ParquetColumnSchema& schema, const ICo
     DORIS_CHECK_EQ(structure.tuple_size(), schema.children.size());
 
     auto values = ColumnVariantV2::create();
+    auto appender = values->create_encoded_rows_appender();
     auto nulls = ColumnUInt8::create();
     nulls->reserve(physical.size());
     constexpr size_t MAX_DIRECT_SEEK_BATCH_ROWS = 4096;
@@ -1126,7 +1196,7 @@ ColumnPtr normalize_unshredded_path(const 
ParquetColumnSchema& schema, const ICo
                 count_copied_bytes(null_value);
             }
         }
-        append_variant_rows(*values, encoded_rows);
+        appender.append(std::span<const VariantRef>(encoded_rows));
     }
     *copied_bytes = total_copied_bytes;
     return ColumnNullable::create(std::move(values), std::move(nulls));
@@ -1503,7 +1573,7 @@ public:
         }
         if (!_materialized) {
             SCOPED_TIMER(_profile.variant_reconstruction_time.get());
-            _materialized = encode_variant_column(*_schema, *_physical);
+            _materialized = encode_variant_column(*_schema, *_physical, true, 
&_profile);
             update_counter(_profile.variant_reconstructed_rows,
                            static_cast<int64_t>(_physical->size()));
         }
diff --git a/be/test/core/column/column_variant_v2_test.cpp 
b/be/test/core/column/column_variant_v2_test.cpp
index 90eae92e41c..aa645e471ad 100644
--- a/be/test/core/column/column_variant_v2_test.cpp
+++ b/be/test/core/column/column_variant_v2_test.cpp
@@ -104,6 +104,18 @@ std::string empty_metadata_bytes() {
     return metadata;
 }
 
+std::string single_key_metadata_bytes(std::string_view key) {
+    EXPECT_LE(key.size(), std::numeric_limits<uint8_t>::max());
+    std::string metadata;
+    metadata.push_back(
+            static_cast<char>(VARIANT_ENCODING_VERSION | 
VARIANT_METADATA_SORTED_STRINGS_MASK));
+    metadata.push_back(1);
+    metadata.push_back(0);
+    metadata.push_back(static_cast<char>(key.size()));
+    metadata.append(key);
+    return metadata;
+}
+
 VariantField encoded_field(std::string metadata, std::string value) {
     std::string field;
     append_unsigned(field, metadata.size(), sizeof(uint32_t));
@@ -227,6 +239,51 @@ struct OwnedEncodedData {
     }
 };
 
+class MaterializableShreddedState final : public VariantShreddedState {
+public:
+    explicit MaterializableShreddedState(ColumnPtr materialized)
+            : _materialized(std::move(materialized)) {}
+
+    size_t size() const override { return _materialized->size(); }
+    size_t byte_size() const override { return _materialized->byte_size(); }
+    size_t allocated_bytes() const override { return 
_materialized->allocated_bytes(); }
+    void sanity_check() const override { _materialized->sanity_check(); }
+    void for_each_subcolumn(IColumn::ColumnCallback callback) const override {
+        callback(*_materialized);
+    }
+    std::shared_ptr<VariantShreddedState> filter(const IColumn::Filter& filter,
+                                                 ssize_t result_size_hint) 
const override {
+        return std::make_shared<MaterializableShreddedState>(
+                _materialized->filter(filter, result_size_hint));
+    }
+    std::shared_ptr<VariantShreddedState> select_range(size_t start, size_t 
length) const override {
+        return 
std::make_shared<MaterializableShreddedState>(_materialized->cut(start, 
length));
+    }
+    std::shared_ptr<VariantShreddedState> select_indices(
+            const uint32_t* indices_begin, const uint32_t* indices_end) const 
override {
+        MutableColumnPtr selected = _materialized->clone_empty();
+        selected->insert_indices_from(*_materialized, indices_begin, 
indices_end);
+        return 
std::make_shared<MaterializableShreddedState>(std::move(selected));
+    }
+    bool can_materialize() const override { return true; }
+    bool try_append(const VariantShreddedState&) override { return false; }
+    std::optional<VariantShreddedTypedValue> find_typed_value(
+            std::span<const VariantShreddedPathSegment>) const override {
+        return std::nullopt;
+    }
+    std::optional<ColumnPtr> find_normalized_value(
+            std::span<const VariantShreddedPathSegment>) const override {
+        return std::nullopt;
+    }
+    const ColumnVariantV2& materialized_column() const override {
+        return assert_cast<const ColumnVariantV2&>(*_materialized);
+    }
+    const ColumnVariantV2& serialized_column() const override { return 
materialized_column(); }
+
+private:
+    ColumnPtr _materialized;
+};
+
 OwnedEncodedData late_invalid_encoded_rows() {
     const VariantField first = encode_json("1");
     const VariantField second = encode_json("2");
@@ -1067,6 +1124,108 @@ TEST(ColumnVariantV2Test, 
InvalidEncodedRowsDoNotChangeTypedState) {
     }
 }
 
+TEST(ColumnVariantV2Test, InvalidEncodedRowsAppenderDoesNotChangeTypedState) {
+    constexpr std::array<int32_t, 1> TYPED_VALUES {7};
+    constexpr std::array<uint8_t, 1> TYPED_NULLS {0};
+    const VariantField first = encode_json("1");
+    const VariantField second = encode_json("2");
+    std::string invalid_value(second.ref().value.data, 
second.ref().value.size);
+    invalid_value.push_back('\0');
+    const std::array<VariantRef, 2> invalid_rows {
+            first.ref(), VariantRef {.metadata = second.ref().metadata,
+                                     .value = {invalid_value.data(), 
invalid_value.size()}}};
+
+    auto typed = typed_int32(TYPED_VALUES, TYPED_NULLS);
+    const IColumn* typed_storage = subcolumns(*typed).front().get();
+    auto appender = typed->create_encoded_rows_appender();
+    EXPECT_THROW(appender.append(invalid_rows), Exception);
+    ASSERT_TRUE(typed->is_typed());
+    EXPECT_EQ(subcolumns(*typed).front().get(), typed_storage);
+    expect_int32_rows(*typed, TYPED_VALUES, TYPED_NULLS);
+    typed->sanity_check();
+}
+
+TEST(ColumnVariantV2Test, EncodedRowsAppenderRejectsSameEncodedColumnBorrow) {
+    auto column = ColumnVariantV2::create();
+    insert_encoded_field(*column, 
encode_json(R"({"old":"value-that-must-remain-owned"})"));
+    const std::string expected = json_at(*column, 0);
+    const VariantRef borrowed = column->get_value_ref(0);
+    // Repeating the borrowed value makes the requested append exceed the 
existing value capacity,
+    // covering the reserve-induced invalidation that this API must reject 
before mutation.
+    const std::vector<VariantRef> borrowed_rows(4096, borrowed);
+
+    auto appender = column->create_encoded_rows_appender();
+    try {
+        appender.append(borrowed_rows);
+        FAIL() << "expected same-column encoded rows to be rejected";
+    } catch (const Exception& exception) {
+        EXPECT_EQ(exception.code(), ErrorCode::INVALID_ARGUMENT);
+        EXPECT_NE(exception.message().find("must not alias"), 
std::string::npos)
+                << exception.message();
+    }
+
+    ASSERT_EQ(column->size(), 1);
+    EXPECT_EQ(json_at(*column, 0), expected);
+    column->sanity_check();
+}
+
+TEST(ColumnVariantV2Test, EncodedRowsAppenderRejectsSameShreddedColumnBorrow) {
+    auto materialized = ColumnVariantV2::create();
+    insert_encoded_field(*materialized, encode_json(R"({"old":7})"));
+    auto column = ColumnVariantV2::create_shredded(
+            
std::make_shared<MaterializableShreddedState>(std::move(materialized)));
+    const VariantRef borrowed = column->get_value_ref(0);
+
+    auto appender = column->create_encoded_rows_appender();
+    try {
+        appender.append(std::span<const VariantRef>(&borrowed, 1));
+        FAIL() << "expected same-column shredded rows to be rejected";
+    } catch (const Exception& exception) {
+        EXPECT_EQ(exception.code(), ErrorCode::INVALID_ARGUMENT);
+        EXPECT_NE(exception.message().find("must not alias"), 
std::string::npos)
+                << exception.message();
+    }
+
+    EXPECT_TRUE(column->is_shredded());
+    ASSERT_EQ(column->size(), 1);
+    EXPECT_EQ(json_at(*column, 0), R"({"old":7})");
+    column->sanity_check();
+}
+
+TEST(ColumnVariantV2Test, 
EncodedRowsAppenderInternsHighCardinalityMetadataAcrossChunks) {
+    constexpr size_t ROWS = 4097;
+    std::vector<std::string> metadata_storage;
+    metadata_storage.reserve(ROWS);
+    for (size_t row = 0; row < ROWS; ++row) {
+        metadata_storage.push_back(single_key_metadata_bytes("unused-" + 
std::to_string(row)));
+    }
+    const std::string value = integer_value_bytes(7, 1);
+    std::vector<VariantRef> encoded_rows;
+    encoded_rows.reserve(ROWS);
+    for (const std::string& metadata : metadata_storage) {
+        encoded_rows.push_back({.metadata = {.data = metadata.data(), .size = 
metadata.size()},
+                                .value = {value.data(), value.size()}});
+    }
+
+    auto column = ColumnVariantV2::create();
+    auto appender = column->create_encoded_rows_appender();
+    appender.append(std::span<const VariantRef>(encoded_rows.data(), 4096));
+    appender.append(std::span<const VariantRef>(encoded_rows.data() + 4096, 
1));
+
+    ASSERT_EQ(column->size(), ROWS);
+    ASSERT_EQ(metadata_count(*column), ROWS);
+    // A hash collision may require a byte comparison, but lookups must not 
scan the growing
+    // destination dictionary as the old O(U^2) implementation did.
+    EXPECT_LT(appender.metadata_comparisons_for_test(), ROWS * 4);
+    for (const size_t row : {size_t {0}, size_t {4095}, size_t {4096}}) {
+        const VariantRef imported = column->get_value_ref(row);
+        EXPECT_EQ(as_view({imported.metadata.data, imported.metadata.size}), 
metadata_storage[row]);
+        EXPECT_EQ(imported.value, StringRef(value));
+        EXPECT_EQ(imported.get_int(), 7);
+    }
+    column->sanity_check();
+}
+
 TEST(ColumnVariantV2Test, InvalidEncodedRowsDoNotPartiallyAppendEncodedState) {
     const OwnedEncodedData invalid = late_invalid_encoded_rows();
     for (const bool omit_single_metadata_ids : {false, true}) {
@@ -1118,6 +1277,21 @@ TEST(ColumnVariantV2Test, 
EmptyEncodedAppendsPreserveTypedState) {
     ASSERT_TRUE(batch_destination->is_typed());
     EXPECT_EQ(subcolumns(*batch_destination).front().get(), batch_storage);
     expect_int32_rows(*batch_destination, VALUES, NULLS);
+
+    auto borrowed_destination = typed_int32(VALUES, NULLS);
+    const IColumn* borrowed_storage = 
subcolumns(*borrowed_destination).front().get();
+    auto borrowed_appender = 
borrowed_destination->create_encoded_rows_appender();
+    borrowed_appender.append(std::span<const VariantRef> {});
+    ASSERT_TRUE(borrowed_destination->is_typed());
+    EXPECT_EQ(subcolumns(*borrowed_destination).front().get(), 
borrowed_storage);
+    expect_int32_rows(*borrowed_destination, VALUES, NULLS);
+
+    auto size_calls = std::make_shared<size_t>(0);
+    auto shredded_destination = ColumnVariantV2::create_shredded(
+            std::make_shared<CountingShreddedState>(1, size_calls));
+    auto shredded_appender = 
shredded_destination->create_encoded_rows_appender();
+    shredded_appender.append(std::span<const VariantRef> {});
+    EXPECT_TRUE(shredded_destination->is_shredded());
 }
 
 TEST(ColumnVariantV2Test, ReadViewBorrowsValidatedEncodedState) {
diff --git a/be/test/format_v2/parquet/variant_column_reader_test.cpp 
b/be/test/format_v2/parquet/variant_column_reader_test.cpp
index a27c9132be6..bec546affb8 100644
--- a/be/test/format_v2/parquet/variant_column_reader_test.cpp
+++ b/be/test/format_v2/parquet/variant_column_reader_test.cpp
@@ -24,6 +24,7 @@
 #include <functional>
 #include <initializer_list>
 #include <limits>
+#include <string>
 #include <string_view>
 #include <vector>
 
@@ -70,6 +71,18 @@ MutableColumnPtr nullable_strings(const 
std::vector<StringRef>& values,
     return ColumnNullable::create(std::move(data), std::move(null_map));
 }
 
+std::string single_key_metadata_bytes(std::string_view key) {
+    EXPECT_LE(key.size(), std::numeric_limits<uint8_t>::max());
+    std::string metadata;
+    metadata.push_back(
+            static_cast<char>(VARIANT_ENCODING_VERSION | 
VARIANT_METADATA_SORTED_STRINGS_MASK));
+    metadata.push_back(1);
+    metadata.push_back(0);
+    metadata.push_back(static_cast<char>(key.size()));
+    metadata.append(key);
+    return metadata;
+}
+
 Status extract_object_key(const ColumnVariantV2& source, const ColumnNullable& 
nullable,
                           StringRef key, ColumnPtr* result) {
     const std::array segments {VariantElementV2PathSegment::object_key(key)};
@@ -963,6 +976,106 @@ TEST(VariantColumnReaderTest, 
UnshreddedCachedMissingStillValidatesEveryObject)
     EXPECT_FALSE(result);
 }
 
+TEST(VariantColumnReaderTest, 
UnshreddedIntegerLeafKeepsCorruptMetadataOnFallback) {
+    VariantBatchBuilder builder;
+    auto row = builder.begin_row();
+    auto root = row.start_object();
+    root.add_key(StringRef("time_us"));
+    row.add_int(7);
+    root.add_key(StringRef("zz_unused"));
+    row.add_int(1);
+    root.finish();
+    row.finish();
+    VariantBatchBuilder batch = builder.finish_batch();
+    const VariantRef encoded = batch.value_at(0);
+
+    std::string invalid_metadata(encoded.metadata.data, encoded.metadata.size);
+    const size_t invalid_key = invalid_metadata.find("zz_unused");
+    ASSERT_NE(invalid_key, std::string::npos);
+    invalid_metadata[invalid_key] = static_cast<char>(0xff);
+
+    MutableColumns fields;
+    fields.push_back(nullable_strings({StringRef(invalid_metadata)}, {0}));
+    fields.push_back(nullable_strings({encoded.value}, {0}));
+    auto physical = root_wrapper(std::move(fields));
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    ASSERT_TRUE(materialize_variant_rows(unshredded_schema(), *physical, 
output).ok());
+    const auto& root_nullable = assert_cast<const ColumnNullable&>(*output);
+    const auto& root_variants =
+            assert_cast<const 
ColumnVariantV2&>(root_nullable.get_nested_column());
+    const std::array segments 
{VariantElementV2PathSegment::object_key(StringRef("time_us"))};
+    std::unique_ptr<ResolvedVariantElementV2Path> path;
+    ASSERT_TRUE(resolve_variant_element_v2_path(segments, &path).ok());
+    ColumnPtr result;
+    ASSERT_TRUE(extract_variant_element_v2(
+                        root_variants, *path,
+                        std::span<const 
uint8_t>(root_nullable.get_null_map_data().data(),
+                                                 
root_nullable.get_null_map_data().size()),
+                        &result)
+                        .ok());
+
+    const auto& result_variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*result).get_nested_column());
+    ASSERT_TRUE(result_variants.is_shredded());
+    EXPECT_THROW((void)result_variants.get_value_ref(0), Exception);
+}
+
+TEST(VariantColumnReaderTest, UnshreddedIntegerLeafValidatesLeadingJsonNull) {
+    VariantBatchBuilder invalid_builder;
+    auto invalid_row = invalid_builder.begin_row();
+    auto invalid_root = invalid_row.start_object();
+    invalid_root.add_key(StringRef("time_us"));
+    invalid_row.add_null();
+    invalid_root.add_key(StringRef("zz_unused"));
+    invalid_row.add_int(1);
+    invalid_root.finish();
+    invalid_row.finish();
+    VariantBatchBuilder invalid_batch = invalid_builder.finish_batch();
+    const VariantRef invalid = invalid_batch.value_at(0);
+
+    VariantBatchBuilder valid_builder;
+    auto valid_row = valid_builder.begin_row();
+    auto valid_root = valid_row.start_object();
+    valid_root.add_key(StringRef("time_us"));
+    valid_row.add_int(7);
+    valid_root.finish();
+    valid_row.finish();
+    VariantBatchBuilder valid_batch = valid_builder.finish_batch();
+    const VariantRef valid = valid_batch.value_at(0);
+
+    std::string invalid_metadata(invalid.metadata.data, invalid.metadata.size);
+    const size_t invalid_key = invalid_metadata.find("zz_unused");
+    ASSERT_NE(invalid_key, std::string::npos);
+    invalid_metadata[invalid_key] = static_cast<char>(0xff);
+
+    MutableColumns fields;
+    fields.push_back(nullable_strings(
+            {StringRef(invalid_metadata), StringRef(valid.metadata.data, 
valid.metadata.size)},
+            {0, 0}));
+    fields.push_back(nullable_strings({invalid.value, valid.value}, {0, 0}));
+    auto physical = root_wrapper(std::move(fields), {0, 0});
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    ASSERT_TRUE(materialize_variant_rows(unshredded_schema(), *physical, 
output).ok());
+    const auto& root_nullable = assert_cast<const ColumnNullable&>(*output);
+    const auto& root_variants =
+            assert_cast<const 
ColumnVariantV2&>(root_nullable.get_nested_column());
+    const std::array segments 
{VariantElementV2PathSegment::object_key(StringRef("time_us"))};
+    std::unique_ptr<ResolvedVariantElementV2Path> path;
+    ASSERT_TRUE(resolve_variant_element_v2_path(segments, &path).ok());
+    ColumnPtr result;
+    ASSERT_TRUE(extract_variant_element_v2(
+                        root_variants, *path,
+                        std::span<const 
uint8_t>(root_nullable.get_null_map_data().data(),
+                                                 
root_nullable.get_null_map_data().size()),
+                        &result)
+                        .ok());
+
+    const auto& result_variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*result).get_nested_column());
+    ASSERT_TRUE(result_variants.is_shredded());
+    EXPECT_THROW((void)result_variants.get_value_ref(0), Exception);
+}
+
 TEST(VariantColumnReaderTest, 
UnshreddedIntegerLeafReportsCorruptPayloadOnFallback) {
     VariantBatchBuilder builder;
     auto row = builder.begin_row();
@@ -1061,6 +1174,192 @@ TEST(VariantColumnReaderTest, 
UnshreddedMixedScalarLeafFallsBackToSubtree) {
     EXPECT_EQ(result_variants.get_value_ref(1).get_string(), 
StringRef("seven"));
 }
 
+TEST(VariantColumnReaderTest, UnshreddedDirectImportPreservesEncodedBytes) {
+    VariantBatchBuilder metadata_builder;
+    auto metadata_row = metadata_builder.begin_row();
+    auto metadata_object = metadata_row.start_object();
+    metadata_object.add_key(StringRef("unused"));
+    metadata_row.add_int(1);
+    metadata_object.finish();
+    metadata_row.finish();
+    VariantBatchBuilder metadata_batch = metadata_builder.finish_batch();
+    const VariantRef metadata_source = metadata_batch.value_at(0);
+
+    const std::array<char, 2> int_seven {
+            static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT8)
+                              << VARIANT_VALUE_HEADER_SHIFT),
+            7};
+    const StringRef encoded_value(int_seven.data(), int_seven.size());
+    MutableColumns fields;
+    fields.push_back(nullable_strings(
+            {{metadata_source.metadata.data, metadata_source.metadata.size}}, 
{0}));
+    fields.push_back(nullable_strings({encoded_value}, {0}));
+    auto physical = root_wrapper(std::move(fields));
+
+    RuntimeProfile runtime_profile("unshredded-direct-import");
+    ParquetProfile parquet_profile;
+    parquet_profile.init(&runtime_profile);
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    const Status status = materialize_variant_rows(unshredded_schema(), 
*physical, output,
+                                                   
parquet_profile.column_reader_profile());
+    ASSERT_TRUE(status.ok()) << status;
+    const auto& variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*output).get_nested_column());
+    ASSERT_TRUE(variants.is_shredded());
+    auto* reconstructed_rows = 
runtime_profile.get_counter("VariantReconstructedRows");
+    auto* reconstruction_time = 
runtime_profile.get_counter("VariantReconstructionTime");
+    auto* direct_import_time = 
runtime_profile.get_counter("VariantUnshreddedDirectImportTime");
+    auto* direct_import_rows = 
runtime_profile.get_counter("VariantUnshreddedDirectImportRows");
+    auto* direct_import_bytes = 
runtime_profile.get_counter("VariantUnshreddedDirectImportBytes");
+    ASSERT_NE(reconstructed_rows, nullptr);
+    ASSERT_NE(reconstruction_time, nullptr);
+    ASSERT_NE(direct_import_time, nullptr);
+    ASSERT_NE(direct_import_rows, nullptr);
+    ASSERT_NE(direct_import_bytes, nullptr);
+    EXPECT_EQ(direct_import_rows->value(), 0);
+    EXPECT_EQ(direct_import_bytes->value(), 0);
+
+    const VariantRef imported = variants.get_value_ref(0);
+    EXPECT_EQ(StringRef(imported.metadata.data, imported.metadata.size),
+              StringRef(metadata_source.metadata.data, 
metadata_source.metadata.size));
+    EXPECT_EQ(imported.value, encoded_value);
+    EXPECT_EQ(imported.get_int(), 7);
+    EXPECT_EQ(reconstructed_rows->value(), 1);
+    EXPECT_EQ(direct_import_rows->value(), 1);
+    EXPECT_EQ(direct_import_bytes->value(),
+              static_cast<int64_t>(metadata_source.metadata.size + 
encoded_value.size));
+    EXPECT_GT(direct_import_time->value(), 0);
+    EXPECT_GE(reconstruction_time->value(), direct_import_time->value());
+}
+
+TEST(VariantColumnReaderTest, 
UnshreddedDirectImportCrossesMaterializationBatchBoundary) {
+    constexpr size_t ROWS = 4097;
+    const std::array<char, 2> int_seven {
+            static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT8)
+                              << VARIANT_VALUE_HEADER_SHIFT),
+            7};
+    const StringRef encoded_value(int_seven.data(), int_seven.size());
+    std::vector<std::string> metadata_storage;
+    std::vector<StringRef> metadata_rows;
+    metadata_storage.reserve(ROWS);
+    metadata_rows.reserve(ROWS);
+    size_t metadata_bytes = 0;
+    for (size_t row = 0; row < ROWS; ++row) {
+        metadata_storage.push_back(single_key_metadata_bytes("unused-" + 
std::to_string(row)));
+        metadata_bytes += metadata_storage.back().size();
+        metadata_rows.emplace_back(metadata_storage.back());
+    }
+    MutableColumns fields;
+    fields.push_back(nullable_strings(metadata_rows, 
std::vector<uint8_t>(ROWS, 0)));
+    fields.push_back(nullable_strings(std::vector<StringRef>(ROWS, 
encoded_value),
+                                      std::vector<uint8_t>(ROWS, 0)));
+    auto physical = root_wrapper(std::move(fields), NullMap(ROWS, 0));
+
+    RuntimeProfile runtime_profile("unshredded-direct-import-batches");
+    ParquetProfile parquet_profile;
+    parquet_profile.init(&runtime_profile);
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    const Status status = materialize_variant_rows(unshredded_schema(), 
*physical, output,
+                                                   
parquet_profile.column_reader_profile());
+    ASSERT_TRUE(status.ok()) << status;
+    const auto& variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*output).get_nested_column());
+    ASSERT_EQ(variants.size(), ROWS);
+    EXPECT_EQ(variants.get_value_ref(0).get_int(), 7);
+    EXPECT_EQ(variants.get_value_ref(4095).get_int(), 7);
+    EXPECT_EQ(variants.get_value_ref(4096).get_int(), 7);
+    EXPECT_EQ(StringRef(variants.get_value_ref(0).metadata.data,
+                        variants.get_value_ref(0).metadata.size),
+              metadata_rows[0]);
+    EXPECT_EQ(StringRef(variants.get_value_ref(4096).metadata.data,
+                        variants.get_value_ref(4096).metadata.size),
+              metadata_rows[4096]);
+    auto* direct_import_rows = 
runtime_profile.get_counter("VariantUnshreddedDirectImportRows");
+    auto* direct_import_bytes = 
runtime_profile.get_counter("VariantUnshreddedDirectImportBytes");
+    ASSERT_NE(direct_import_rows, nullptr);
+    ASSERT_NE(direct_import_bytes, nullptr);
+    EXPECT_EQ(direct_import_rows->value(), ROWS);
+    EXPECT_EQ(direct_import_bytes->value(),
+              static_cast<int64_t>(metadata_bytes + ROWS * 
encoded_value.size));
+}
+
+TEST(VariantColumnReaderTest, 
UnshreddedDirectImportProfilesImmediateFailureTime) {
+    const std::array<char, 1> invalid_value {static_cast<char>(0xff)};
+    const StringRef metadata(VARIANT_EMPTY_METADATA.data(), 
VARIANT_EMPTY_METADATA.size());
+    MutableColumns fields;
+    fields.push_back(nullable_strings({metadata}, {0}));
+    fields.push_back(nullable_strings({{invalid_value.data(), 
invalid_value.size()}}, {0}));
+    auto physical = root_wrapper(std::move(fields));
+
+    RuntimeProfile 
runtime_profile("unshredded-direct-import-immediate-failure");
+    ParquetProfile parquet_profile;
+    parquet_profile.init(&runtime_profile);
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    const Status status = materialize_variant_rows(unshredded_schema(), 
*physical, output,
+                                                   
parquet_profile.column_reader_profile());
+    ASSERT_TRUE(status.ok()) << status;
+    const auto& variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*output).get_nested_column());
+    EXPECT_THROW((void)variants.get_value_ref(0), Exception);
+
+    auto* reconstructed_rows = 
runtime_profile.get_counter("VariantReconstructedRows");
+    auto* direct_import_time = 
runtime_profile.get_counter("VariantUnshreddedDirectImportTime");
+    auto* direct_import_rows = 
runtime_profile.get_counter("VariantUnshreddedDirectImportRows");
+    auto* direct_import_bytes = 
runtime_profile.get_counter("VariantUnshreddedDirectImportBytes");
+    ASSERT_NE(reconstructed_rows, nullptr);
+    ASSERT_NE(direct_import_time, nullptr);
+    ASSERT_NE(direct_import_rows, nullptr);
+    ASSERT_NE(direct_import_bytes, nullptr);
+    EXPECT_EQ(reconstructed_rows->value(), 0);
+    EXPECT_GT(direct_import_time->value(), 0);
+    EXPECT_EQ(direct_import_rows->value(), 0);
+    EXPECT_EQ(direct_import_bytes->value(), 0);
+}
+
+TEST(VariantColumnReaderTest, 
UnshreddedDirectImportProfilesCompletedChunksBeforeFailure) {
+    constexpr size_t VALID_ROWS = 4096;
+    constexpr size_t ROWS = VALID_ROWS + 1;
+    const std::array<char, 2> int_seven {
+            static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT8)
+                              << VARIANT_VALUE_HEADER_SHIFT),
+            7};
+    const std::array<char, 1> invalid_value {static_cast<char>(0xff)};
+    const StringRef metadata(VARIANT_EMPTY_METADATA.data(), 
VARIANT_EMPTY_METADATA.size());
+    const StringRef encoded_value(int_seven.data(), int_seven.size());
+    std::vector<StringRef> values(VALID_ROWS, encoded_value);
+    values.emplace_back(invalid_value.data(), invalid_value.size());
+    MutableColumns fields;
+    fields.push_back(nullable_strings(std::vector<StringRef>(ROWS, metadata),
+                                      std::vector<uint8_t>(ROWS, 0)));
+    fields.push_back(nullable_strings(values, std::vector<uint8_t>(ROWS, 0)));
+    auto physical = root_wrapper(std::move(fields), NullMap(ROWS, 0));
+
+    RuntimeProfile runtime_profile("unshredded-direct-import-late-failure");
+    ParquetProfile parquet_profile;
+    parquet_profile.init(&runtime_profile);
+    auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
+    const Status status = materialize_variant_rows(unshredded_schema(), 
*physical, output,
+                                                   
parquet_profile.column_reader_profile());
+    ASSERT_TRUE(status.ok()) << status;
+    const auto& variants = assert_cast<const ColumnVariantV2&>(
+            assert_cast<const ColumnNullable&>(*output).get_nested_column());
+    EXPECT_THROW((void)variants.get_value_ref(0), Exception);
+
+    auto* reconstructed_rows = 
runtime_profile.get_counter("VariantReconstructedRows");
+    auto* direct_import_time = 
runtime_profile.get_counter("VariantUnshreddedDirectImportTime");
+    auto* direct_import_rows = 
runtime_profile.get_counter("VariantUnshreddedDirectImportRows");
+    auto* direct_import_bytes = 
runtime_profile.get_counter("VariantUnshreddedDirectImportBytes");
+    ASSERT_NE(reconstructed_rows, nullptr);
+    ASSERT_NE(direct_import_time, nullptr);
+    ASSERT_NE(direct_import_rows, nullptr);
+    ASSERT_NE(direct_import_bytes, nullptr);
+    EXPECT_EQ(reconstructed_rows->value(), 0);
+    EXPECT_GT(direct_import_time->value(), 0);
+    EXPECT_EQ(direct_import_rows->value(), VALID_ROWS);
+    EXPECT_EQ(direct_import_bytes->value(),
+              static_cast<int64_t>(VALID_ROWS * (metadata.size + 
encoded_value.size)));
+}
+
 TEST(VariantColumnReaderTest, 
RequiredPhysicalGroupAppendsToNullableExternalSlot) {
     const std::array<char, 2> int_seven {
             static_cast<char>(static_cast<uint8_t>(VariantPrimitiveId::INT8)
@@ -2259,6 +2558,8 @@ TEST(VariantColumnReaderTest, 
ShreddedStateOutlivesScannerProfile) {
     reused_profile.init(runtime_profile.get());
     EXPECT_EQ(parquet_profile.variant_reconstructed_rows,
               reused_profile.variant_reconstructed_rows);
+    EXPECT_EQ(parquet_profile.variant_unshredded_direct_import_rows,
+              reused_profile.variant_unshredded_direct_import_rows);
 
     auto visible_output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
     ASSERT_TRUE(materialize_variant_rows(shredded_int64_schema(), 
shredded_int64_physical({7}),
@@ -2268,6 +2569,7 @@ TEST(VariantColumnReaderTest, 
ShreddedStateOutlivesScannerProfile) {
             assert_cast<const 
ColumnNullable&>(*visible_output).get_nested_column());
     EXPECT_EQ(visible_variants.get_value_ref(0).get_int(), 7);
     
EXPECT_EQ(runtime_profile->get_counter("VariantReconstructedRows")->value(), 1);
+    
EXPECT_EQ(runtime_profile->get_counter("VariantUnshreddedDirectImportRows")->value(),
 0);
 
     auto output = 
make_nullable(std::make_shared<DataTypeVariantV2>())->create_column();
     ASSERT_TRUE(materialize_variant_rows(shredded_int64_schema(), 
shredded_int64_physical({42}),


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

Reply via email to