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


##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -1431,58 +1668,89 @@ class ParquetVariantShreddedState final : public 
VariantShreddedState {
             const ParquetColumnSchema* wrapper_schema = nullptr;
             ColumnPtr wrapper = struct_child(*typed_schema, typed, key, 
&wrapper_schema);
             if (!wrapper) {
-                return path_miss();
+                return seek_residual();
+            }
+            ColumnPtr residual = struct_child(*wrapper_schema, wrapper, 
"value", nullptr);
+            typed = struct_child(*wrapper_schema, wrapper, "typed_value", 
&typed_schema);
+            if (!typed) {
+                // The wrapper stores this field unshredded. Its residual 
holds the whole value, so
+                // the remainder of the path resolves inside those bytes.
+                add_level(std::move(residual), nullptr);
+                return seek_residual();
+            }
+            add_level(std::move(residual), typed);
+
+            if (position + 1 < path.size()) {

Review Comment:
   [P1] Reject conflicting carriers on the nonterminal primitive branch
   
   For a path such as `b.c`, this branch records both `b.typed_value` and 
`b.value` and then calls `seek_residual()`. If both are non-null, 
`resolve_variant_path_rows()` forces the deepest level when no leaf schema 
exists and returns `c` from `b.value`; canonical reconstruction instead reaches 
`append_typed_value()` and rejects every primitive `typed_value` plus residual 
pair as corruption. The existing terminal-carrier thread does not cover this 
nonterminal branch. Please validate the pair before seeking the residual and 
add a direct-vs-canonical `b.c` test with both carriers present.



##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -1211,6 +1220,191 @@ ColumnPtr normalize_materialized_path(const 
ColumnVariantV2& materialized,
     return ColumnNullable::create(std::move(values), std::move(nulls));
 }
 
+// Column-level access to one optional Parquet column. Caching the null map 
beside the values lets
+// the row loops resolve a path without repeating the ColumnNullable cast for 
every row.
+struct NullableColumnView {
+    const IColumn* values = nullptr;
+    const uint8_t* nulls = nullptr;
+
+    bool present() const { return values != nullptr; }
+    bool is_null(size_t row) const {
+        return values == nullptr || (nulls != nullptr && nulls[row] != 0);
+    }
+};
+
+NullableColumnView view_column(const ColumnPtr& column) {
+    if (!column) {
+        return {};
+    }
+    if (const auto* nullable = check_and_get_column<ColumnNullable>(*column)) {
+        return {.values = &nullable->get_nested_column(),
+                .nulls = nullable->get_null_map_data().data()};
+    }
+    return {.values = column.get(), .nulls = nullptr};
+}
+
+// One `value`/`typed_value` pair along a shredded object path. Level 0 is the 
Variant root and
+// level i + 1 belongs to path[i], so the residual at level i encodes the 
value of that path prefix
+// for every row whose typed value beside it is null.
+struct ShreddedPathLevel {
+    ColumnPtr residual;
+    ColumnPtr typed;
+    NullableColumnView residual_view;
+    NullableColumnView typed_view;
+};
+
+// A residual can only supply the requested path for rows whose typed value is 
null. Shredding keeps
+// an object's residual keys disjoint from its shredded fields, so a present 
typed value always owns
+// the path and the residual beside it holds unrelated keys.
+bool residual_supplies_any_row(const ShreddedPathLevel& level, size_t rows) {
+    if (!level.residual_view.present()) {
+        return false;
+    }
+    for (size_t row = 0; row < rows; ++row) {
+        if (level.typed_view.is_null(row) && 
!level.residual_view.is_null(row)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+// Seeks object paths inside encoded residual bytes. Keys resolve to a 
dictionary field id once per
+// distinct dictionary instead of once per row, and the dictionary index is 
built only when a path
+// still has keys to resolve - a residual that already is the requested value 
needs no dictionary.
+class VariantResidualSeeker {
+public:
+    using MetadataIndexProvider = std::function<const 
UnshreddedMetadataIndex&()>;
+
+    VariantResidualSeeker(const NullableColumnView& metadata, size_t 
path_length,
+                          MetadataIndexProvider provider)
+            : _metadata(metadata), _path_length(path_length), 
_provider(std::move(provider)) {}
+
+    // Resolves path[path_offset..] inside `value`. The offset keeps the cache 
keyed by absolute
+    // path position, because different rows can enter the residual at 
different depths.
+    bool seek(size_t row, StringRef value, size_t path_offset,
+              std::span<const VariantShreddedPathSegment> path, VariantRef* 
output) {
+        DORIS_CHECK(output != nullptr);
+        if (path.empty()) {
+            // The residual already is the requested value; only its 
dictionary travels with it.
+            if (_metadata.is_null(row)) {
+                return false;
+            }
+            const StringRef bytes = _metadata.values->get_data_at(row);
+            *output = VariantRef {.metadata = {.data = bytes.data, .size = 
bytes.size},
+                                  .value = value};
+            return true;
+        }
+
+        const UnshreddedMetadataIndex& index = _index == nullptr ? 
_resolve_index() : *_index;
+        const uint32_t dictionary = index.row_dictionary_ids[row];
+        if (dictionary == UnshreddedMetadataIndex::NULL_ROW) {
+            return false;
+        }
+        VariantRef current {.metadata = index.dictionaries[dictionary], .value 
= value};
+        for (size_t position = 0; position < path.size(); ++position) {
+            if (path[position].kind != 
VariantShreddedPathSegment::Kind::OBJECT_KEY) {
+                // Array segments never reach a residual: a shredded array 
lives in its typed_value,
+                // and the caller keeps those paths on the reconstruction path.
+                return false;
+            }
+            if (current.basic_type() != VariantBasicType::OBJECT) {
+                return false;
+            }
+            int64_t& field_id = _field_ids[dictionary * _path_length + 
path_offset + position];
+            bool layout_validated = false;
+            if (field_id == UNRESOLVED_FIELD_ID) {
+                // object_find() validates the object layout before consulting 
the dictionary, so
+                // resolving a key has to validate it here too.
+                static_cast<void>(current.num_elements());
+                layout_validated = true;
+                field_id = current.metadata.find_key(path[position].key);
+            }
+            if (field_id < 0) {
+                // A cached dictionary miss must not hide a corrupt object in 
a later row.
+                if (!layout_validated) {
+                    static_cast<void>(current.num_elements());
+                }
+                return false;
+            }
+            if (!current.object_find_by_id(static_cast<uint32_t>(field_id), 
&current)) {

Review Comment:
   [P1] Preserve residual corruption checks on successful lookup
   
   `object_find_by_id()` validates only field IDs visited by its binary search 
and the selected child. A residual object can therefore contain a valid 
requested key plus an unrelated member with an out-of-range field ID or 
malformed value, and this fast path returns the requested value; canonical 
reconstruction iterates every residual member through `object_value_at()` and 
rejects the same row. This is distinct from the metadata-miss thread because 
metadata is valid and lookup succeeds. Please validate the complete residual 
object before publishing a hit or miss (cached once per row if needed), with a 
differential corrupt-sibling test.



##########
be/src/format_v2/parquet/reader/variant_column_reader.cpp:
##########
@@ -1431,58 +1668,89 @@ class ParquetVariantShreddedState final : public 
VariantShreddedState {
             const ParquetColumnSchema* wrapper_schema = nullptr;
             ColumnPtr wrapper = struct_child(*typed_schema, typed, key, 
&wrapper_schema);
             if (!wrapper) {
-                return path_miss();
+                return seek_residual();
+            }
+            ColumnPtr residual = struct_child(*wrapper_schema, wrapper, 
"value", nullptr);
+            typed = struct_child(*wrapper_schema, wrapper, "typed_value", 
&typed_schema);
+            if (!typed) {
+                // The wrapper stores this field unshredded. Its residual 
holds the whole value, so
+                // the remainder of the path resolves inside those bytes.
+                add_level(std::move(residual), nullptr);
+                return seek_residual();
+            }
+            add_level(std::move(residual), typed);
+
+            if (position + 1 < path.size()) {
+                // An intermediate typed_value that is not an object cannot 
carry the rest of the
+                // path, so those rows resolve from the residual of this very 
wrapper - the object
+                // form of this field is what did not match its shredded type. 
The level just added
+                // is that residual; dropping it would search the parent 
instead, where shredding
+                // guarantees this key can never appear.
+                if (typed_schema->kind != ParquetColumnSchemaKind::STRUCT) {
+                    return seek_residual();
+                }
+                continue;
             }
 
-            if (ColumnPtr residual = struct_child(*wrapper_schema, wrapper, 
"value", nullptr);
-                static_cast<bool>(residual) && has_present_value(residual)) {
-                // A residual value can contribute data to the same logical 
object. Reconstructing
-                // is required in that case; returning only the typed leaf 
would drop information.
-                
update_counter(_profile.variant_direct_leaf_residual_fallbacks, 1);
+            if (typed_schema->kind != ParquetColumnSchemaKind::PRIMITIVE ||
+                check_and_get_column<ColumnNullable>(*typed) == nullptr) {
+                
update_counter(_profile.variant_direct_leaf_unsupported_fallbacks, 1);
+                if (!_complete) {
+                    // Binary element_at evaluates complex prefixes before the 
validated leaf.
+                    // Serialize only retained descendants so projected-out 
fields stay hidden.
+                    if (auto normalized = find_normalized_value(path); 
normalized.has_value()) {
+                        return VariantShreddedTypedValue {
+                                .column = nullptr, .type = nullptr, 
.normalized = *normalized};
+                    }
+                }
                 return std::nullopt;
             }
 
-            typed = struct_child(*wrapper_schema, wrapper, "typed_value", 
&typed_schema);
-            if (!typed) {
-                return path_miss();
-            }
-            if (position + 1 == path.size()) {
-                if (typed_schema->kind != ParquetColumnSchemaKind::PRIMITIVE ||
-                    check_and_get_column<ColumnNullable>(*typed) == nullptr) {
-                    
update_counter(_profile.variant_direct_leaf_unsupported_fallbacks, 1);
+            const bool needs_row_merge =
+                    std::ranges::any_of(levels, [rows](const 
ShreddedPathLevel& level) {
+                        return residual_supplies_any_row(level, rows);
+                    });
+            if (needs_row_merge) {
+                // Decoding a residual needs the root dictionary. A state that 
projected the
+                // metadata away cannot serve those rows, so it keeps the 
complete fallback.
+                if (!metadata.present()) {
+                    
update_counter(_profile.variant_direct_leaf_residual_fallbacks, 1);
                     if (!_complete) {
-                        // Binary element_at evaluates complex prefixes before 
the validated leaf.
-                        // Serialize only retained descendants so 
projected-out fields stay hidden.
+                        // A projected state cannot rebuild its root, so 
serialize the retained
+                        // descendants instead of demanding the complete 
Variant.
                         if (auto normalized = find_normalized_value(path); 
normalized.has_value()) {
                             return VariantShreddedTypedValue {
                                     .column = nullptr, .type = nullptr, 
.normalized = *normalized};
                         }
                     }
                     return std::nullopt;
                 }
-                if (!supports_direct_typed_variant_state(*typed_schema)) {
-                    if (_complete) {
-                        
update_counter(_profile.variant_direct_leaf_unsupported_fallbacks, 1);
-                        return std::nullopt;
-                    }
-                    // A partial projection cannot reconstruct its root. 
Normalize only the exact
-                    // requested leaf so Parquet annotations survive 
heterogeneous file schemas.
-                    update_counter(_profile.variant_direct_leaf_rows,
-                                   static_cast<int64_t>(typed->size()));
-                    return VariantShreddedTypedValue {
-                            .column = nullptr,
-                            .type = nullptr,
-                            .normalized = 
normalize_projected_primitive_leaf(*typed_schema, typed)};
-                }
-                update_counter(_profile.variant_direct_leaf_rows,
-                               static_cast<int64_t>(typed->size()));
-                return VariantShreddedTypedValue {.column = std::move(typed),
-                                                  .type = 
remove_nullable(typed_schema->type),
-                                                  .normalized = nullptr};
+                update_counter(_profile.variant_direct_leaf_rows, 
static_cast<int64_t>(rows));
+                
update_counter(_profile.variant_direct_leaf_residual_merged_rows,
+                               static_cast<int64_t>(rows));
+                return VariantShreddedTypedValue {.column = nullptr,
+                                                  .type = nullptr,
+                                                  .normalized = 
resolve_variant_path_rows(
+                                                          typed_schema, 
typed.get(), metadata, rows,
+                                                          levels, path, 
metadata_index)};
             }
-            if (typed_schema->kind != ParquetColumnSchemaKind::STRUCT) {
-                return path_miss();
+
+            update_counter(_profile.variant_direct_leaf_rows, 
static_cast<int64_t>(rows));

Review Comment:
   [P1] Validate metadata before publishing a successful leaf
   
   This successful direct/normalized handoff never validates the root 
dictionary. Newly projectable binary/UUID/temporal leaves can therefore return 
a plausible value with null or malformed metadata, while canonical 
materialization rejects null metadata and calls 
`VariantMetadataRef::validate()` for every non-outer-null row. In row groups 
whose residual statistics are all null, `prune_variant_residual_columns_impl()` 
even removes metadata from I/O, so the residual-miss thread cannot fix this 
path. Please validate metadata before every successful handoff (and retain a 
validation read when pruning), with a malformed-metadata 
direct/normalized-vs-canonical test.



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