github-actions[bot] commented on code in PR #66674:
URL: https://github.com/apache/doris/pull/66674#discussion_r3770471052
##########
be/src/core/data_type_serde/data_type_variant_v2_serde.cpp:
##########
@@ -208,71 +687,260 @@ char* DataTypeVariantV2SerDe::serialize(const IColumn&
column, char* buf, int be
buf = serialize_const_flag_and_row_num(&physical, buf, &saved_rows);
const auto& variant = assert_cast<const ColumnVariantV2&>(*physical);
DCHECK_EQ(variant.size(), saved_rows);
- unaligned_store<bool>(buf, variant.is_typed());
- buf += sizeof(bool);
- if (variant.is_typed()) {
- write_variant_v2_type(variant._typed_type, buf);
- return make_nullable(variant._typed_type)->serialize(*variant._typed,
buf, be_exec_version);
+
+ if (!variant.is_shredded()) {
+ return serialize_non_shredded_payload(variant, buf, be_exec_version);
}
- const DataTypeString string_type;
- buf = string_type.serialize(*variant._metadatas, buf, be_exec_version);
- buf = serialize_meta_ids(*variant._meta_ids, buf);
- return string_type.serialize(*variant._values, buf, be_exec_version);
+
+ unaligned_store<uint8_t>(buf,
static_cast<uint8_t>(VariantV2WireRepresentation::SHREDDED));
Review Comment:
[P1] Preserve the legacy wire format for older BEs. This branch emits the
new SHREDDED tag for every `be_exec_version`, but whole-root Variant V2 scans
can produce S and send it through remote exchange or tablet-writer RPCs during
a rolling upgrade. Both base and head advertise exec version 11, and the base
reader treats this byte as the old `bool typed` discriminator, so tag 2 makes
it misparse the following S header. `ShreddedWireDoesNotDependOnBeExecVersion`
uses patched code on both sides and actually locks tag 2 in for version 10.
Please introduce a new execution-version capability, serialize an encoded/tag-0
snapshot below it, and cover the legacy reader (or an equivalent golden
fixture).
##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -315,20 +691,463 @@ ValidatedTypedInput validate_typed_input(ColumnPtr
column, DataTypePtr scalar_ty
} // namespace
+template <typename Selection>
+void ColumnVariantV2::_append_source_fields_to_residual(
+ ColumnVariantV2& residual, const ColumnVariantV2& source,
+ const DorisVector<const ShreddedField*>& fields_to_residual, const
Selection& selection) {
+ DORIS_CHECK(residual.is_encoded());
+ DorisVector<uint8_t> rows_needing_merge(selection.size(), 0);
+ for (size_t output_row = 0; output_row < selection.size(); ++output_row) {
+ const size_t source_row = selection.source_row(output_row);
+ for (const auto* field : fields_to_residual) {
+ if (field->presence->get_data()[source_row] != 0) {
+ rows_needing_merge[output_row] = 1;
+ break;
+ }
+ }
+ }
+
+ const auto append_direct_run = [&](size_t run_begin, size_t run_end) {
+ if constexpr (std::is_same_v<Selection, ShreddedRangeSelection>) {
+ residual._append_encoded_range(source, selection.start + run_begin,
+ run_end - run_begin);
+ } else {
+ static_assert(std::is_same_v<Selection, ShreddedIndicesSelection>);
+ residual._append_encoded_indices(source, selection.begin +
run_begin,
+ selection.begin + run_end);
+ }
+ };
+
+#ifdef BE_TEST
+ size_t slow_rows = 0;
+#endif
+ ShreddedFieldRefs present_fields;
+ present_fields.reserve(fields_to_residual.size());
+ const auto view = source.read_view();
+ for (size_t run_begin = 0; run_begin < selection.size();) {
+ const bool needs_residual_merge = rows_needing_merge[run_begin] != 0;
+ size_t run_end = run_begin + 1;
+ while (run_end < selection.size() &&
+ (rows_needing_merge[run_end] != 0) == needs_residual_merge) {
+ ++run_end;
+ }
+ if (!needs_residual_merge) {
+ append_direct_run(run_begin, run_end);
+ run_begin = run_end;
+ continue;
+ }
+
+ VariantBatchBuilder builder({.rows = run_end - run_begin});
+ for (size_t output_row = run_begin; output_row < run_end;
++output_row) {
+ const size_t source_row = selection.source_row(output_row);
+ present_fields.clear();
+ for (const auto* field : fields_to_residual) {
+ if (field->presence->get_data()[source_row] != 0) {
+ present_fields.push_back(field);
+ }
+ }
+ DORIS_CHECK(!present_fields.empty());
+ auto output = builder.begin_row();
+ append_merged_shredded_node(output,
view.residual_value_at(source_row), present_fields,
+ 0, present_fields.size(), 0,
source_row);
+ output.finish();
+ }
+ VariantBatchBuilder encoded = builder.finish_batch();
+ residual.insert_encoded_batch(encoded);
+#ifdef BE_TEST
+ slow_rows += run_end - run_begin;
+#endif
+ run_begin = run_end;
+ }
+#ifdef BE_TEST
+ _test_shredded_conflict_slow_rows += slow_rows;
+#endif
+}
+
+template <typename Selection>
+void ColumnVariantV2::_append_same_shredded_layout_rows(const ColumnVariantV2&
source,
+ const Selection&
selection) {
+ DCHECK(is_shredded());
+ DCHECK(source.is_shredded());
+ DCHECK_EQ(_shredded_fields.size(), source._shredded_fields.size());
+ if constexpr (std::is_same_v<Selection, ShreddedRangeSelection>) {
+ _append_encoded_range(source, selection.start, selection.size());
+ } else {
+ static_assert(std::is_same_v<Selection, ShreddedIndicesSelection>);
+ _append_encoded_indices(source, selection.begin, selection.end);
+ }
+ for (size_t index = 0; index < _shredded_fields.size(); ++index) {
+ auto& destination_field = _shredded_fields[index];
+ const auto& source_field = source._shredded_fields[index];
+ const auto& destination_values =
+ assert_cast<const ColumnVariantV2&>(*destination_field.values);
+ if (destination_values.is_typed()) {
+ DCHECK(shredded_children_append_compatible(destination_field,
source_field));
+ } else {
+ const auto& source_values = assert_cast<const
ColumnVariantV2&>(*source_field.values);
+ if (source_values.is_typed() &&
+ !has_present_selected_row(*source_field.presence, selection)) {
+ _append_missing_shredded_field(destination_field,
selection.size());
+ continue;
+ }
+ }
+ mutate_subcolumn(destination_field.values);
+ mutate_subcolumn<ColumnUInt8>(destination_field.presence);
+ selection.insert_from(*destination_field.values, *source_field.values);
Review Comment:
[P1] Do not encode inactive typed padding during S-to-S append. An encoded
destination child can reach this call with a typed source child, and
`selection.insert_from` converts every selected T row even when the source
field's presence bit is 0. Valid S columns may keep arbitrary padding there
(the new invalid-DATE element-at test demonstrates this contract), so DATE
conversion can throw on an unobservable row. Because the residual was already
appended, this path also leaves the destination with mismatched row counts
after the exception. Please mask absent rows to canonical padding and make the
direct append atomic; cover range and indices paths with inactive invalid typed
padding.
##########
be/src/storage/segment/variant/variant_column_writer_impl.cpp:
##########
@@ -1993,15 +1994,66 @@ Status VariantSubcolumnWriter::_append_v2(const
VariantColumnData& column, size_
return Status::OK();
};
- const auto view = source->read_view();
- if (!view.is_typed()) {
- return append_encoded(view, column.row_pos);
+ if (source->is_encoded()) {
+ const Status status = append_encoded(source->read_view(),
column.row_pos);
+ if (status.ok() && append_stats != nullptr) {
+ *append_stats = {};
+ }
+ return status;
}
- auto encoded_batch = ColumnVariantV2::create();
+ if (source->is_shredded()) {
+ VariantShredderAppendStats result_stats;
+ const ColumnVariantV2::ReadView view = source->read_view();
+ for (size_t offset = 0; offset < num_rows; ++offset) {
+ if (!outer_nulls.empty() && outer_nulls[offset] != 0) {
+ ++result_stats.native_shredded_rows;
+ continue;
+ }
+ const size_t input_row = column.row_pos + offset;
+ bool has_active_field = false;
+ for (size_t field = 0; field < view.shredded_field_count();
++field) {
+ if (view.shredded_field_presence(field).get_data()[input_row]
!= 0) {
+ has_active_field = true;
+ break;
+ }
+ }
+ if (!has_active_field) {
+ const VariantRef residual = view.residual_value_at(input_row);
+ if (!residual.is_null()) {
+ RETURN_IF_ERROR(_v2_builder->append(residual, _num_rows +
offset));
+ }
+ ++result_stats.native_shredded_rows;
+ continue;
+ }
+
+ // An extracted subcolumn is a whole-value consumer. Reconstruct
only rows that carry
+ // active shredded fields; missing-field/scalar/array rows use the
residual directly.
+ ColumnVariantV2::MutablePtr encoded_row;
+ RETURN_IF_CATCH_EXCEPTION(
+ { encoded_row =
source->materialize_encoded_range(input_row, 1); });
Review Comment:
[P2] Batch this reconstruction for extracted-column writes. For every active
S row, this loop has already scanned the field-presence layout and then
`materialize_encoded_range(row, 1)` scans it again while allocating a new
builder and temporary column. Dense shredded batches therefore do roughly 2*R*F
probes plus R allocations, and each extracted writer repeats it. Please reuse
one batch encoder/snapshot (or at least materialize active runs) and add a
many-row/many-field test that bounds encoded-range materializations.
##########
be/src/storage/segment/variant/v2/variant_shredder.cpp:
##########
@@ -659,6 +791,173 @@ Status VariantShredder::append(const
ColumnVariantV2::ReadView& view, size_t beg
}
}
+Status VariantShredder::append_shredded(const ColumnVariantV2& source, size_t
begin, size_t length,
+ std::span<const uint8_t> outer_nulls,
+ VariantShredderAppendStats*
append_stats) {
+ RETURN_IF_ERROR(_impl->require_collecting());
+ const ColumnVariantV2::ReadView view = source.read_view();
+ if (!view.is_shredded()) {
+ return _impl->fail(
+ Status::InvalidArgument("Variant shredder shredded append
requires S-state input"));
+ }
+ if (begin > view.size() || length > view.size() - begin) {
+ return _impl->fail(
+ Status::InvalidArgument("Variant shredder range [{}, {})
exceeds input size {}",
+ begin, begin + length, view.size()));
+ }
+ if (!outer_nulls.empty() && outer_nulls.size() != length) {
+ return _impl->fail(
+ Status::InvalidArgument("Variant shredder outer-null span has
{} rows, expected {}",
+ outer_nulls.size(), length));
+ }
+ if (length > std::numeric_limits<size_t>::max() - _impl->rows) {
+ return _impl->fail(Status::InvalidArgument("Variant shredder row count
overflows size_t"));
+ }
+
+ VariantShredderAppendStats result_stats;
+ try {
+ DorisVector<Impl::MetadataPathCache> residual_metadata_caches;
+ residual_metadata_caches.reserve(view.residual_metadata_count());
+ for (size_t metadata_index = 0; metadata_index <
view.residual_metadata_count();
+ ++metadata_index) {
+ residual_metadata_caches.emplace_back(
+
view.residual_metadata_at(static_cast<uint32_t>(metadata_index)));
+ }
+
+ const size_t field_count = view.shredded_field_count();
+ DorisVector<ColumnVariantV2::ReadView> field_views;
+ field_views.reserve(field_count);
+ DorisVector<std::optional<Impl::PathIndex>>
field_path_indices(field_count);
+ DorisVector<size_t> field_canonical_groups(field_count, 0);
+ std::unordered_map<std::string_view, size_t> canonical_field_groups;
+ canonical_field_groups.reserve(field_count);
+ for (size_t field_index = 0; field_index < field_count; ++field_index)
{
+
field_views.emplace_back(view.shredded_field_values(field_index).read_view());
+ const PathInData& field_path =
view.shredded_field_path(field_index);
+ const auto [group, inserted] = canonical_field_groups.emplace(
+ field_path.get_path(), canonical_field_groups.size());
+ static_cast<void>(inserted);
+ field_canonical_groups[field_index] = group->second;
+ }
+ DorisVector<std::string_view>
canonical_group_paths(canonical_field_groups.size());
+ for (const auto& [path, group] : canonical_field_groups) {
+ canonical_group_paths[group] = path;
+ }
+ DorisVector<size_t>
active_group_markers(canonical_field_groups.size(), 0);
+
+ DorisVector<size_t> active_fields;
+ active_fields.reserve(field_count);
+ DorisVector<size_t> active_groups;
+ active_groups.reserve(canonical_field_groups.size());
+ DorisVector<char> encoded_slow_scratch;
+ for (size_t offset = 0; offset < length; ++offset) {
+ if (!outer_nulls.empty() && outer_nulls[offset] != 0) {
+ _impl->append_default_root();
+ ++_impl->rows;
+ ++result_stats.native_shredded_rows;
+ continue;
+ }
+
+ const size_t input_row = begin + offset;
+ active_fields.clear();
+ active_groups.clear();
+ bool duplicate_active_path = false;
+ const size_t row_marker = offset + 1;
+ for (size_t field_index = 0; field_index < field_count;
++field_index) {
+ if
(view.shredded_field_presence(field_index).get_data()[input_row] != 0) {
+ active_fields.push_back(field_index);
+ if
(!_impl->shredded_field_participates(field_views[field_index], input_row)) {
+ continue;
+ }
+ const size_t group = field_canonical_groups[field_index];
+ duplicate_active_path |= active_group_markers[group] ==
row_marker;
+ if (active_group_markers[group] != row_marker) {
+ active_group_markers[group] = row_marker;
+ active_groups.push_back(group);
+ }
+ }
+ }
+
+ const uint32_t metadata_index =
view.residual_metadata_id_at(input_row);
+ if (metadata_index >= residual_metadata_caches.size()) {
+ return _impl->fail(Status::Corruption(
+ "Variant residual row {} metadata index {} exceeds {}
entries", input_row,
+ metadata_index, residual_metadata_caches.size()));
+ }
+ const VariantRef residual = view.residual_value_at(input_row);
+
+ // A literal dotted key and a nested path intentionally collide in
the legacy storage
+ // namespace. Probe only residual branches that can spell a
currently active canonical
+ // path; unrelated dotted keys do not trigger a full residual
pre-scan.
+ Impl::MetadataPathCache& residual_metadata_cache =
+ residual_metadata_caches[metadata_index];
+ bool residual_collision = false;
+ if (!duplicate_active_path) {
+ for (size_t group : active_groups) {
+ const std::string_view canonical_path =
canonical_group_paths[group];
+ if (canonical_path.find('.') != std::string_view::npos &&
+ _impl->residual_contains_canonical_leaf(residual,
canonical_path)) {
Review Comment:
[P2] Avoid rescanning the residual once per active nested field. Reader
assembly deliberately leaves each moved nested leaf's empty ancestor in the
residual, so F paths such as `p_i.leaf` produce F top-level empty objects. This
loop then calls `residual_contains_canonical_leaf` F times, and every call
linearly searches those root children, making the native write path O(F^2) per
row (about four million key comparisons at the default 2048-path limit) even
when no fallback is needed. Please traverse/index the residual once and merge
or probe the active canonical groups against that result, with a wide-S
inspection-count test.
##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -1103,7 +2354,12 @@ void ColumnVariantV2::deserialize(StringRef* keys,
size_t num_rows) {
void ColumnVariantV2::update_hash_with_value(size_t row, SipHash& hash) const {
DCHECK_LT(row, size());
- if (_typed) {
+ if (is_shredded()) {
+ auto encoded = materialize_encoded_range(row, 1);
Review Comment:
[P2] Avoid rebuilding an encoded column per hashed S row.
`Block::update_hash` calls this virtual method in a row loop (and
Array/Map/Struct hashing can call it per nested value), so this branch performs
N `VariantBatchBuilder`/column allocations plus N full layout scans. The batch
hash overrides already materialize a range once. Please provide direct
canonical S hashing or a batch-scoped snapshot/cache, and add a multi-row
interface test that bounds materializations.
##########
be/src/core/data_type_serde/data_type_variant_v2_serde.cpp:
##########
@@ -208,71 +687,260 @@ char* DataTypeVariantV2SerDe::serialize(const IColumn&
column, char* buf, int be
buf = serialize_const_flag_and_row_num(&physical, buf, &saved_rows);
const auto& variant = assert_cast<const ColumnVariantV2&>(*physical);
DCHECK_EQ(variant.size(), saved_rows);
- unaligned_store<bool>(buf, variant.is_typed());
- buf += sizeof(bool);
- if (variant.is_typed()) {
- write_variant_v2_type(variant._typed_type, buf);
- return make_nullable(variant._typed_type)->serialize(*variant._typed,
buf, be_exec_version);
+
+ if (!variant.is_shredded()) {
+ return serialize_non_shredded_payload(variant, buf, be_exec_version);
}
- const DataTypeString string_type;
- buf = string_type.serialize(*variant._metadatas, buf, be_exec_version);
- buf = serialize_meta_ids(*variant._meta_ids, buf);
- return string_type.serialize(*variant._values, buf, be_exec_version);
+
+ unaligned_store<uint8_t>(buf,
static_cast<uint8_t>(VariantV2WireRepresentation::SHREDDED));
+ buf += sizeof(uint8_t);
+ unaligned_store<uint8_t>(buf, SHREDDED_WIRE_VERSION);
+ buf += sizeof(uint8_t);
+ char* payload_size_position = buf;
+ buf += sizeof(uint64_t);
+ char* const payload_begin = buf;
+
+ unaligned_store<uint8_t>(buf, variant._shredded_path_set_sealed ? 1 : 0);
+ buf += sizeof(uint8_t);
+ char* residual_size_position = buf;
+ buf += sizeof(uint64_t);
+ char* const residual_begin = buf;
+ buf = serialize_encoded_payload(variant, buf, be_exec_version);
+ unaligned_store<uint64_t>(
+ residual_size_position,
+ checked_wire_length(buf - residual_begin, "shredded residual
payload"));
+ unaligned_store<uint32_t>(
+ buf, checked_wire_count(variant._shredded_fields.size(), "shredded
field"));
+ buf += sizeof(uint32_t);
+ for (const auto& field : variant._shredded_fields) {
+ const auto& parts = field.path.get_parts();
+ unaligned_store<uint32_t>(buf, checked_wire_count(parts.size(), "path
part"));
+ buf += sizeof(uint32_t);
+ for (const auto& part : parts) {
+ unaligned_store<uint32_t>(buf, checked_wire_count(part.key.size(),
"path key byte"));
+ buf += sizeof(uint32_t);
+ if (!part.key.empty()) {
+ std::memcpy(buf, part.key.data(), part.key.size());
+ buf += part.key.size();
+ }
+ unaligned_store<uint8_t>(buf, part.is_nested ? 1 : 0);
+ buf += sizeof(uint8_t);
+ unaligned_store<uint8_t>(buf, part.anonymous_array_level);
+ buf += sizeof(uint8_t);
+ }
+ const auto& presence = static_cast<const
ColumnUInt8::Ptr&>(field.presence)->get_data();
+ unaligned_store<uint64_t>(buf, presence.size());
+ buf += sizeof(uint64_t);
+ if (!presence.empty()) {
+ std::memcpy(buf, presence.data(), presence.size());
+ buf += presence.size();
+ }
+ char* child_size_position = buf;
+ buf += sizeof(uint64_t);
+ char* const child_begin = buf;
+ buf = serialize_non_shredded_payload(assert_cast<const
ColumnVariantV2&>(*field.values),
+ buf, be_exec_version);
+ unaligned_store<uint64_t>(child_size_position,
+ checked_wire_length(buf - child_begin,
"shredded child payload"));
+ }
+ unaligned_store<uint64_t>(payload_size_position,
+ checked_wire_length(buf - payload_begin,
"shredded payload"));
+ return buf;
}
const char* DataTypeVariantV2SerDe::deserialize(const char* buf,
MutableColumnPtr* column,
int be_exec_version) {
+ constexpr size_t REPRESENTATION_OFFSET = VARIANT_V2_COLUMN_HEADER_BYTES;
+ const char* const representation_position = buf + REPRESENTATION_OFFSET;
+ const auto representation = static_cast<VariantV2WireRepresentation>(
+ unaligned_load<uint8_t>(representation_position));
+ if (representation == VariantV2WireRepresentation::SHREDDED) {
+ const char* const payload_size_position = representation_position +
sizeof(uint8_t) * 2;
+ const uint64_t payload_size =
unaligned_load<uint64_t>(payload_size_position);
+ const uintptr_t payload_address =
+ reinterpret_cast<uintptr_t>(payload_size_position) +
sizeof(uint64_t);
+ if (payload_size > std::numeric_limits<uintptr_t>::max() -
payload_address) {
+ throw Exception(Status::Corruption(
+ "Shredded ColumnVariantV2 payload length {} overflows
address space",
+ payload_size));
+ }
+ return deserialize(buf, reinterpret_cast<const char*>(payload_address
+ payload_size),
+ column, be_exec_version);
+ }
+
auto* destination = assert_cast<ColumnVariantV2*>(column->get());
- size_t saved_rows = 0;
- buf = deserialize_const_flag_and_row_num(buf, column, &saved_rows);
- const bool typed = unaligned_load<bool>(buf);
+ const auto is_const_flag = unaligned_load<uint8_t>(buf);
+ if (is_const_flag > 1) {
+ throw Exception(
+ Status::Corruption("ColumnVariantV2 has invalid const flag
{}", is_const_flag));
+ }
+ const bool is_const = is_const_flag != 0;
buf += sizeof(bool);
+ const auto logical_rows = unaligned_load<size_t>(buf);
+ buf += sizeof(size_t);
+ const auto saved_rows = unaligned_load<size_t>(buf);
+ buf += sizeof(size_t);
+ if ((is_const && saved_rows != 1) || (!is_const && saved_rows !=
logical_rows)) {
+ throw Exception(Status::Corruption(
+ "ColumnVariantV2 invalid row header: const={}, logical
rows={}, saved rows={}",
+ is_const, logical_rows, saved_rows));
+ }
- ColumnVariantV2::MutablePtr decoded;
- if (typed) {
- DataTypePtr type = read_variant_v2_type(buf);
- const DataTypePtr nullable_type = make_nullable(type);
- MutableColumnPtr typed_column = nullable_type->create_column();
- buf = nullable_type->deserialize(buf, &typed_column, be_exec_version);
- decoded = ColumnVariantV2::create_typed(std::move(typed_column),
std::move(type));
+ MutableColumnPtr decoded;
+ if (representation != VariantV2WireRepresentation::ENCODED &&
+ representation != VariantV2WireRepresentation::TYPED_SCALAR) {
+ throw Exception(Status::Corruption("Unknown ColumnVariantV2
representation tag {}",
+
static_cast<uint8_t>(representation)));
+ }
+ buf = deserialize_non_shredded_payload(buf, &decoded, be_exec_version);
+
+ const auto& decoded_variant = assert_cast<const
ColumnVariantV2&>(*decoded);
+ if (decoded_variant.size() != saved_rows) {
+ throw Exception(Status::Corruption(
+ "ColumnVariantV2 saved row count {} does not match decoded row
count {}",
+ saved_rows, decoded_variant.size()));
+ }
+ if (is_const) {
+ ColumnPtr decoded_data = std::move(decoded);
+ *column = ColumnConst::create(std::move(decoded_data), logical_rows);
} else {
- const DataTypeString string_type;
- MutableColumnPtr metadatas = string_type.create_column();
- MutableColumnPtr meta_ids = MetaIdsColumn::create();
- MutableColumnPtr values = string_type.create_column();
- buf = string_type.deserialize(buf, &metadatas, be_exec_version);
- buf = deserialize_meta_ids(buf, &meta_ids);
- buf = string_type.deserialize(buf, &values, be_exec_version);
-
- const auto& ids = assert_cast<const
MetaIdsColumn&>(*meta_ids).get_data();
- if (ids.size() != values->size()) {
+
destination->_adopt_state_from(assert_cast<ColumnVariantV2&>(*decoded));
+ }
+ return buf;
+}
+
+const char* DataTypeVariantV2SerDe::deserialize(const char* buf, const char*
end,
+ MutableColumnPtr* column, int
be_exec_version) {
+ if (buf == nullptr || end == nullptr || end < buf) {
+ throw Exception(Status::Corruption("ColumnVariantV2 has an invalid
wire buffer"));
+ }
+
+ ShreddedWireCursor cursor(buf, static_cast<size_t>(end - buf));
+ const auto header = read_serialized_column_header(cursor, "column header");
+ const char* const representation_position = cursor.position();
+ const auto representation =
+
static_cast<VariantV2WireRepresentation>(cursor.read<uint8_t>("representation
header"));
+
+ MutableColumnPtr decoded;
+ const char* result = nullptr;
+ if (representation == VariantV2WireRepresentation::ENCODED ||
+ representation == VariantV2WireRepresentation::TYPED_SCALAR) {
+ result = deserialize_non_shredded_payload(representation_position,
&decoded,
+ be_exec_version);
+ if (result > end) {
+ throw Exception(
+ Status::Corruption("ColumnVariantV2 payload exceeds the
provided wire buffer"));
+ }
+ } else if (representation == VariantV2WireRepresentation::SHREDDED) {
+ const auto wire_version = cursor.read<uint8_t>("shredded wire
version");
+ if (wire_version != SHREDDED_WIRE_VERSION_WITHOUT_LAYOUT_STATE &&
+ wire_version != SHREDDED_WIRE_VERSION) {
+ throw Exception(Status::Corruption(
+ "Unsupported shredded ColumnVariantV2 wire version {}",
wire_version));
+ }
+ const auto payload_size = cursor.read<uint64_t>("shredded payload
length");
+ const auto payload = cursor.read_bytes(payload_size, "shredded
payload");
+ result = cursor.position();
+
+ ShreddedWireCursor payload_cursor(payload.data(), payload.size());
+ bool path_set_sealed = true;
+ if (wire_version == SHREDDED_WIRE_VERSION) {
+ const uint8_t sealed_flag =
+ payload_cursor.read<uint8_t>("shredded path-set sealed
flag");
+ if (sealed_flag > 1) {
+ throw Exception(Status::Corruption(
+ "Shredded ColumnVariantV2 has invalid path-set sealed
flag {}",
+ sealed_flag));
+ }
+ path_set_sealed = sealed_flag != 0;
+ }
+ const auto residual_size = payload_cursor.read<uint64_t>("residual
payload length");
+ const auto residual_payload = payload_cursor.read_bytes(residual_size,
"residual payload");
+
+ auto residual = ColumnVariantV2::create();
+ const char* const residual_end = deserialize_encoded_payload(
Review Comment:
[P1] Bound the nested decoder before calling it. `residual_payload` is a
framed span, but `deserialize_encoded_payload` receives only its start pointer;
its String/meta-id decoders read embedded headers, resize, and memcpy before
this post-hoc end comparison. A valid outer S payload with an undersized
residual (or forged inner length) can therefore read or allocate beyond the
declared frame instead of returning `CORRUPTION`; the child path below has the
same problem. Please pass `(begin,end)`/a cursor through every nested E/T
decoder and add valid-outer/invalid-inner residual and child cases.
##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -668,15 +1801,18 @@ void ColumnVariantV2::get(size_t row, Field& result)
const {
}
VariantField value;
- if (_typed) {
+ if (is_typed()) {
const auto& nullable = assert_cast<const ColumnNullable&>(*_typed);
visit_typed_scalar_column(nullable, _typed_type->get_primitive_type(),
_typed_type->get_scale(), row, row + 1,
[&](size_t, const VariantScalarRef& scalar) {
value =
VariantField::from_scalar(scalar);
});
- } else {
+ } else if (is_encoded()) {
value = VariantField::from_ref(get_value_ref(row));
+ } else {
+ auto encoded = materialize_encoded_range(row, 1);
Review Comment:
[P2] Avoid rebuilding an encoded column in generic `Field` extraction.
`map_agg_v2` calls `get` for every input row and permits Variant values, while
aggregate-key `VARIANT REPLACE` reaches `CopyStore::set_value -> get` per
selected group. Whole-root scans now produce S, so these paths allocate a
builder/column and scan every layout field per row or high-cardinality group.
Please build the owned `VariantField` directly or reuse a batch snapshot (and
preserve S through reader REPLACE where possible), with many-row tests that
bound materializations.
##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -969,7 +2199,11 @@ void ColumnVariantV2::insert_data(const char* pos, size_t
length) {
StringRef ColumnVariantV2::serialize_value_into_arena(size_t row, Arena& arena,
const char*& begin)
const {
DCHECK_LT(row, size());
- if (_typed) {
+ if (is_shredded()) {
+ auto encoded = materialize_encoded_range(row, 1);
Review Comment:
[P2] Avoid publishing a temporary encoded column for each arena-serialized S
value. Serialized join/DISTINCT keys call this interface per row, and
Array/Map/Struct serialization calls the S child's size and write interfaces
separately, so a nested value is reconstructed at least twice (builder
allocation plus full layout scan each time). Please implement direct canonical
S sizing/writing or reuse a batch-scoped encoded snapshot across both passes,
and add direct plus nested arena-serialization tests that bound
materializations.
##########
be/src/storage/segment/variant/v2/variant_assembler.cpp:
##########
@@ -381,6 +463,51 @@ Status append_merge_value(const MergeValue& value, size_t
row, ObjectEmitter* em
value.cell);
}
+Status append_shredded_merge_value(const MergeValue& value, size_t row,
+ VariantShreddedColumnBuilder::Batch*
shredded_batch,
+ ObjectEmitter* emitter, bool* routed) {
+ DORIS_CHECK(shredded_batch != nullptr);
+ DORIS_CHECK(routed != nullptr);
+ *routed = false;
+ const std::optional<size_t> path_index =
+ value.materialized_slot != nullptr
+ ? value.materialized_slot->shredded_path_index
+ : shredded_batch->find_raw_path(value.raw_path,
value.depth);
+ if (!path_index.has_value()) {
+ return Status::OK();
+ }
+
+ if (value.materialized != nullptr &&
+ is_supported_variant_typed_identity(value.materialized->primitive)) {
+ shredded_batch->append_materialized(*path_index, row);
+ emitter->erase_leaf(value.raw_path, value.is_direct_subtree_value,
value.depth);
+ *routed = true;
+ return Status::OK();
+ }
+
+ // Structural cells must remain in the residual. Decode an exact candidate
once into row-local
+ // scratch so both the scalar decision and residual append consume the
same validated value.
+ VariantBatchBuilder scratch({.rows = 1});
Review Comment:
[P2] Reuse decoding scratch across the scan batch. This branch builds and
publishes a fresh one-row `VariantBatchBuilder` for every sparse candidate that
maps to the fixed layout; the new type/shape-conflict tests make that an
ordinary reachable scan shape. The root-sidecar branch repeats the same work
per row even though production binds every layout path, so `append_root` skips
extraction and only copies the decoded value to the residual. Please decode
through batch-owned scratch/direct classification and route bound-layout roots
directly to the residual, with a many-row test that bounds temporary builder
publications.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]