github-actions[bot] commented on code in PR #66758:
URL: https://github.com/apache/doris/pull/66758#discussion_r3796685093


##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2008,17 +2008,48 @@ static Status build_scan_projection(ColumnMapping* 
mapping, bool force_full_comp
     return Status::OK();
 }
 
+static void merge_variant_access_paths(VariantAccessPaths* target,
+                                       const VariantAccessPaths& source) {
+    DORIS_CHECK(target != nullptr);
+    target->insert(target->end(), source.begin(), source.end());
+    std::ranges::sort(*target);
+    target->erase(std::unique(target->begin(), target->end()), target->end());
+}
+
+static void register_root_variant_access_paths(FileScanRequest* file_request,
+                                               const ColumnMapping& mapping,
+                                               bool is_predicate_column) {
+    DORIS_CHECK(file_request != nullptr);
+    if (!mapping.file_local_id.has_value() || mapping.table_type == nullptr ||
+        mapping.variant_access_paths.empty() ||

Review Comment:
   [P2] Carry access paths to nested Variant terminals
   
   Nested Variant paths are preserved on recursive child mappings, but this 
root-only registration returns for a surrounding STRUCT/ARRAY/MAP, and 
recursive `build_variant_plan()` calls receive no path metadata. A nested 
unshredded Variant therefore sees an empty plan and bypasses the new multi-path 
materialization fallback, so queries such as `info.payload['x']` plus 
`info.payload['deep']['name']` still seek and re-encode every row once per 
expression. Please represent/pass paths per nested Variant terminal and add 
STRUCT/ARRAY/MAP multi-path coverage. This is distinct from the live top-level 
multi-path thread because that fix is never wired to nested terminals.



##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -514,6 +516,323 @@ void import_unshredded_variant_range(const 
ParquetColumnSchema& schema, const Co
     appender.append(std::span<const VariantRef>(encoded_rows));
 }
 
+struct DirectResidualSeekResult {
+    ColumnPtr column;
+    int64_t rows = 0;
+    int64_t selected_value_bytes = 0;
+    int64_t container_index_builds = 0;
+    int64_t container_index_hits = 0;
+};
+
+struct DirectSeekContainerKey {
+    const char* metadata_data = nullptr;
+    size_t metadata_size = 0;
+    const char* value_data = nullptr;
+    size_t value_size = 0;
+
+    bool operator==(const DirectSeekContainerKey&) const = default;
+};
+
+struct DirectSeekContainerKeyHash {
+    size_t operator()(const DirectSeekContainerKey& key) const noexcept {
+        size_t hash = std::hash<const void*> {}(key.metadata_data);
+        auto combine = [&](size_t value) {
+            hash ^= value + 0x9e3779b97f4a7c15ULL + (hash << 6) + (hash >> 2);
+        };
+        combine(std::hash<size_t> {}(key.metadata_size));
+        combine(std::hash<const void*> {}(key.value_data));
+        combine(std::hash<size_t> {}(key.value_size));
+        return hash;
+    }
+};
+
+class DirectSeekContainerCache {
+public:
+    struct Lookup {
+        VariantContainerLookup* value;
+        bool cache_hit;
+    };
+
+    Lookup find_or_build(VariantRef value, size_t path_position, 
DirectResidualSeekResult& result,
+                         std::optional<VariantContainerLookup>& 
uncached_lookup) {
+        const DirectSeekContainerKey key {.metadata_data = value.metadata.data,
+                                          .metadata_size = value.metadata.size,
+                                          .value_data = value.value.data,
+                                          .value_size = value.value.size};
+        if (auto found = _entries.find(key); found != _entries.end()) {
+            ++result.container_index_hits;
+            return {.value = &found->second, .cache_hit = true};
+        }
+        ++result.container_index_builds;
+        if (path_position >= MAX_RETAINED_PATH_DEPTH || _entries.size() >= 
MAX_RETAINED_ENTRIES) {
+            // Validation is still mandatory after the high-water mark. Keep 
only this traversal's
+            // lookup alive so an adversarial rows-times-depth path cannot 
grow persistent state.
+            uncached_lookup.emplace(value);
+            return {.value = &*uncached_lookup, .cache_hit = false};
+        }
+        auto [inserted, was_inserted] = _entries.try_emplace(key, value);
+        DORIS_CHECK(was_inserted);
+        return {.value = &inserted->second, .cache_hit = false};
+    }
+
+    bool object_find_by_id(Lookup lookup, int64_t field_id, VariantRef* out) {
+        size_t promoted_bytes = 0;
+        const size_t promotion_budget =
+                lookup.cache_hit ? MAX_PROMOTED_OFFSET_BYTES - 
_promoted_offset_bytes : 0;
+        const bool found =
+                lookup.value->object_find_by_id(field_id, out, 
promotion_budget, &promoted_bytes);
+        _promoted_offset_bytes += promoted_bytes;
+        return found;
+    }
+
+    void reserve(size_t size) {
+        if (_entries.empty()) {
+            const size_t retained = size > MAX_RETAINED_ENTRIES / 
MAX_RETAINED_PATH_DEPTH
+                                            ? MAX_RETAINED_ENTRIES
+                                            : size * MAX_RETAINED_PATH_DEPTH;
+            _entries.reserve(retained);
+        }
+    }
+
+    void reset() {
+        ContainerMap empty;
+        _entries.swap(empty);
+        _promoted_offset_bytes = 0;
+    }
+
+    size_t allocated_bytes() const {
+        return _entries.bucket_count() * sizeof(void*) +
+               _entries.size() * sizeof(ContainerMap::value_type) + 
_promoted_offset_bytes;
+    }
+
+private:
+    // Four retained ancestors cover the common root/nested-object projections 
for a default
+    // scanner batch. The independent entry and promoted-offset caps keep 
state bounded for deep
+    // paths, appended columns, and noncanonical wide objects.
+    static constexpr size_t MAX_RETAINED_PATH_DEPTH = 4;
+    static constexpr size_t MAX_RETAINED_ENTRIES = 16 * 1024;
+    static constexpr size_t MAX_PROMOTED_OFFSET_BYTES = 4 * 1024 * 1024;
+
+    using ContainerMap = std::unordered_map<
+            DirectSeekContainerKey, VariantContainerLookup, 
DirectSeekContainerKeyHash,
+            std::equal_to<DirectSeekContainerKey>,
+            CustomStdAllocator<std::pair<const DirectSeekContainerKey, 
VariantContainerLookup>>>;
+    ContainerMap _entries;
+    size_t _promoted_offset_bytes = 0;
+};
+
+struct UnshreddedMetadataCache {
+    static constexpr uint32_t NO_METADATA = 
std::numeric_limits<uint32_t>::max();
+
+    DorisVector<VariantMetadataRef> metadatas;
+    DorisVector<uint32_t> row_metadata_ids;
+
+    size_t allocated_bytes() const {
+        return metadatas.capacity() * sizeof(VariantMetadataRef) +
+               row_metadata_ids.capacity() * sizeof(uint32_t);
+    }
+};
+
+std::shared_ptr<const UnshreddedMetadataCache> build_unshredded_metadata_cache(
+        const ParquetColumnSchema& schema, const IColumn& physical, size_t 
metadata_index) {
+    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);
+    if (structure.tuple_size() != schema.children.size()) {
+        throw Exception(ErrorCode::CORRUPTION, "Parquet Variant {} physical 
field count mismatch",
+                        schema.name);
+    }
+
+    using MetadataIndex =
+            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>>>;
+    auto cache = std::make_shared<UnshreddedMetadataCache>();
+    cache->row_metadata_ids.resize(physical.size(), 
UnshreddedMetadataCache::NO_METADATA);
+    MetadataIndex metadata_index_by_value;
+    std::optional<uint32_t> previous_metadata_id;
+
+    auto same_metadata = [&](uint32_t id, StringRef bytes) {
+        const VariantMetadataRef cached = cache->metadatas[id];
+        return StringRef(cached.data, cached.size) == bytes;
+    };
+    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 = 
cell_at(structure.get_column(metadata_index), row);
+        if (metadata_cell.is_null) {
+            throw Exception(ErrorCode::CORRUPTION, "Parquet Variant {} has 
null metadata at row {}",
+                            schema.name, row);
+        }
+        const StringRef metadata_bytes = 
metadata_cell.column->get_data_at(row);
+        if (previous_metadata_id.has_value() &&
+            same_metadata(*previous_metadata_id, metadata_bytes)) {
+            cache->row_metadata_ids[row] = *previous_metadata_id;
+            continue;
+        }
+
+        const std::string_view key(metadata_bytes.data == nullptr ? "" : 
metadata_bytes.data,
+                                   metadata_bytes.size);
+        if (!metadata_index_by_value.empty()) {
+            if (const auto found = metadata_index_by_value.find(key);
+                found != metadata_index_by_value.end()) {
+                cache->row_metadata_ids[row] = found->second;
+                previous_metadata_id = found->second;
+                continue;
+            }
+        }
+
+        // Parquet requires every Variant metadata/value pair to be 
spec-valid. This direct-seek
+        // cache therefore only deduplicates the immutable metadata bytes 
instead of eagerly
+        // walking every dictionary key. The bounded find_key()/key_at() 
accessors still reject
+        // truncated layouts before any referenced key is observed.
+        VariantMetadataRef metadata {.data = metadata_bytes.data, .size = 
metadata_bytes.size};

Review Comment:
   [P1] Validate metadata before trusting the sorted flag
   
   This cache stores each unique metadata blob without validation, but path 
preparation immediately calls `find_key()`, which binary-searches whenever the 
header merely advertises sorted strings. For example, metadata keys `['z','a']` 
marked sorted plus a root object id table `[1,0]` is locally ordered as `a,z`, 
yet lookup of `z` returns `-1` and this path emits SQL NULL even though the key 
is present. The previous materialization path rejected the same row through 
`validate_variant_metadata()`. Please validate each unique dictionary before 
using its ordering/uniqueness claims and cover a falsely advertised sorted 
dictionary. This is distinct from the existing root-payload/depth threads 
because the wrong result comes from the metadata search invariant.



##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -527,12 +527,66 @@ class CompositeVariantShreddedState final : public 
VariantShreddedState {
                                               .normalized = nullptr};
         }
 
-        auto normalized = find_normalized_value(path);
-        if (!normalized.has_value()) {
-            return std::nullopt;
+        const bool all_direct_values =
+                all_direct && std::ranges::all_of(matches, [](const auto& 
match) {
+                    const bool typed = match.column && match.type && 
!match.normalized;
+                    const bool normalized = match.normalized && !match.column 
&& !match.type;
+                    return typed || normalized;
+                });
+        if (all_direct_values) {
+            auto values = ColumnVariantV2::create();
+            auto nulls = ColumnUInt8::create();
+            nulls->reserve(size());
+            for (size_t index = 0; index < matches.size(); ++index) {
+                const auto& match = matches[index];
+                std::optional<ColumnPtr> normalized_typed;
+                if (!match.normalized) {
+                    // Typed Parquet leaves need their segment's schema-aware 
normalization to
+                    // preserve physical distinctions such as INT32 versus 
INT64. Normalize only
+                    // those segments; a normalized unshredded match already 
contains exact bytes.
+                    normalized_typed = 
_segments[index]->find_normalized_value(path);
+                    if (!normalized_typed.has_value()) {
+                        return std::nullopt;
+                    }
+                }
+                const ColumnPtr& matched = match.normalized ? match.normalized 
: *normalized_typed;
+                const auto& nullable = assert_cast<const 
ColumnNullable&>(*matched);
+                nulls->insert_range_from(nullable.get_null_map_column(), 0, 
nullable.size());
+                const auto& variants =
+                        assert_cast<const 
ColumnVariantV2&>(nullable.get_nested_column());
+                values->insert_range_from(variants, 0, variants.size());
+            }
+            return VariantShreddedTypedValue {
+                    .column = nullptr,
+                    .type = nullptr,
+                    .normalized = ColumnNullable::create(std::move(values), 
std::move(nulls))};
+        }
+
+        auto values = ColumnVariantV2::create();
+        auto nulls = ColumnUInt8::create();
+        nulls->reserve(size());
+        for (size_t index = 0; index < _segments.size(); ++index) {
+            std::optional<ColumnPtr> normalized;
+            if (index < matches.size() && matches[index].normalized) {
+                // Keep normalized prefix matches that were completed before a 
later segment
+                // requested fallback. In particular, do not seek an 
unshredded segment twice.
+                normalized = matches[index].normalized;
+            } else {
+                normalized = _segments[index]->find_normalized_value(path);

Review Comment:
   [P2] Count mixed residual fallbacks reached through normalization
   
   After an earlier segment misses, this loop sends every unvisited segment 
directly to `find_normalized_value()`. A later complete mixed Parquet segment 
then reconstructs through `materialized_column()`, but 
`VariantDirectResidualSeekFallbacks` is incremented only by that segment's 
skipped `find_typed_value()` fallback. Consequently `[partial miss, complete 
mixed]` reports zero while the reversed segment order reports one for the same 
reconstruction work, contrary to the new counter contract. Please count the 
complete-mixed-to-materialized transition in one common place and test both 
segment orders. This is separate from the old cached rows/bytes comment because 
it concerns the residual fallback counter and composite branch.



-- 
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]

Reply via email to