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


##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -497,6 +554,9 @@ void ColumnVariantV2::for_each_subcolumn(ColumnCallback 
callback) const {
 }
 
 void ColumnVariantV2::mutate_subcolumns() {
+    if (_shredded) {
+        ensure_encoded();

Review Comment:
   [P1] Preserve leaf projections across output detachment
   
   A retained leaf-only state reaches this method during every normal 
table-reader handoff.
   `finalize_chunk()` calls `IColumn::mutate()` on each mapped output; nullable 
mutation recurses
   into the nested `ColumnVariantV2`, and this `ensure_encoded()` then calls
   `materialized_column()`, which intentionally throws while `_complete` is 
false. A trivial
   Variant mapping is just a `VSlotRef`, and even the last mapping's detach 
helper mutates it, so
   `SELECT v['n']` fails here before the upper element expression can consume 
the validated leaf.
   Please make ownership detachment preserve/share immutable shredded state and 
defer canonical
   encoding until an operation actually needs full logical values; add an 
end-to-end
   `TableReader` leaf-projection test.
   



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -1200,6 +1200,11 @@ private List<Split> doGetSplits(int numBackends) throws 
UserException {
             return doGetSystemTableSplits();
         }
 
+        boolean projectsVariant = !isTableLevelCountStarPushdown()
+                && desc.getSlots().stream()
+                        .anyMatch(slot -> 
IcebergUtils.containsVariant(slot.getColumn().getType()));

Review Comment:
   [P1] Gate smooth upgrades from the effective projected slot type
   
   This checks the original Iceberg `Column`, not the scan slot type after 
nested-column pruning.
   `SlotTypeReplacer` changes the `SlotReference` data type but retains 
`originalColumn`, and
   `PlanTranslatorContext` consequently stores the full original column while 
serializing the
   pruned type in the descriptor. For `STRUCT<label: STRING, payload: 
VARIANT>`, selecting only
   `info.label` sends no Variant to BE but is still rejected whenever a 
smooth-upgrade source is
   present. Derive this capability check from `slot.getType()` (and 
materialized semantic slots)
   and add a non-Variant sibling projection case.
   



##########
be/src/core/column/variant_v2/column_variant_v2.cpp:
##########
@@ -724,6 +792,20 @@ void ColumnVariantV2::insert_range_from( // 
NOLINT(readability-function-size)
         return;
     }
 
+    if (!_shredded && !_typed && empty() && _metadatas->empty() && 
source._shredded && start == 0 &&
+        length == source.size()) {
+        _shredded = source._shredded;
+        _check_invariants();
+        return;
+    }
+    if (_shredded) {
+        ensure_encoded();
+    }
+    if (source._shredded) {
+        insert_range_from(source._shredded->materialized_column(), start, 
length);

Review Comment:
   [P1] Keep subset slices of leaf projections shredded
   
   Fixing the ownership-detach path does not make proper slices usable. 
`IColumn::cut()` creates
   an empty destination and calls `insert_range_from()`; for a proper subset 
the full-source
   sharing branch above does not apply, so this line asks an intentionally 
incomplete projection
   for its canonical column and throws. `ParquetScanScheduler` reaches this 
after an all-filtered
   prefix widens predicate probing and a later probe has more survivors than 
`_batch_size`:
   `materialize_pending_predicate_batch()` slices every retained 
predicate/output column with
   `column->cut(...)`. Please give the shredded state native range/index 
selection (or otherwise
   implement subset insertion without full materialization) and cover that 
widened-batch path.
   



##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -55,6 +56,180 @@ static bool is_map_node(const tparquet::SchemaElement& 
schema) {
            (schema.__isset.logicalType && schema.logicalType.__isset.MAP);
 }
 
+static bool is_variant_node(const tparquet::SchemaElement& schema) {
+    return schema.__isset.logicalType && schema.logicalType.__isset.VARIANT;
+}
+
+class ScopedBoolOverride {
+public:
+    ScopedBoolOverride(bool& target, bool value) : _target(target), 
_original(target) {
+        _target = value;
+    }
+    ~ScopedBoolOverride() { _target = _original; }
+
+private:
+    bool& _target;
+    bool _original;
+};
+
+static Status validate_variant_layout(const tparquet::SchemaElement& 
group_schema,
+                                      const NativeFieldSchema& group_field) {
+    const auto& annotation = group_schema.logicalType.VARIANT;
+    if (annotation.__isset.specification_version && 
annotation.specification_version != 1) {
+        return Status::NotSupported("Parquet Variant specification version {} 
is not supported",
+                                    annotation.specification_version);
+    }
+    if (group_field.children.size() < 2 || group_field.children.size() > 3) {
+        return Status::Corruption(
+                "Parquet Variant {} must contain metadata, value, and optional 
typed_value",
+                group_schema.name);
+    }
+
+    const NativeFieldSchema* metadata = nullptr;
+    const NativeFieldSchema* value = nullptr;
+    const NativeFieldSchema* typed_value = nullptr;
+    for (const auto& child : group_field.children) {
+        const NativeFieldSchema** target = nullptr;
+        if (child.name == "metadata") {
+            target = &metadata;
+        } else if (child.name == "value") {
+            target = &value;
+        } else if (child.name == "typed_value") {
+            target = &typed_value;
+        } else {
+            return Status::Corruption("Parquet Variant {} has unexpected child 
{}",
+                                      group_schema.name, child.name);
+        }
+        if (*target != nullptr) {
+            return Status::Corruption("Parquet Variant {} has duplicate child 
{}",
+                                      group_schema.name, child.name);
+        }
+        *target = &child;
+    }
+    if (metadata == nullptr || value == nullptr) {
+        return Status::Corruption("Parquet Variant {} requires metadata and 
value children",
+                                  group_schema.name);
+    }
+    if (!metadata->children.empty() || metadata->physical_type != 
tparquet::Type::BYTE_ARRAY ||
+        metadata->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::REQUIRED) {
+        return Status::Corruption("Parquet Variant {} metadata must be a 
required BYTE_ARRAY",
+                                  group_schema.name);
+    }
+    const auto expected_value_repetition = typed_value == nullptr
+                                                   ? 
tparquet::FieldRepetitionType::REQUIRED
+                                                   : 
tparquet::FieldRepetitionType::OPTIONAL;
+    // SQL nullability belongs to the outer Variant group. Only shredding 
makes value optional,
+    // because typed_value may carry all or part of the logical value instead.
+    if (!value->children.empty() || value->physical_type != 
tparquet::Type::BYTE_ARRAY ||
+        value->parquet_schema.repetition_type != expected_value_repetition) {
+        return Status::Corruption("Parquet Variant {} value must be a {} 
BYTE_ARRAY",
+                                  group_schema.name,
+                                  typed_value == nullptr ? "required" : 
"optional");
+    }
+    if (typed_value != nullptr &&
+        typed_value->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::OPTIONAL) {
+        return Status::Corruption("Parquet Variant {} typed_value must be 
optional",
+                                  group_schema.name);
+    }
+
+    std::function<Status(const NativeFieldSchema&)> validate_typed_value;
+    std::function<Status(const NativeFieldSchema&)> validate_wrapper;
+    validate_wrapper = [&](const NativeFieldSchema& wrapper) -> Status {
+        if (!wrapper.parquet_schema.__isset.repetition_type ||
+            wrapper.parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::REQUIRED) {
+            return Status::Corruption("Parquet Variant shredded wrapper {} 
must be required",
+                                      wrapper.name);
+        }
+        const NativeFieldSchema* fallback = nullptr;
+        const NativeFieldSchema* typed = nullptr;
+        for (const auto& child : wrapper.children) {
+            if (child.name == "value") {
+                fallback = &child;
+            } else if (child.name == "typed_value") {
+                typed = &child;
+            } else {
+                return Status::Corruption("Parquet Variant wrapper {} has 
unexpected child {}",
+                                          wrapper.name, child.name);
+            }
+        }
+        if (fallback == nullptr || typed == nullptr || 
!fallback->children.empty() ||

Review Comment:
   [P1] Accept valid omitted children in shredded wrappers
   
   Requiring both schema children rejects conforming Variant shredding layouts. 
The
   [Parquet 
specification](https://parquet.apache.org/docs/file-format/types/variantshredding/)
   permits an object-field wrapper to omit `typed_value` when that field is not 
shredded as a
   specific type, and an ARRAY element wrapper may omit `value` when fully 
typed or omit
   `typed_value` when untyped (with at least one representation present). 
`append_wrapper()`
   already handles a missing child, but these files fail during schema open 
before decoding.
   Please validate the context-specific allowed shapes, validate each present 
child, and recurse
   only when `typed_value` exists; add schema-open coverage for value-only 
object fields and
   typed-only/value-only ARRAY elements.
   



##########
be/src/exec/scan/file_scanner_v2.cpp:
##########
@@ -627,8 +672,15 @@ Status FileScannerV2::_create_table_reader_for_format(
 
 Status FileScannerV2::_prepare_table_reader_split(const TFileRangeDesc& range,
                                                   std::map<std::string, Field> 
partition_values) {
+    const auto format_type = get_range_format_type(*_params, range);
+    const bool projects_variant =
+            !_local_state->is_count_star_pushdown() &&
+            std::ranges::any_of(_projected_columns, [](const 
format::ColumnDefinition& column) {
+                return contains_variant_type(column.type);
+            });
+    RETURN_IF_ERROR(validate_iceberg_variant_file_format(range, format_type, 
projects_variant));

Review Comment:
   [P1] Gate only splits that physically read Variant data
   
   This rejects from the logical projected type before `prepare_split()` can 
apply
   partition/runtime-filter pruning, while the earlier 
`_build_projected_columns()` check rejects
   before per-file schema mapping. That breaks valid schema evolution: the new 
regression itself
   writes an ORC file containing only `id`, then adds `v VARIANT`; the mapper 
should synthesize
   NULL for that old file, and a mixed table should decode Variant only from 
its newer Parquet
   file. Instead both queries fail even though no ORC Variant value exists. 
Delay the capability
   check until the split survives pruning and mapping proves a requested 
Variant has a physical
   source, and make the evolved-ORC/mixed-format cases return NULL plus the 
Parquet value.
   



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