github-actions[bot] commented on code in PR #67016:
URL: https://github.com/apache/doris/pull/67016#discussion_r4003508999
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -586,13 +611,105 @@ ColumnPtr normalize_projected_primitive_leaf(const
ParquetColumnSchema& schema,
return ColumnNullable::create(std::move(values), std::move(nulls));
}
-bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
- VariantRef* output) {
- DORIS_CHECK(output != nullptr);
- for (const auto& segment : path) {
+struct UnshreddedPathCacheEntry {
+ static constexpr uint32_t MISSING = std::numeric_limits<uint32_t>::max();
+
+ uint32_t value_offset = MISSING;
+ uint32_t value_size = 0;
+
+ bool present() const noexcept { return value_offset != MISSING; }
+};
+
+struct UnshreddedPathCache {
+ DorisVector<UnshreddedPathCacheEntry> entries;
+
+ size_t byte_size() const noexcept { return entries.size() *
sizeof(UnshreddedPathCacheEntry); }
+ size_t allocated_bytes() const noexcept {
+ return entries.capacity() * sizeof(UnshreddedPathCacheEntry);
+ }
+};
+
+struct UnshreddedMetadataIndex {
+ static constexpr uint32_t NULL_ROW = std::numeric_limits<uint32_t>::max();
+
+ const IColumn* physical_identity = nullptr;
+ DorisVector<VariantMetadataRef> dictionaries;
+ DorisVector<uint32_t> row_dictionary_ids;
+
+ size_t byte_size() const noexcept {
+ return dictionaries.size() * sizeof(VariantMetadataRef) +
+ row_dictionary_ids.size() * sizeof(uint32_t);
+ }
+ size_t allocated_bytes() const noexcept {
+ return dictionaries.capacity() * sizeof(VariantMetadataRef) +
+ row_dictionary_ids.capacity() * sizeof(uint32_t);
+ }
+};
+
+std::shared_ptr<const UnshreddedMetadataIndex> build_unshredded_metadata_index(
+ const ParquetColumnSchema& schema, const IColumn& physical,
+ std::pair<size_t, size_t> child_indices) {
+ const auto* outer_nullable =
check_and_get_column<ColumnNullable>(physical);
+ const IColumn& wrapper =
+ outer_nullable == nullptr ? physical :
outer_nullable->get_nested_column();
+ const auto& structure = assert_cast<const ColumnStruct&>(wrapper);
+ DORIS_CHECK_EQ(structure.tuple_size(), schema.children.size());
+
+ auto index = std::make_shared<UnshreddedMetadataIndex>();
+ index->physical_identity = &physical;
+ index->row_dictionary_ids.resize(physical.size(),
UnshreddedMetadataIndex::NULL_ROW);
+ 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 dictionary_ids;
+ for (size_t row = 0; row < physical.size(); ++row) {
+ if (outer_nullable != nullptr &&
outer_nullable->get_null_map_data()[row] != 0) {
+ continue;
+ }
+ const Cell metadata =
cell_at(structure.get_column(child_indices.first), row);
+ if (metadata.is_null) {
+ throw Exception(ErrorCode::CORRUPTION, "Parquet Variant {} has
null metadata at row {}",
+ schema.name, row);
+ }
+ const StringRef bytes = metadata.column->get_data_at(row);
+ uint32_t dictionary_id = 0;
+ if (index->dictionaries.empty()) {
+ index->dictionaries.push_back({.data = bytes.data, .size =
bytes.size});
+ } else if (index->dictionaries.size() == 1 && dictionary_ids.empty() &&
+ StringRef(index->dictionaries.front().data,
index->dictionaries.front().size) ==
+ bytes) {
+ // Iceberg normally repeats one metadata dictionary throughout a
decoded block. Delay
+ // the hash table until a second dictionary is actually observed.
+ } else {
+ if (dictionary_ids.empty()) {
+ const VariantMetadataRef first = index->dictionaries.front();
+ dictionary_ids.emplace(std::string_view(first.data,
first.size), 0);
+ }
+ const std::string_view key(bytes.data, bytes.size);
+ if (const auto found = dictionary_ids.find(key); found !=
dictionary_ids.end()) {
+ dictionary_id = found->second;
+ } else {
+ dictionary_id =
static_cast<uint32_t>(index->dictionaries.size());
+ index->dictionaries.push_back({.data = bytes.data, .size =
bytes.size});
+ dictionary_ids.emplace(key, dictionary_id);
+ }
+ }
+ index->row_dictionary_ids[row] = dictionary_id;
+ }
+ return index;
+}
+
+template <typename ObjectFinder>
+bool find_materialized_path_impl(VariantRef current,
+ std::span<const VariantShreddedPathSegment>
path,
+ const ObjectFinder& find_object, VariantRef*
output) {
+ DCHECK(output != nullptr);
+ for (size_t position = 0; position < path.size(); ++position) {
Review Comment:
[P1] Preserve the maximum nesting-depth check on direct traversal
This loop can descend through an arbitrarily long selected object/array
chain without carrying the absolute Variant depth. A structurally valid row
with 129 nested single-child containers followed by a canonical string/int is
therefore returned as a typed value, while the former root reconstruction
rejects it in `VariantBatchBuilder::require_import_depth()` at the 128-level
limit. Chained `element_at` calls also reset the subtree's apparent root, so
later normalization cannot recover the check. Please track the accumulated
prefix depth and enforce `VARIANT_MAX_NESTING_DEPTH`, with 128/129 boundary
tests for one-shot and chained traversal.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -611,6 +728,400 @@ bool find_materialized_path(VariantRef current,
std::span<const VariantShreddedP
return true;
}
+bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
+ VariantRef* output) {
+ return find_materialized_path_impl(
+ current, path,
+ [](VariantRef object, StringRef key, size_t, VariantRef* found) {
+ return object.object_find(key, found);
+ },
+ output);
+}
+
+bool find_materialized_path_with_index(VariantRef current, uint32_t
dictionary_id,
+ const UnshreddedMetadataIndex&
metadata_index,
+ std::span<const
VariantShreddedPathSegment> path,
+ DorisVector<int64_t>&
resolved_field_ids,
+ VariantRef* output) {
+ DCHECK_LT(dictionary_id, metadata_index.dictionaries.size());
+ DCHECK_EQ(resolved_field_ids.size(), metadata_index.dictionaries.size() *
path.size());
+ constexpr int64_t UNRESOLVED_FIELD_ID = -2;
+ return find_materialized_path_impl(
+ current, path,
+ [&](VariantRef object, StringRef key, size_t position, VariantRef*
found) {
+ int64_t& field_id = resolved_field_ids[dictionary_id *
path.size() + position];
+ bool layout_validated = false;
+ if (field_id == UNRESOLVED_FIELD_ID) {
+ // object_find() validates the object layout before
consulting metadata.
+ static_cast<void>(object.num_elements());
+ layout_validated = true;
+ field_id =
metadata_index.dictionaries[dictionary_id].find_key(key);
Review Comment:
[P1] Validate metadata before trusting binary-search ordering
This resolves a key with `find_key()` on raw Parquet metadata before
`VariantMetadataRef::validate()` has established the advertised
sorted-and-unique invariant. For example, metadata containing `["b","a"]` while
setting the sorted flag makes lookup of `"a"` return `-1`, so a present field
is emitted as a missing-path SQL NULL; the former root reconstruction rejected
the metadata. The later integer-only validation cannot help misses, strings,
JSON nulls, or fallback types. Please validate each distinct dictionary before
caching path IDs and cover false sorted/duplicate metadata.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -611,6 +728,400 @@ bool find_materialized_path(VariantRef current,
std::span<const VariantShreddedP
return true;
}
+bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
+ VariantRef* output) {
+ return find_materialized_path_impl(
+ current, path,
+ [](VariantRef object, StringRef key, size_t, VariantRef* found) {
+ return object.object_find(key, found);
+ },
+ output);
+}
+
+bool find_materialized_path_with_index(VariantRef current, uint32_t
dictionary_id,
+ const UnshreddedMetadataIndex&
metadata_index,
+ std::span<const
VariantShreddedPathSegment> path,
+ DorisVector<int64_t>&
resolved_field_ids,
+ VariantRef* output) {
+ DCHECK_LT(dictionary_id, metadata_index.dictionaries.size());
+ DCHECK_EQ(resolved_field_ids.size(), metadata_index.dictionaries.size() *
path.size());
+ constexpr int64_t UNRESOLVED_FIELD_ID = -2;
+ return find_materialized_path_impl(
+ current, path,
+ [&](VariantRef object, StringRef key, size_t position, VariantRef*
found) {
+ int64_t& field_id = resolved_field_ids[dictionary_id *
path.size() + position];
+ bool layout_validated = false;
+ if (field_id == UNRESOLVED_FIELD_ID) {
+ // object_find() validates the object layout before
consulting metadata.
+ static_cast<void>(object.num_elements());
+ layout_validated = true;
+ field_id =
metadata_index.dictionaries[dictionary_id].find_key(key);
+ }
+ if (field_id < 0) {
+ // A cached metadata miss must not hide a corrupt object
in a later row.
+ if (!layout_validated) {
+ static_cast<void>(object.num_elements());
+ }
+ return false;
+ }
+ return
object.object_find_by_id(static_cast<uint32_t>(field_id), found);
Review Comment:
[P1] Validate the raw object layout before indexed lookup
This now looks up directly in the Parquet `metadata + value` bytes, before
`VariantBatchBuilder::validate_import_object()` has checked strict key ordering
and a complete non-overlapping partition of the object value region.
`object_find_by_id()` only validates the IDs it probes and the selected child's
encoded length. For example, a two-field object with IDs `[0,1]` and both
offsets set to zero makes both keys return the first scalar, whereas the former
reconstruction path rejects the overlap. Please establish those object-layout
invariants before using the cached ID, and cover
overlapping/gapped/out-of-order entries.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -611,6 +728,400 @@ bool find_materialized_path(VariantRef current,
std::span<const VariantShreddedP
return true;
}
+bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
+ VariantRef* output) {
+ return find_materialized_path_impl(
+ current, path,
+ [](VariantRef object, StringRef key, size_t, VariantRef* found) {
+ return object.object_find(key, found);
+ },
+ output);
+}
+
+bool find_materialized_path_with_index(VariantRef current, uint32_t
dictionary_id,
+ const UnshreddedMetadataIndex&
metadata_index,
+ std::span<const
VariantShreddedPathSegment> path,
+ DorisVector<int64_t>&
resolved_field_ids,
+ VariantRef* output) {
+ DCHECK_LT(dictionary_id, metadata_index.dictionaries.size());
+ DCHECK_EQ(resolved_field_ids.size(), metadata_index.dictionaries.size() *
path.size());
+ constexpr int64_t UNRESOLVED_FIELD_ID = -2;
+ return find_materialized_path_impl(
+ current, path,
+ [&](VariantRef object, StringRef key, size_t position, VariantRef*
found) {
+ int64_t& field_id = resolved_field_ids[dictionary_id *
path.size() + position];
+ bool layout_validated = false;
+ if (field_id == UNRESOLVED_FIELD_ID) {
+ // object_find() validates the object layout before
consulting metadata.
+ static_cast<void>(object.num_elements());
+ layout_validated = true;
+ field_id =
metadata_index.dictionaries[dictionary_id].find_key(key);
+ }
+ if (field_id < 0) {
+ // A cached metadata miss must not hide a corrupt object
in a later row.
+ if (!layout_validated) {
+ static_cast<void>(object.num_elements());
+ }
+ return false;
+ }
+ return
object.object_find_by_id(static_cast<uint32_t>(field_id), found);
+ },
+ output);
+}
+
+struct UnshreddedPathScan {
+ MutableColumnPtr outer_nulls;
+ MutableColumnPtr typed_values;
+ DataTypePtr typed_type;
+ std::shared_ptr<const UnshreddedPathCache> path_cache;
+ int64_t copied_bytes = 0;
+};
+
+enum class UnshreddedTypedKind : uint8_t { UNKNOWN, STRING, INTEGER,
UNSUPPORTED };
+
+class UnshreddedTypedValueBuilder {
+public:
+ UnshreddedTypedValueBuilder(size_t rows, const UnshreddedMetadataIndex&
metadata_index)
+ : _rows(rows),
+ _metadata_index(metadata_index),
+ _inner_nulls(ColumnUInt8::create()),
+ _result_nulls(ColumnUInt8::create()),
+ _validated_metadata(metadata_index.dictionaries.size(), 0) {
+ _inner_nulls->reserve(rows);
+ _result_nulls->reserve(rows);
+ }
+
+ void append_outer_null() { append_null(1); }
+
+ void append_json_null(uint32_t dictionary_id) {
+ if (_typed_kind == UnshreddedTypedKind::UNKNOWN) {
+ _pending_json_null_dictionaries.push_back(dictionary_id);
+ } else if (_typed_kind == UnshreddedTypedKind::INTEGER &&
+ !validate_integer_metadata(dictionary_id)) {
+ mark_unsupported();
+ }
+ append_null(0);
+ }
+
+ void append_scalar(const VariantRef& found, uint32_t dictionary_id, size_t
row) {
+ if (_typed_kind == UnshreddedTypedKind::UNSUPPORTED) {
+ append_null(0);
+ return;
+ }
+
+ const VariantBasicType basic_type = found.basic_type();
+ const bool is_string = basic_type == VariantBasicType::SHORT_STRING ||
+ (basic_type == VariantBasicType::PRIMITIVE &&
+ found.primitive_id() ==
VariantPrimitiveId::STRING);
+ if (is_string) {
+ if (!prepare(UnshreddedTypedKind::STRING, row)) {
+ append_null(0);
+ return;
+ }
+ const StringRef string = found.get_string();
Review Comment:
[P1] Preserve UTF-8 validation on the direct STRING path
`VariantRef::get_string()` validates the encoded length, but it does not
validate UTF-8. Copying it straight into a typed `ColumnString` therefore
accepts a malformed requested Variant string that the former
`VariantBatchBuilder` import rejected via `require_import_utf8()` /
`VariantScalarRef::string()`. This is observable without a later encode because
the typed Variant-to-string cast returns the physical string column directly.
Please validate the selected bytes before typed promotion and add an
invalid-UTF-8 direct-extraction case.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -758,6 +1344,11 @@ class ParquetVariantShreddedState final : public
VariantShreddedState {
return path_miss();
}
+ if (unshredded_child_indices(*_schema).has_value()) {
+ return VariantShreddedTypedValue {
+ .column = nullptr, .type = nullptr, .normalized =
direct_unshredded_path(path)};
Review Comment:
[P2] Avoid executing direct seek twice for composite states
This eagerly runs `direct_unshredded_path()` while
`CompositeVariantShreddedState` is only probing whether all segments have one
homogeneous typed representation. A mixed unshredded/partial-shredded composite
fails that homogeneity check because this result is normalized, then
`CompositeVariantShreddedState::find_normalized_value()` calls this segment's
`direct_unshredded_path()` again. Such mixed file layouts therefore scan every
row and allocate the result twice for one `element_at`. Please let the
composite reuse the normalized result it already obtained, or make this probe
non-materializing.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -611,6 +728,400 @@ bool find_materialized_path(VariantRef current,
std::span<const VariantShreddedP
return true;
}
+bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
+ VariantRef* output) {
+ return find_materialized_path_impl(
+ current, path,
+ [](VariantRef object, StringRef key, size_t, VariantRef* found) {
+ return object.object_find(key, found);
+ },
+ output);
+}
+
+bool find_materialized_path_with_index(VariantRef current, uint32_t
dictionary_id,
+ const UnshreddedMetadataIndex&
metadata_index,
+ std::span<const
VariantShreddedPathSegment> path,
+ DorisVector<int64_t>&
resolved_field_ids,
+ VariantRef* output) {
+ DCHECK_LT(dictionary_id, metadata_index.dictionaries.size());
+ DCHECK_EQ(resolved_field_ids.size(), metadata_index.dictionaries.size() *
path.size());
+ constexpr int64_t UNRESOLVED_FIELD_ID = -2;
+ return find_materialized_path_impl(
+ current, path,
+ [&](VariantRef object, StringRef key, size_t position, VariantRef*
found) {
+ int64_t& field_id = resolved_field_ids[dictionary_id *
path.size() + position];
+ bool layout_validated = false;
+ if (field_id == UNRESOLVED_FIELD_ID) {
+ // object_find() validates the object layout before
consulting metadata.
+ static_cast<void>(object.num_elements());
+ layout_validated = true;
+ field_id =
metadata_index.dictionaries[dictionary_id].find_key(key);
+ }
+ if (field_id < 0) {
+ // A cached metadata miss must not hide a corrupt object
in a later row.
+ if (!layout_validated) {
+ static_cast<void>(object.num_elements());
+ }
+ return false;
+ }
+ return
object.object_find_by_id(static_cast<uint32_t>(field_id), found);
+ },
+ output);
+}
+
+struct UnshreddedPathScan {
+ MutableColumnPtr outer_nulls;
+ MutableColumnPtr typed_values;
+ DataTypePtr typed_type;
+ std::shared_ptr<const UnshreddedPathCache> path_cache;
+ int64_t copied_bytes = 0;
+};
+
+enum class UnshreddedTypedKind : uint8_t { UNKNOWN, STRING, INTEGER,
UNSUPPORTED };
+
+class UnshreddedTypedValueBuilder {
+public:
+ UnshreddedTypedValueBuilder(size_t rows, const UnshreddedMetadataIndex&
metadata_index)
+ : _rows(rows),
+ _metadata_index(metadata_index),
+ _inner_nulls(ColumnUInt8::create()),
+ _result_nulls(ColumnUInt8::create()),
+ _validated_metadata(metadata_index.dictionaries.size(), 0) {
+ _inner_nulls->reserve(rows);
+ _result_nulls->reserve(rows);
+ }
+
+ void append_outer_null() { append_null(1); }
+
+ void append_json_null(uint32_t dictionary_id) {
+ if (_typed_kind == UnshreddedTypedKind::UNKNOWN) {
+ _pending_json_null_dictionaries.push_back(dictionary_id);
+ } else if (_typed_kind == UnshreddedTypedKind::INTEGER &&
+ !validate_integer_metadata(dictionary_id)) {
+ mark_unsupported();
+ }
+ append_null(0);
+ }
+
+ void append_scalar(const VariantRef& found, uint32_t dictionary_id, size_t
row) {
+ if (_typed_kind == UnshreddedTypedKind::UNSUPPORTED) {
+ append_null(0);
+ return;
+ }
+
+ const VariantBasicType basic_type = found.basic_type();
+ const bool is_string = basic_type == VariantBasicType::SHORT_STRING ||
+ (basic_type == VariantBasicType::PRIMITIVE &&
+ found.primitive_id() ==
VariantPrimitiveId::STRING);
+ if (is_string) {
+ if (!prepare(UnshreddedTypedKind::STRING, row)) {
+ append_null(0);
+ return;
+ }
+ const StringRef string = found.get_string();
+
assert_cast<ColumnString&>(*_typed_values).insert_data(string.data,
string.size);
+ _inner_nulls->insert_value(0);
+ _result_nulls->insert_value(0);
+ DCHECK_LE(string.size,
+ static_cast<size_t>(std::numeric_limits<int64_t>::max()
- _copied_bytes));
+ _copied_bytes += static_cast<int64_t>(string.size);
+ return;
+ }
+
+ const auto primitive_id = basic_type == VariantBasicType::PRIMITIVE
+ ? found.primitive_id()
+ : VariantPrimitiveId::NULL_VALUE;
+ const bool is_integer = primitive_id == VariantPrimitiveId::INT8 ||
+ primitive_id == VariantPrimitiveId::INT16 ||
+ primitive_id == VariantPrimitiveId::INT32 ||
+ primitive_id == VariantPrimitiveId::INT64;
+ const int64_t integer = is_integer ? found.get_int() : 0;
+ // Typed Variant integers are re-encoded using the narrowest width.
Keep explicitly widened
+ // source integers on the encoded path so observable physical types
remain unchanged.
+ const bool has_canonical_width =
+ is_integer &&
VariantScalarRef::integer(integer).encoded_size() == found.value.size;
+ if (!has_canonical_width || !validate_integer_metadata(dictionary_id)
||
Review Comment:
[P1] Validate selected unsupported scalars before publishing a subtree
When the selected value is neither a string nor a canonical integer, this
branch marks the result unsupported without invoking the primitive's semantic
accessor. An exactly bounded `DECIMAL16` with scale 39 thus becomes a non-null
lazy shredded value; `element_at(...) IS NOT NULL` reads only the returned null
map and succeeds, whereas the former reconstruction called `get_decimal()` /
`import_primitive()` and rejected it. Please validate the selected scalar
before deferring its representation, and add invalid decimal scale/magnitude
tests with a null-only consumer.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -611,6 +728,400 @@ bool find_materialized_path(VariantRef current,
std::span<const VariantShreddedP
return true;
}
+bool find_materialized_path(VariantRef current, std::span<const
VariantShreddedPathSegment> path,
+ VariantRef* output) {
+ return find_materialized_path_impl(
+ current, path,
+ [](VariantRef object, StringRef key, size_t, VariantRef* found) {
+ return object.object_find(key, found);
+ },
+ output);
+}
+
+bool find_materialized_path_with_index(VariantRef current, uint32_t
dictionary_id,
+ const UnshreddedMetadataIndex&
metadata_index,
+ std::span<const
VariantShreddedPathSegment> path,
+ DorisVector<int64_t>&
resolved_field_ids,
+ VariantRef* output) {
+ DCHECK_LT(dictionary_id, metadata_index.dictionaries.size());
+ DCHECK_EQ(resolved_field_ids.size(), metadata_index.dictionaries.size() *
path.size());
+ constexpr int64_t UNRESOLVED_FIELD_ID = -2;
+ return find_materialized_path_impl(
+ current, path,
+ [&](VariantRef object, StringRef key, size_t position, VariantRef*
found) {
+ int64_t& field_id = resolved_field_ids[dictionary_id *
path.size() + position];
+ bool layout_validated = false;
+ if (field_id == UNRESOLVED_FIELD_ID) {
+ // object_find() validates the object layout before
consulting metadata.
+ static_cast<void>(object.num_elements());
+ layout_validated = true;
+ field_id =
metadata_index.dictionaries[dictionary_id].find_key(key);
+ }
+ if (field_id < 0) {
+ // A cached metadata miss must not hide a corrupt object
in a later row.
+ if (!layout_validated) {
+ static_cast<void>(object.num_elements());
+ }
+ return false;
+ }
+ return
object.object_find_by_id(static_cast<uint32_t>(field_id), found);
+ },
+ output);
+}
+
+struct UnshreddedPathScan {
+ MutableColumnPtr outer_nulls;
+ MutableColumnPtr typed_values;
+ DataTypePtr typed_type;
+ std::shared_ptr<const UnshreddedPathCache> path_cache;
+ int64_t copied_bytes = 0;
+};
+
+enum class UnshreddedTypedKind : uint8_t { UNKNOWN, STRING, INTEGER,
UNSUPPORTED };
+
+class UnshreddedTypedValueBuilder {
+public:
+ UnshreddedTypedValueBuilder(size_t rows, const UnshreddedMetadataIndex&
metadata_index)
+ : _rows(rows),
+ _metadata_index(metadata_index),
+ _inner_nulls(ColumnUInt8::create()),
+ _result_nulls(ColumnUInt8::create()),
+ _validated_metadata(metadata_index.dictionaries.size(), 0) {
+ _inner_nulls->reserve(rows);
+ _result_nulls->reserve(rows);
+ }
+
+ void append_outer_null() { append_null(1); }
+
+ void append_json_null(uint32_t dictionary_id) {
+ if (_typed_kind == UnshreddedTypedKind::UNKNOWN) {
+ _pending_json_null_dictionaries.push_back(dictionary_id);
+ } else if (_typed_kind == UnshreddedTypedKind::INTEGER &&
+ !validate_integer_metadata(dictionary_id)) {
+ mark_unsupported();
+ }
+ append_null(0);
+ }
+
+ void append_scalar(const VariantRef& found, uint32_t dictionary_id, size_t
row) {
+ if (_typed_kind == UnshreddedTypedKind::UNSUPPORTED) {
+ append_null(0);
+ return;
+ }
+
+ const VariantBasicType basic_type = found.basic_type();
+ const bool is_string = basic_type == VariantBasicType::SHORT_STRING ||
+ (basic_type == VariantBasicType::PRIMITIVE &&
+ found.primitive_id() ==
VariantPrimitiveId::STRING);
+ if (is_string) {
+ if (!prepare(UnshreddedTypedKind::STRING, row)) {
+ append_null(0);
+ return;
+ }
+ const StringRef string = found.get_string();
+
assert_cast<ColumnString&>(*_typed_values).insert_data(string.data,
string.size);
+ _inner_nulls->insert_value(0);
+ _result_nulls->insert_value(0);
+ DCHECK_LE(string.size,
+ static_cast<size_t>(std::numeric_limits<int64_t>::max()
- _copied_bytes));
+ _copied_bytes += static_cast<int64_t>(string.size);
+ return;
+ }
+
+ const auto primitive_id = basic_type == VariantBasicType::PRIMITIVE
+ ? found.primitive_id()
+ : VariantPrimitiveId::NULL_VALUE;
+ const bool is_integer = primitive_id == VariantPrimitiveId::INT8 ||
+ primitive_id == VariantPrimitiveId::INT16 ||
+ primitive_id == VariantPrimitiveId::INT32 ||
+ primitive_id == VariantPrimitiveId::INT64;
+ const int64_t integer = is_integer ? found.get_int() : 0;
+ // Typed Variant integers are re-encoded using the narrowest width.
Keep explicitly widened
+ // source integers on the encoded path so observable physical types
remain unchanged.
+ const bool has_canonical_width =
+ is_integer &&
VariantScalarRef::integer(integer).encoded_size() == found.value.size;
+ if (!has_canonical_width || !validate_integer_metadata(dictionary_id)
||
+ !prepare(UnshreddedTypedKind::INTEGER, row)) {
+ mark_unsupported();
Review Comment:
[P1] Validate selected containers before returning a lazy subtree
This unsupported fallback can publish a selected array before validating its
child boundaries. For example, an otherwise valid parent can contain a
two-element array with offsets `[0,1,1]` and one NULL byte: its declared outer
size is exact, but the second `array_at()` is invalid. Selecting that array
produces a non-null shredded value, so `element_at(root, 'array') IS NOT NULL`
returns true; the former `VariantBatchBuilder::import_array()` rejected it.
Please validate the selected container before deferring it, and cover malformed
interior offsets with a null-only consumer.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -902,6 +1515,103 @@ class ParquetVariantShreddedState final : public
VariantShreddedState {
}
private:
+ template <typename PathCacheSelector>
+ std::shared_ptr<ParquetVariantShreddedState> select_state(
+ ColumnPtr selected_physical, const PathCacheSelector&
select_path_cache) const {
+ std::shared_ptr<const UnshreddedPathCache> path_cache;
+ {
+ std::lock_guard lock(_materialization_lock);
+ path_cache = _unshredded_path_cache;
+ }
+
+ std::shared_ptr<const UnshreddedPathCache> selected_path_cache;
+ if (path_cache) {
+ selected_path_cache = select_path_cache(*path_cache);
+ }
+ return std::make_shared<ParquetVariantShreddedState>(
+ _schema, std::move(selected_physical), _complete, _profile,
_unshredded_prefix,
+ std::move(selected_path_cache));
+ }
+
+ std::vector<OwnedShreddedPathSegment> combined_unshredded_path(
+ std::span<const VariantShreddedPathSegment> suffix) const {
+ std::vector<OwnedShreddedPathSegment> combined = _unshredded_prefix;
+ combined.reserve(combined.size() + suffix.size());
+ for (const auto& segment : suffix) {
+ combined.push_back(
+ {.kind = segment.kind,
+ .key = segment.kind ==
VariantShreddedPathSegment::Kind::OBJECT_KEY
+ ? (segment.key.size == 0
+ ? std::string()
+ : std::string(segment.key.data,
segment.key.size))
+ : std::string(),
+ .index = segment.index});
+ }
+ return combined;
+ }
+
+ static std::vector<VariantShreddedPathSegment> borrow_unshredded_path(
+ const std::vector<OwnedShreddedPathSegment>& owned) {
+ std::vector<VariantShreddedPathSegment> borrowed;
+ borrowed.reserve(owned.size());
+ for (const auto& segment : owned) {
+ borrowed.push_back({.kind = segment.kind,
+ .key = {segment.key.data(),
segment.key.size()},
+ .index = segment.index});
+ }
+ return borrowed;
+ }
+
+ std::shared_ptr<const UnshreddedMetadataIndex>
get_unshredded_metadata_index(
+ std::pair<size_t, size_t> child_indices) const {
+ std::lock_guard lock(_materialization_lock);
+ if (!_unshredded_metadata_index) {
+ _unshredded_metadata_index =
+ build_unshredded_metadata_index(*_schema, *_physical,
child_indices);
+ }
+ return _unshredded_metadata_index;
+ }
+
+ ColumnPtr direct_unshredded_path(std::span<const
VariantShreddedPathSegment> suffix) const {
+ const auto child_indices = unshredded_child_indices(*_schema);
+ DORIS_CHECK(child_indices.has_value());
+ std::vector<OwnedShreddedPathSegment> combined =
combined_unshredded_path(suffix);
+ const auto borrowed_combined = borrow_unshredded_path(combined);
+ std::shared_ptr<const UnshreddedMetadataIndex> metadata_index;
+ UnshreddedPathScan scan;
+ {
+ SCOPED_TIMER(_profile.variant_unshredded_direct_seek_time.get());
+ metadata_index = get_unshredded_metadata_index(*child_indices);
+ scan = scan_unshredded_path(
+ *_schema, *_physical, *child_indices, *metadata_index,
+ _unshredded_path_cache
+ ? suffix
+ : std::span<const
VariantShreddedPathSegment>(borrowed_combined),
+ _unshredded_path_cache.get());
+ }
+
+ const auto rows = static_cast<int64_t>(_physical->size());
+ update_counter(_profile.variant_unshredded_direct_seek_rows, rows);
+ if (_unshredded_path_cache) {
+ update_counter(_profile.variant_unshredded_prefix_reuse_rows,
rows);
+ }
+ MutableColumnPtr values;
+ std::shared_ptr<ParquetVariantShreddedState> subtree_state;
+ if (scan.typed_values) {
+ values =
ColumnVariantV2::create_typed(std::move(scan.typed_values),
+ std::move(scan.typed_type));
+ update_counter(_profile.variant_unshredded_direct_seek_bytes,
scan.copied_bytes);
+ update_counter(_profile.variant_direct_leaf_rows, rows);
+ } else {
+ subtree_state = std::make_shared<ParquetVariantShreddedState>(
Review Comment:
[P2] Reuse the metadata index across chained subtree states
This subtree retains the identical immutable `_physical` column and already
carries the prefix-offset cache, but it drops the metadata index that was just
built for the scan. The next chained `element_at` therefore walks every row and
compares/hashes the complete metadata bytes again before it can use the prefix,
making a depth-D chain O(D*R*M) in rows and metadata bytes and allocating a
fresh row-ID vector at each step. Please pass the shared index into states that
keep the same physical identity; filter/cut/gather can still rebuild it and
append can reset it.
##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -878,6 +1473,24 @@ class ParquetVariantShreddedState final : public
VariantShreddedState {
ErrorCode::INTERNAL_ERROR,
"A projected Parquet Variant can only serve its validated
shredded leaves");
}
+ if (!_unshredded_prefix.empty()) {
+ if (!_normalized_prefix) {
+ const auto child_indices = unshredded_child_indices(*_schema);
+ DORIS_CHECK(child_indices.has_value());
+ const auto borrowed =
borrow_unshredded_path(_unshredded_prefix);
+ int64_t copied_bytes = 0;
+ {
+
SCOPED_TIMER(_profile.variant_unshredded_direct_seek_time.get());
+ _normalized_prefix = normalize_unshredded_path(
Review Comment:
[P2] Consume the cached offsets when materializing this subtree
`direct_unshredded_path()` already computed and retained each selected
value's exact root-relative offset and size. This call ignores that cache and
starts from every raw root again, re-walking the complete accumulated prefix
before it can copy the same values. A depth-D chain ending in a
BINARY/BOOL/DECIMAL/container/mixed value therefore adds redundant O(R*D)
lookup work when a cast, serialization, hash, or `get_value_ref()` forces
materialization. Please normalize directly from `_unshredded_path_cache` (while
applying the selected-value validators) and add a test or counter proving the
prefix is not resolved twice.
--
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]