This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.1 by this push:
new bb02660ca2c branch-4.1:[opt](variant) Avoid rebuilding unshredded
Parquet Variant values (#66714)
bb02660ca2c is described below
commit bb02660ca2c447fdd6a6075cadb8e5fd75b538ef
Author: daidai <[email protected]>
AuthorDate: Fri Aug 14 10:09:13 2026 +0800
branch-4.1:[opt](variant) Avoid rebuilding unshredded Parquet Variant
values (#66714)
### What problem does this PR solve?
Problem Summary:
Import validated unshredded metadata/value bytes directly into
ColumnVariantV2 instead of recursively decoding and re-encoding them.
Add Parquet profile counters for direct-import time, rows, and bytes,
and cover lazy materialization and shredded-path isolation.
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
.../core/column/variant_v2/column_variant_v2.cpp | 335 +++++++++++++++++++--
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 | 127 +++++++-
be/test/core/column/column_variant_v2_test.cpp | 174 +++++++++++
.../parquet/variant_column_reader_test.cpp | 202 +++++++++++++
7 files changed, 849 insertions(+), 41 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 8996372a9d0..4a467582fe9 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>
@@ -124,6 +125,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) {
@@ -937,40 +952,242 @@ void ColumnVariantV2::insert_encoded_rows( //
NOLINT(readability-function-size)
}
}
- if (_typed) {
- 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 = assert_cast<ColumnString&>(*_values);
- auto& metadata_ids = assert_cast<MetaIdsColumn&>(*_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 = assert_cast<const ColumnString&>(
+ *static_cast<const IColumn::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 = assert_cast<const ColumnString&>(
+ *static_cast<const IColumn::Ptr&>(owner._metadatas));
+ const auto& values = assert_cast<const ColumnString&>(*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 = assert_cast<ColumnString&>(*column._values);
+ auto& metadata_ids = assert_cast<MetaIdsColumn&>(*column._meta_ids);
+
+ auto current_metadatas = [&]() -> const ColumnString& {
+ return assert_cast<const ColumnString&>(
+ *static_cast<const IColumn::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) {
@@ -984,10 +1201,38 @@ 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);
- if (_typed) {
+ 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) {
ensure_encoded();
}
DORIS_CHECK(_typed_type == nullptr) << "encoded state cannot retain a
typed data type";
@@ -995,11 +1240,28 @@ void ColumnVariantV2::insert_encoded_batch(const
VariantBatchBuilder& block) {
require_exclusive(_values, "values");
auto& values = assert_cast<ColumnString&>(*_values);
auto& metadata_ids = assert_cast<MetaIdsColumn&>(*_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();
@@ -1893,6 +2155,15 @@ 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 =
+ assert_cast<const ColumnString&>(*static_cast<const
IColumn::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 7cd167663be..affea6a7e34 100644
--- a/be/src/core/column/variant_v2/column_variant_v2.h
+++ b/be/src/core/column/variant_v2/column_variant_v2.h
@@ -114,6 +114,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.
@@ -183,6 +211,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.
@@ -249,6 +281,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 eec657e741f..ce3a973bb02 100644
--- a/be/src/format_v2/parquet/parquet_profile.cpp
+++ b/be/src/format_v2/parquet/parquet_profile.cpp
@@ -111,6 +111,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_direct_leaf_rows =
add_persistent_counter(profile, "VariantDirectLeafRows",
TUnit::UNIT, parquet_profile);
variant_direct_leaf_path_misses = add_persistent_counter(profile,
"VariantDirectLeafPathMisses",
@@ -349,6 +355,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_direct_leaf_rows = variant_direct_leaf_rows,
.variant_direct_leaf_path_misses = variant_direct_leaf_path_misses,
.variant_direct_leaf_residual_fallbacks =
variant_direct_leaf_residual_fallbacks,
diff --git a/be/src/format_v2/parquet/parquet_profile.h
b/be/src/format_v2/parquet/parquet_profile.h
index 18d285cf358..ef74f312cd1 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;
std::shared_ptr<RuntimeProfile::Counter> variant_direct_leaf_rows;
std::shared_ptr<RuntimeProfile::Counter> variant_direct_leaf_path_misses;
std::shared_ptr<RuntimeProfile::Counter>
variant_direct_leaf_residual_fallbacks;
@@ -181,6 +186,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_direct_leaf_rows;
std::shared_ptr<RuntimeProfile::Counter> variant_direct_leaf_path_misses;
std::shared_ptr<RuntimeProfile::Counter>
variant_direct_leaf_residual_fallbacks;
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 d2c6fef0ed5..829ebc7b369 100644
--- a/be/src/format_v2/parquet/reader/variant_column_reader.cpp
+++ b/be/src/format_v2/parquet/reader/variant_column_reader.cpp
@@ -26,6 +26,7 @@
#include <mutex>
#include <optional>
#include <string_view>
+#include <utility>
#include <vector>
#include "common/exception.h"
@@ -38,6 +39,7 @@
#include "core/column/column_vector.h"
#include "core/column/variant_v2/column_variant_v2.h"
#include "core/column/variant_v2/column_variant_v2_typed_column.h"
+#include "core/custom_allocator.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_variant_v2.h"
#include "core/value/variant/variant_batch_builder.h"
@@ -52,6 +54,9 @@ struct Cell {
bool is_null = false;
};
+constexpr std::array<char, 1> VARIANT_NULL_VALUE {static_cast<char>(
+ static_cast<uint8_t>(VariantPrimitiveId::NULL_VALUE) <<
VARIANT_VALUE_HEADER_SHIFT)};
+
Cell cell_at(const IColumn& column, size_t row) {
if (row >= column.size()) {
throw Exception(ErrorCode::CORRUPTION, "Parquet Variant row {} exceeds
column size {}", row,
@@ -444,9 +449,74 @@ void encode_variant_range(const ParquetColumnSchema&
schema, const IColumn& wrap
}
}
-ColumnVariantV2::MutablePtr encode_variant_column(const ParquetColumnSchema&
schema,
- const IColumn& physical,
- bool require_metadata =
true) {
+std::optional<std::pair<size_t, size_t>> unshredded_child_indices(
+ const ParquetColumnSchema& schema) {
+ if (schema.children.size() != 2 || find_child(schema, "typed_value",
nullptr) != nullptr) {
+ return std::nullopt;
+ }
+ size_t metadata_index = 0;
+ size_t value_index = 0;
+ if (find_child(schema, "metadata", &metadata_index) == nullptr ||
+ find_child(schema, "value", &value_index) == nullptr) {
+ return std::nullopt;
+ }
+ return std::pair {metadata_index, value_index};
+}
+
+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);
@@ -461,11 +531,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;
}
@@ -887,7 +996,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 6e2683c39f7..1abb13f67c1 100644
--- a/be/test/core/column/column_variant_v2_test.cpp
+++ b/be/test/core/column/column_variant_v2_test.cpp
@@ -99,6 +99,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));
@@ -272,6 +284,51 @@ private:
ColumnVariantV2::MutablePtr _serialized;
};
+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(const IColumn::ImutableColumnCallback& 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");
@@ -1010,6 +1067,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}) {
@@ -1061,6 +1220,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 74c79d515c8..3581b237436 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>
@@ -68,6 +69,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;
+}
+
ParquetColumnSchema unshredded_schema() {
ParquetColumnSchema schema;
schema.name = "payload";
@@ -572,6 +585,192 @@ TEST(VariantColumnReaderTest,
UnshreddedRowsPreserveSqlNullAndVariantNull) {
EXPECT_TRUE(variants.get_value_ref(2).is_null());
}
+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)
@@ -1690,6 +1889,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}),
@@ -1699,6 +1900,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]