github-actions[bot] commented on code in PR #66206:
URL: https://github.com/apache/doris/pull/66206#discussion_r3673818619
##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -1784,15 +1784,97 @@ static bool table_root_is_map(const ColumnMapping&
mapping) {
return remove_nullable(mapping.table_type)->get_primitive_type() ==
TYPE_MAP;
}
+static const ColumnDefinition* find_file_child_by_name(
+ const std::vector<ColumnDefinition>& children, std::string_view name) {
+ const auto child_it = std::ranges::find_if(
+ children, [name](const ColumnDefinition& child) { return
child.name == name; });
+ return child_it == children.end() ? nullptr : &*child_it;
+}
+
+static bool build_variant_leaf_path_projection(const ColumnMapping& mapping,
+ const std::vector<std::string>&
path,
+ LocalColumnIndex*
root_projection) {
+ DORIS_CHECK(root_projection != nullptr);
+ if (path.empty() || !mapping.file_local_id.has_value()) {
+ return false;
+ }
+ *root_projection = LocalColumnIndex::partial_local(*mapping.file_local_id);
+ const auto* root_typed =
find_file_child_by_name(mapping.original_file_children, "typed_value");
+ if (root_typed == nullptr || root_typed->children.empty()) {
Review Comment:
[P1] Restrict leaf projection traversal to shredded objects
A conforming shredded array stores its typed payload as a LIST whose
physical child is named `element`. A query such as `v['element']` is an
object-key lookup on that array and should be missing/NULL, but this code can
match the LIST child as though it were an object field and retain an incomplete
projection when the residuals are all null. Extraction then rejects the
non-STRUCT root in `find_typed_value()`, and the fallback cannot materialize an
incomplete state, so the query fails with `A projected Parquet Variant can only
serve its validated shredded leaves`. Please require the root and each
intermediate `typed_value` to be STRUCT/object nodes before matching key names,
and cover a shredded array with an `element` key lookup.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/iceberg/IcebergUtils.java:
##########
@@ -713,7 +713,11 @@ public static Type
icebergTypeToDorisType(org.apache.iceberg.types.Type type, bo
.collect(Collectors.toCollection(ArrayList::new));
return new StructType(nestedTypes);
case VARIANT:
- return Type.UNSUPPORTED;
+ // Iceberg Variant uses the Parquet Variant encoding directly.
Mark it compute-only
+ // so BE scanners materialize ColumnVariantV2 without changing
persisted Doris
+ // table metadata semantics.
+ return new org.apache.doris.catalog.VariantType(
Review Comment:
[P1] Gate Iceberg DML until the writer supports VARIANT
Separately from the read-side upgrade gate, returning a normal Doris type
here also lets `BindSink.bindIcebergTableSink()` plan INSERT/rewrite operations
for tables containing VARIANT. `IcebergTableSink` always serializes the
complete Iceberg schema, and `VIcebergTableWriter::open()` feeds it to the BE
Iceberg `SchemaParser`; its `TypeID`/`Types::from_primitive_string()` has no
`variant` case and throws `Cannot parse type string to primitive: variant`.
Consequently any Doris INSERT into such a table fails only after reaching the
sink, even when the VARIANT column was omitted, while the new tests avoid the
path by writing through Spark. If this feature is read-only, reject Iceberg
DML/rewrite during FE analysis for root or nested VARIANT; otherwise the writer
stack and a Doris INSERT test need full support.
##########
be/src/format_v2/parquet/parquet_statistics.cpp:
##########
@@ -614,12 +927,17 @@ void collect_filtered_leaf_ids(const ParquetColumnSchema&
column_schema,
return;
}
for (const auto& child_schema : column_schema.children) {
- if (!format::is_child_projected(projection, child_schema->local_id)) {
+ if (column_schema.kind != ParquetColumnSchemaKind::VARIANT &&
Review Comment:
[P2] Account only for the retained Variant projection
Once `finalize_variant_leaf_projections()` retains a leaf-only Variant
projection, reconstruction no longer reads every physical sibling. This special
case still traverses the entire Variant and
`native_requested_compressed_bytes()` consequently adds metadata, residual, and
unrelated typed chunks to `FilteredBytes`, even though prefetch/readers request
only the projected leaf. Please follow the actual projection here (using the
full sibling set only for a full Variant projection) so the pruning profile
reports bytes that would really have been read.
##########
be/src/format_v2/parquet/native_schema_desc.cpp:
##########
@@ -55,6 +55,84 @@ 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 &&
Review Comment:
[P2] Validate the complete shredded schema at open
This validates only the root wrapper and then lets the ordinary Parquet type
mapper accept any optional `typed_value` subtree. The Variant shredding
specification also fixes the nested shape and scalar matrix: named object-field
and LIST-element wrappers are required, and types such as unsigned INTEGER,
adjusted TIME, or TIME(MILLIS) are not valid shredded scalars. The current gap
makes malformed input projection-dependent—for example, root reconstruction
rejects an unsigned typed leaf in `integer_width()`, while the direct leaf path
returns its widened Doris column without reaching that check. Please
recursively validate every wrapper and typed leaf here so nonconforming
shredding fails once at schema open, and add negative schema tests that
exercise both root and narrow projections.
##########
be/src/exec/scan/access_path_parser.cpp:
##########
@@ -470,6 +470,44 @@ Status
AccessPathParser::build_nested_children(format::ColumnDefinition* column,
if (is_scanner_materialized_virtual_column(column->name)) {
return Status::OK();
}
+ if (remove_nullable(column->type)->get_primitive_type() == TYPE_VARIANT) {
+ column->variant_access_paths.clear();
+ for (const auto& access_path : access_paths) {
+ if (access_path.type != TAccessPathType::DATA ||
+ !access_path.__isset.data_access_path) {
+ return Status::NotSupported(
+ "AccessPathParser only supports DATA access paths for
Variant slot {}",
+ column->name);
+ }
+ const auto& path = access_path.data_access_path.path;
+ if (path.empty()) {
+ // Match the generic access-path tree: an empty DATA path
denotes the whole slot
+ // and dominates every narrower Variant path in the same
request.
+ column->variant_access_paths.clear();
+ return Status::OK();
+ }
+ int32_t top_level_id = -1;
+ if (to_lower(path.front()) != to_lower(column->name) &&
+ (!parse_non_negative_int(path.front(), &top_level_id) ||
+ !column->has_identifier_field_id() ||
+ top_level_id != column->get_identifier_field_id())) {
+ return Status::NotSupported(
+ "AccessPathParser access path {} does not match
Variant slot {}",
+ access_path_to_string(path), column->name);
+ }
+ if (path.size() == 1) {
+ // A whole-root access covers every subpath and must disable
physical leaf pruning.
+ column->variant_access_paths.clear();
+ return Status::OK();
+ }
+ column->variant_access_paths.emplace_back(path.begin() + 1,
path.end());
Review Comment:
[P1] Do not leaf-project lossy Variant access paths
By the time these strings reach this branch, FE has already lost several
distinctions that are semantic for Variant access: `v[1]` and `v['1']` both
produce `"1"`; the `TreeMultimap` comparator dot-joins components, so
`v['a.b']` and `v['a']['b']` compare equal; and a nested literal key `"NULL"`
is stripped as though it were the null-metadata sentinel. The mapper
nevertheless treats each surviving string sequence as an exact object path and
may retain an incomplete leaf projection. On conforming fully shredded objects,
evaluation preserves the original expression path, cannot find the mismatched
or omitted typed leaf, and then fails because the incomplete Variant cannot be
materialized. Please preserve segment kind, component boundaries, and sentinel
provenance end to end—or disable leaf projection unless the path is provably
lossless—and add differential tests for all three collisions.
--
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]