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


##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2004,6 +2125,7 @@ Status 
TableColumnMapper::_create_mapping_for_column(const ColumnDefinition& tab
     mapping->global_index = global_index;
     mapping->table_column_name = table_column.name;
     mapping->table_type = table_column.type;
+    mapping->variant_access_paths = table_column.variant_access_paths;

Review Comment:
   [P1] Propagate nested Variant paths through recursive mappings
   
   This assignment runs only while creating a root mapping. `AccessPathParser` 
attaches `{"x"}` to the nested `info.payload` `ColumnDefinition`, but recursive 
child construction at `_create_direct_mapping()` copies only its name and type. 
`build_complex_projection()` then reaches that Variant mapping with no child 
mappings and projects the complete wrapper, so the new regression's 
`VariantLeafProjections > 0` assertion cannot pass. Propagate 
`variant_access_paths` into recursive mappings and invoke the Variant 
projection builder at nested STRUCT/ARRAY/MAP terminals.



##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -55,6 +56,209 @@ 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);
+    }
+
+    enum class WrapperContext : uint8_t { OBJECT_FIELD, ARRAY_ELEMENT };
+    std::function<Status(const NativeFieldSchema&)> validate_typed_value;
+    std::function<Status(const NativeFieldSchema&, WrapperContext)> 
validate_wrapper;
+    validate_wrapper = [&](const NativeFieldSchema& wrapper, WrapperContext 
context) -> 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") {
+                if (fallback != nullptr) {
+                    return Status::Corruption(
+                            "Parquet Variant wrapper {} has duplicate value 
child", wrapper.name);
+                }
+                fallback = &child;
+            } else if (child.name == "typed_value") {
+                if (typed != nullptr) {
+                    return Status::Corruption(
+                            "Parquet Variant wrapper {} has duplicate 
typed_value child",
+                            wrapper.name);
+                }
+                typed = &child;
+            } else {
+                return Status::Corruption("Parquet Variant wrapper {} has 
unexpected child {}",
+                                          wrapper.name, child.name);
+            }
+        }
+        if (fallback == nullptr && typed == nullptr) {
+            return Status::Corruption(
+                    "Parquet Variant shredded wrapper {} requires at least one 
of value or "
+                    "typed_value",
+                    wrapper.name);
+        }
+        // Object fields always retain the fallback value carrier; only 
typed_value is optional.
+        // Array elements may omit either carrier when every element uses the 
remaining one.
+        if (context == WrapperContext::OBJECT_FIELD && fallback == nullptr) {
+            return Status::Corruption(
+                    "Parquet Variant object wrapper {} requires an optional 
value child",
+                    wrapper.name);
+        }
+        if (fallback != nullptr &&
+            (!fallback->children.empty() || fallback->physical_type != 
tparquet::Type::BYTE_ARRAY ||
+             !fallback->parquet_schema.__isset.repetition_type ||
+             fallback->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::OPTIONAL)) {
+            return Status::Corruption(
+                    "Parquet Variant wrapper {} value must be an optional 
BYTE_ARRAY",
+                    wrapper.name);
+        }
+        if (typed != nullptr) {
+            if (!typed->parquet_schema.__isset.repetition_type ||
+                typed->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::OPTIONAL) {
+                return Status::Corruption("Parquet Variant wrapper {} 
typed_value must be optional",
+                                          wrapper.name);
+            }
+            return validate_typed_value(*typed);
+        }
+        return Status::OK();
+    };
+    validate_typed_value = [&](const NativeFieldSchema& typed) -> Status {
+        if (!typed.unsupported_reason.empty()) {
+            return Status::NotSupported("Parquet Variant typed value {} is not 
supported: {}",
+                                        typed.name, typed.unsupported_reason);
+        }
+        if (typed.children.empty()) {
+            const auto& physical = typed.parquet_schema;
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.INTEGER &&
+                !physical.logicalType.INTEGER.isSigned) {
+                return Status::Corruption(
+                        "Parquet Variant unsigned integers are not valid typed 
values");
+            }
+            if (physical.__isset.converted_type &&
+                (physical.converted_type == tparquet::ConvertedType::UINT_8 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_16 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_32 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_64)) 
{
+                return Status::Corruption(
+                        "Parquet Variant unsigned integers are not valid typed 
values");
+            }
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.TIME) {
+                const auto& time = physical.logicalType.TIME;
+                // Variant v1 has one canonical TIME representation: local 
wall-clock MICROS.
+                // Accepting adjusted or lower-precision forms would make 
projection-dependent
+                // reconstruction disagree with the canonical Variant value.
+                if (time.isAdjustedToUTC) {
+                    return Status::Corruption(
+                            "Parquet Variant TIME must have 
isAdjustedToUTC=false");
+                }
+                if (!time.unit.__isset.MICROS) {
+                    return Status::Corruption(
+                            "Parquet Variant TIME(MILLIS) is not supported; 
use TIME(MICROS)");
+                }
+            }
+            if (physical.__isset.converted_type &&
+                physical.converted_type == 
tparquet::ConvertedType::TIME_MILLIS) {
+                return Status::Corruption(
+                        "Parquet Variant TIME(MILLIS) is not supported; use 
TIME(MICROS)");
+            }
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.TIMESTAMP &&
+                physical.logicalType.TIMESTAMP.unit.__isset.NANOS) {
+                // Reject at schema open so full reconstruction and direct 
typed-leaf access have
+                // the same precision contract instead of diverging after 
projection planning.
+                return Status::NotSupported("Parquet Variant TIMESTAMP(NANOS) 
is not supported");
+            }
+            return Status::OK();
+        }
+
+        const PrimitiveType primitive = 
remove_nullable(typed.data_type)->get_primitive_type();
+        if (primitive == TYPE_STRUCT) {
+            for (const auto& child : typed.children) {

Review Comment:
   [P1] Reject duplicate shredded object fields before planning
   
   This loop validates wrappers independently but never requires unique names. 
With two `typed_value` wrappers named `x`, full materialization skips an absent 
first wrapper and emits the populated second one, while 
`find_file_child_by_name()`/`find_child()` bind a direct `v['x']` projection to 
the first wrapper and return NULL; if both are populated, the full path instead 
errors on the duplicate key. The same file therefore produces 
query-shape-dependent values. Reject duplicate wrapper names here before 
projection planning, with first-absent/second-present and both-present negative 
fixtures.



##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -55,6 +56,209 @@ 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);
+    }
+
+    enum class WrapperContext : uint8_t { OBJECT_FIELD, ARRAY_ELEMENT };
+    std::function<Status(const NativeFieldSchema&)> validate_typed_value;
+    std::function<Status(const NativeFieldSchema&, WrapperContext)> 
validate_wrapper;
+    validate_wrapper = [&](const NativeFieldSchema& wrapper, WrapperContext 
context) -> 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") {
+                if (fallback != nullptr) {
+                    return Status::Corruption(
+                            "Parquet Variant wrapper {} has duplicate value 
child", wrapper.name);
+                }
+                fallback = &child;
+            } else if (child.name == "typed_value") {
+                if (typed != nullptr) {
+                    return Status::Corruption(
+                            "Parquet Variant wrapper {} has duplicate 
typed_value child",
+                            wrapper.name);
+                }
+                typed = &child;
+            } else {
+                return Status::Corruption("Parquet Variant wrapper {} has 
unexpected child {}",
+                                          wrapper.name, child.name);
+            }
+        }
+        if (fallback == nullptr && typed == nullptr) {
+            return Status::Corruption(
+                    "Parquet Variant shredded wrapper {} requires at least one 
of value or "
+                    "typed_value",
+                    wrapper.name);
+        }
+        // Object fields always retain the fallback value carrier; only 
typed_value is optional.
+        // Array elements may omit either carrier when every element uses the 
remaining one.
+        if (context == WrapperContext::OBJECT_FIELD && fallback == nullptr) {
+            return Status::Corruption(
+                    "Parquet Variant object wrapper {} requires an optional 
value child",
+                    wrapper.name);
+        }
+        if (fallback != nullptr &&
+            (!fallback->children.empty() || fallback->physical_type != 
tparquet::Type::BYTE_ARRAY ||
+             !fallback->parquet_schema.__isset.repetition_type ||
+             fallback->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::OPTIONAL)) {
+            return Status::Corruption(
+                    "Parquet Variant wrapper {} value must be an optional 
BYTE_ARRAY",
+                    wrapper.name);
+        }
+        if (typed != nullptr) {
+            if (!typed->parquet_schema.__isset.repetition_type ||
+                typed->parquet_schema.repetition_type != 
tparquet::FieldRepetitionType::OPTIONAL) {
+                return Status::Corruption("Parquet Variant wrapper {} 
typed_value must be optional",
+                                          wrapper.name);
+            }
+            return validate_typed_value(*typed);
+        }
+        return Status::OK();
+    };
+    validate_typed_value = [&](const NativeFieldSchema& typed) -> Status {
+        if (!typed.unsupported_reason.empty()) {
+            return Status::NotSupported("Parquet Variant typed value {} is not 
supported: {}",
+                                        typed.name, typed.unsupported_reason);
+        }
+        if (typed.children.empty()) {
+            const auto& physical = typed.parquet_schema;
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.INTEGER &&
+                !physical.logicalType.INTEGER.isSigned) {
+                return Status::Corruption(
+                        "Parquet Variant unsigned integers are not valid typed 
values");
+            }
+            if (physical.__isset.converted_type &&
+                (physical.converted_type == tparquet::ConvertedType::UINT_8 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_16 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_32 ||
+                 physical.converted_type == tparquet::ConvertedType::UINT_64)) 
{
+                return Status::Corruption(
+                        "Parquet Variant unsigned integers are not valid typed 
values");
+            }
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.TIME) {
+                const auto& time = physical.logicalType.TIME;
+                // Variant v1 has one canonical TIME representation: local 
wall-clock MICROS.
+                // Accepting adjusted or lower-precision forms would make 
projection-dependent
+                // reconstruction disagree with the canonical Variant value.
+                if (time.isAdjustedToUTC) {
+                    return Status::Corruption(
+                            "Parquet Variant TIME must have 
isAdjustedToUTC=false");
+                }
+                if (!time.unit.__isset.MICROS) {
+                    return Status::Corruption(
+                            "Parquet Variant TIME(MILLIS) is not supported; 
use TIME(MICROS)");
+                }
+            }
+            if (physical.__isset.converted_type &&
+                physical.converted_type == 
tparquet::ConvertedType::TIME_MILLIS) {
+                return Status::Corruption(
+                        "Parquet Variant TIME(MILLIS) is not supported; use 
TIME(MICROS)");
+            }
+            if (physical.__isset.logicalType && 
physical.logicalType.__isset.TIMESTAMP &&
+                physical.logicalType.TIMESTAMP.unit.__isset.NANOS) {
+                // Reject at schema open so full reconstruction and direct 
typed-leaf access have
+                // the same precision contract instead of diverging after 
projection planning.
+                return Status::NotSupported("Parquet Variant TIMESTAMP(NANOS) 
is not supported");
+            }
+            return Status::OK();

Review Comment:
   [P1] Enforce the Variant typed-value type matrix
   
   This primitive branch accepts every type that generic Parquet inference can 
represent, but the [Variant shredding 
contract](https://parquet.apache.org/docs/file-format/types/variantshredding/) 
has an exact physical/logical pair matrix. Consequently FLOAT16, 
JSON/ENUM/BSON, INT96, unannotated FIXED_LEN_BYTE_ARRAY, and mismatched 
integer/decimal pairs reach `append_typed_scalar()` and are re-encoded as 
plausible float/binary/timestamp/decimal Variant values instead of being 
rejected. That silently changes the source value's logical identity. Please 
validate the complete matrix here, including physical width, annotation, 
signedness, fixed length, precision/scale, and time unit, and add negative 
fixtures for unsupported pairs.



##########
be/src/format_v2/parquet/parquet_reader.cpp:
##########
@@ -57,6 +58,99 @@ struct ParquetReaderScanState {
     bool enable_strict_mode = false;
 };
 
+const ParquetColumnSchema* projected_schema_child(const ParquetColumnSchema& 
schema,
+                                                  int32_t local_id) {
+    const auto child_it = std::ranges::find_if(
+            schema.children, [local_id](const auto& child) { return 
child->local_id == local_id; });
+    return child_it == schema.children.end() ? nullptr : child_it->get();
+}
+
+const ParquetColumnSchema* schema_child_by_name(const ParquetColumnSchema& 
schema,
+                                                std::string_view name) {
+    const auto child_it = std::ranges::find_if(
+            schema.children, [name](const auto& child) { return child->name == 
name; });
+    return child_it == schema.children.end() ? nullptr : child_it->get();
+}
+
+bool collect_variant_residual_leaf_ids(const ParquetColumnSchema& schema,
+                                       const format::LocalColumnIndex& 
projection,
+                                       std::vector<int>* residual_leaf_ids) {
+    DORIS_CHECK(residual_leaf_ids != nullptr);
+    const auto* value = schema_child_by_name(schema, "value");
+    const auto* typed_value = schema_child_by_name(schema, "typed_value");
+    if (value != nullptr && typed_value != nullptr) {
+        if (value->kind != ParquetColumnSchemaKind::PRIMITIVE || 
value->leaf_column_id < 0) {
+            return false;
+        }
+        residual_leaf_ids->push_back(value->leaf_column_id);
+    }
+    for (const auto& child_projection : projection.children) {
+        const auto* child = projected_schema_child(schema, 
child_projection.local_id());
+        if (child == nullptr ||
+            !collect_variant_residual_leaf_ids(*child, child_projection, 
residual_leaf_ids)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool detail::variant_projection_is_fully_shredded(const 
tparquet::FileMetaData& metadata,
+                                                  const ParquetColumnSchema& 
schema,
+                                                  const 
format::LocalColumnIndex& projection) {
+    if (schema.kind != ParquetColumnSchemaKind::VARIANT ||
+        !format::is_partial_projection(&projection)) {
+        return false;
+    }
+    std::vector<int> residual_leaf_ids;
+    if (!collect_variant_residual_leaf_ids(schema, projection, 
&residual_leaf_ids)) {
+        return false;
+    }
+    std::ranges::sort(residual_leaf_ids);
+    residual_leaf_ids.erase(std::unique(residual_leaf_ids.begin(), 
residual_leaf_ids.end()),
+                            residual_leaf_ids.end());
+    for (const auto& row_group : metadata.row_groups) {
+        for (const int leaf_id : residual_leaf_ids) {
+            if (leaf_id < 0 || leaf_id >= 
static_cast<int>(row_group.columns.size())) {
+                return false;
+            }
+            const auto& chunk = row_group.columns[leaf_id];
+            if (!chunk.__isset.meta_data || 
!chunk.meta_data.__isset.statistics ||
+                !chunk.meta_data.statistics.__isset.null_count ||
+                chunk.meta_data.statistics.null_count != row_group.num_rows) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+size_t finalize_variant_leaf_projections(
+        const NativeParquetMetadata& metadata,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        std::vector<format::LocalColumnIndex>* projections) {
+    DORIS_CHECK(projections != nullptr);
+    size_t retained = 0;
+    for (auto& projection : *projections) {
+        const int32_t local_id = projection.local_id();
+        if (local_id < 0 || local_id >= 
static_cast<int32_t>(file_schema.size()) ||
+            file_schema[local_id]->kind != ParquetColumnSchemaKind::VARIANT ||

Review Comment:
   [P1] Finalize nested Variant projections recursively
   
   This condition limits residual completeness validation to top-level Variant 
columns. Once the nested mapping is wired, a STRUCT/LIST/MAP-rooted request 
skips `variant_projection_is_fully_shredded()` and unsafe-node expansion, yet 
native planning still builds an incomplete nested Variant state. If that 
wrapper has a populated residual `value`, direct leaf lookup can then return 
only the typed leaf and silently drop the residual contribution. Walk the 
schema/projection recursively, expand each unsafe nested Variant before reader 
creation, and count retained nested projections. For ARRAY/MAP nesting, prove 
fallback absence in the repeated Variant-instance domain rather than comparing 
leaf `null_count` with top-level Row Group rows.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/BindSink.java:
##########
@@ -725,6 +725,7 @@ private Plan 
bindIcebergTableSink(MatchingContext<UnboundIcebergTableSink<Plan>>
         Pair<IcebergExternalDatabase, IcebergExternalTable> pair = 
bind(ctx.cascadesContext, sink);
         IcebergExternalDatabase database = pair.first;
         IcebergExternalTable table = pair.second;
+        IcebergUtils.validateWriteSchema(table.getFullSchema());

Review Comment:
   [P1] Apply the read-only gate to direct Iceberg merge sinks
   
   This is the only production call to `validateWriteSchema()`, so it protects 
`UnboundIcebergTableSink` but not UPDATE/MERGE: both commands construct 
`LogicalIcebergMergeSink` directly and its expression binder never invokes this 
check. UPDATE also projects every base column, so changing an unrelated field 
rewrites the unchanged Variant through the writer that this PR declares 
unsupported, risking lossy data. Enforce the gate at a common data-writing 
Iceberg sink boundary (or explicitly in both commands) and cover UPDATE plus 
matched-update/not-matched-insert MERGE.



##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -517,6 +726,21 @@ Status NativeFieldDescriptor::parse_group_field(
         const std::vector<tparquet::SchemaElement>& t_schemas, size_t curr_pos,
         NativeFieldSchema* group_field) {
     auto& group_schema = t_schemas[curr_pos];
+    group_field->parquet_schema = group_schema;
+    if (is_variant_node(group_schema)) {

Review Comment:
   [P1] Preserve repetition when parsing Variant groups
   
   This branch runs before generic repeated-group handling, although the 
[Variant encoding 
contract](https://parquet.apache.org/docs/file-format/types/variantencoding/) 
places no additional restriction on group repetition. For `repeated group v 
(VARIANT(1))`, `parse_struct_field()` neither increments the 
repetition/definition levels nor constructs the ARRAY wrapper, and 
`build_native_node_schema()` exposes a scalar Variant with maximum repetition 
level zero while the leaf pages use repeated levels. A valid file can therefore 
fail decoding or return the wrong row shape. Preserve the repeated wrapper and 
its levels here (or reject it explicitly if unsupported), and add 
required/optional/repeated Variant schema and read fixtures.



##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/source/IcebergScanNode.java:
##########
@@ -263,6 +263,15 @@ protected void doInitialize() throws UserException {
             }
         }
         super.doInitialize();
+        // This gate must run during shared initialization: batch split 
assignment bypasses
+        // doGetSplits(), but it must never assign a semantic Variant 
projection to an old BE.
+        
checkVariantBackendCompatibilityForCurrentScan(backendPolicy.getBackends());
+    }
+
+    void checkVariantBackendCompatibilityForCurrentScan(Iterable<Backend> 
backends)
+            throws UserException {
+        boolean projectsVariant = !isTableLevelCountStarPushdown() && 
projectsVariant(desc);

Review Comment:
   [P1] Gate COUNT(*) only after a metadata count is proven
   
   This treats every syntactic COUNT(*) as non-Variant during `doInitialize()`, 
but only later does `isBatchMode()` discover whether `getCountFromSnapshot()` 
returned a nonnegative metadata count. Missing counters or equality/non-ignored 
position deletes fall back to normal data splits; the V1 Iceberg reader then 
disables its underlying COUNT reader for delete files and performs a real 
carrier-column read. Together with the matching legacy-scanner exemption, a 
Variant-only table can therefore reach an old/legacy reader that cannot decode 
it. Grant this exemption only after a nonnegative table-level count is 
attached; otherwise require current backends and FileScannerV2.



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