This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new fbafbbe89e [format] Support nested variant column pruning for shredded
parquet files (#9389)
fbafbbe89e is described below
commit fbafbbe89e5c92f9205b9d60e1b87a08e3023224
Author: Juntao Zhang <[email protected]>
AuthorDate: Mon Aug 31 13:44:14 2026 +0800
[format] Support nested variant column pruning for shredded parquet files
(#9389)
---
.../parquet/VariantShreddingReadPlanFactory.java | 52 +-
.../format/parquet/VariantShreddingTypePruner.java | 259 ++++++++++
.../parquet/reader/VariantShreddingReadTest.java | 211 ++++++++
.../reader/VariantShreddingTypePrunerTest.java | 572 +++++++++++++++++++++
4 files changed, 1043 insertions(+), 51 deletions(-)
diff --git
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingReadPlanFactory.java
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingReadPlanFactory.java
index eaeeca1d2d..0e902c56e3 100644
---
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingReadPlanFactory.java
+++
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingReadPlanFactory.java
@@ -33,7 +33,6 @@ import org.apache.paimon.data.shredding.ShreddingReadPlan;
import org.apache.paimon.data.variant.PaimonShreddingUtils;
import org.apache.paimon.data.variant.PaimonShreddingUtils.FieldToExtract;
import org.apache.paimon.data.variant.VariantMetadataUtils;
-import org.apache.paimon.data.variant.VariantPathSegment;
import org.apache.paimon.format.shredding.ShreddingReadPlanFactory;
import org.apache.paimon.types.ArrayType;
import org.apache.paimon.types.DataField;
@@ -52,14 +51,11 @@ import org.apache.parquet.schema.Type;
import javax.annotation.Nullable;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
import static org.apache.paimon.data.variant.Variant.METADATA;
import static org.apache.paimon.data.variant.Variant.VALUE;
-import static org.apache.paimon.data.variant.VariantMetadataUtils.path;
import static
org.apache.paimon.format.parquet.ParquetSchemaConverter.parquetListElementType;
import static
org.apache.paimon.format.parquet.ParquetSchemaConverter.parquetMapKeyValueType;
@@ -124,53 +120,7 @@ public class VariantShreddingReadPlanFactory implements
ShreddingReadPlanFactory
/** Clips a Variant Parquet field according to the logical Variant row
read type. */
public static Type clipVariantType(RowType variantRowType, GroupType
parquetType) {
- if
(!parquetType.containsField(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME)) {
- return parquetType;
- }
-
- boolean canClip = true;
- Set<String> fieldsToRead = new HashSet<>();
- for (DataField field : variantRowType.getFields()) {
- String path = path(field.description());
- VariantPathSegment[] pathSegments = VariantPathSegment.parse(path);
- if (pathSegments.length < 1) {
- canClip = false;
- break;
- }
-
- // TODO: support nested column pruning.
- VariantPathSegment pathSegment = pathSegments[0];
- if (pathSegment instanceof VariantPathSegment.ObjectExtraction) {
- fieldsToRead.add(((VariantPathSegment.ObjectExtraction)
pathSegment).getKey());
- } else {
- canClip = false;
- break;
- }
- }
-
- if (!canClip) {
- return parquetType;
- }
-
- List<Type> typedFieldsToRead = new ArrayList<>();
- GroupType typedValue =
-
parquetType.getType(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME).asGroupType();
- for (Type field : typedValue.getFields()) {
- if (fieldsToRead.contains(field.getName())) {
- typedFieldsToRead.add(field);
- fieldsToRead.remove(field.getName());
- }
- }
-
- List<Type> rowGroupFields = new ArrayList<>();
-
rowGroupFields.add(parquetType.getType(PaimonShreddingUtils.METADATA_FIELD_NAME));
- if (!fieldsToRead.isEmpty()) {
-
rowGroupFields.add(parquetType.getType(PaimonShreddingUtils.VARIANT_VALUE_FIELD_NAME));
- }
- if (!typedFieldsToRead.isEmpty()) {
- rowGroupFields.add(typedValue.withNewFields(typedFieldsToRead));
- }
- return parquetType.withNewFields(rowGroupFields);
+ return VariantShreddingTypePruner.clip(variantRowType, parquetType);
}
private static boolean containsVariantFields(DataType dataType) {
diff --git
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingTypePruner.java
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingTypePruner.java
new file mode 100644
index 0000000000..f9e5075342
--- /dev/null
+++
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/VariantShreddingTypePruner.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.format.parquet;
+
+import org.apache.paimon.data.variant.PaimonShreddingUtils;
+import org.apache.paimon.data.variant.VariantMetadataUtils;
+import org.apache.paimon.data.variant.VariantPathSegment;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.RowType;
+
+import org.apache.parquet.schema.GroupType;
+import org.apache.parquet.schema.LogicalTypeAnnotation;
+import org.apache.parquet.schema.Type;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static
org.apache.paimon.format.parquet.ParquetSchemaConverter.parquetListElementType;
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/**
+ * Prunes a shredded Variant Parquet type according to a logical Variant
projection.
+ *
+ * <p>It builds a trie from the requested object/array paths and recursively
removes unneeded fields
+ * from the {@code typed_value} (and list element) groups while preserving
{@code value} fallbacks
+ * when a requested path cannot be satisfied from typed columns.
+ *
+ * <p>Variant object keys are matched case-sensitively, independently of
Parquet column-name
+ * resolution, because Variant path extraction downstream resolves keys
exactly through {@code
+ * objectSchemaMap}.
+ */
+public class VariantShreddingTypePruner {
+ private static final String LIST_WRAPPER_NAME = "list";
+ private static final String LIST_ELEMENT_NAME = "element";
+
+ @Nullable private final PathNode root;
+
+ VariantShreddingTypePruner(RowType variantRowType) {
+ this.root = buildPathTree(variantRowType);
+ }
+
+ /**
+ * Clips the given Parquet Variant type to only include fields needed for
{@code
+ * variantRowType}.
+ *
+ * @param variantRowType the logical Variant projection row type
+ * @param parquetType the physical Parquet Variant type
+ * @return a clipped Parquet type
+ */
+ public static Type clip(RowType variantRowType, GroupType parquetType) {
+ return new
VariantShreddingTypePruner(variantRowType).clip(parquetType);
+ }
+
+ private Type clip(GroupType parquetType) {
+ return clipShreddingRow(parquetType, root);
+ }
+
+ /** A projection trie for Variant object paths and array element paths. */
+ private static class PathNode {
+ private final Map<String, PathNode> children = new HashMap<>();
+ private PathNode arrayElement;
+ private boolean keepAll;
+
+ private PathNode getOrCreateChild(String key) {
+ return children.computeIfAbsent(key, k -> new PathNode());
+ }
+ }
+
+ @Nullable
+ private PathNode buildPathTree(RowType variantRowType) {
+ PathNode root = new PathNode();
+ for (DataField field : variantRowType.getFields()) {
+ String path = VariantMetadataUtils.path(field.description());
+ VariantPathSegment[] segments = VariantPathSegment.parse(path);
+ if (segments.length == 0) {
+ return null;
+ }
+
+ PathNode node = root;
+ for (VariantPathSegment segment : segments) {
+ if (segment instanceof VariantPathSegment.ArrayExtraction) {
+ // Array indices cannot prune individual elements at the
Parquet level,
+ // but we can still prune nested fields inside each array
element.
+ if (node.arrayElement == null) {
+ node.arrayElement = new PathNode();
+ }
+ node = node.arrayElement;
+ } else if (segment instanceof
VariantPathSegment.ObjectExtraction) {
+ String key = ((VariantPathSegment.ObjectExtraction)
segment).getKey();
+ node = node.getOrCreateChild(key);
+ } else {
+ return null;
+ }
+ }
+ node.keepAll = true;
+ }
+ return root;
+ }
+
+ private Type clipShreddingRow(Type type, PathNode node) {
+ if (type.isPrimitive() || node == null) {
+ return type;
+ }
+
+ GroupType group = type.asGroupType();
+ if (node.keepAll ||
!group.containsField(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME)) {
+ return group;
+ }
+
+ List<Type> newFields = new ArrayList<>();
+ if (group.containsField(PaimonShreddingUtils.METADATA_FIELD_NAME)) {
+
newFields.add(group.getType(PaimonShreddingUtils.METADATA_FIELD_NAME));
+ }
+
+ Type typedValue =
group.getType(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME);
+ if (isCanonicalList(typedValue) && node.arrayElement != null) {
+ return clipListShreddingRow(group, node, newFields);
+ } else if (isObjectGroup(typedValue) && node.arrayElement == null) {
+ return clipObjectShreddingRow(group, node, newFields);
+ } else {
+ return group;
+ }
+ }
+
+ private GroupType clipObjectShreddingRow(GroupType group, PathNode node,
List<Type> newFields) {
+ Type typedValueType =
group.getType(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME);
+ GroupType typedValue = typedValueType.asGroupType();
+ // typed_value is an object group: prune by object key.
+ boolean needValue = false;
+ List<Type> clippedTypedFields = new ArrayList<>();
+ Set<String> requestedKeys = new HashSet<>(node.children.keySet());
+
+ for (Type field : typedValue.getFields()) {
+ String fieldName = field.getName();
+ PathNode child = node.children.get(fieldName);
+ if (child == null) {
+ continue;
+ }
+ requestedKeys.remove(fieldName);
+
+ checkArgument(!field.isPrimitive());
+ Type clippedChild = clipShreddingRow(field, child);
+ clippedTypedFields.add(clippedChild);
+ }
+
+ if (!requestedKeys.isEmpty()) {
+ needValue = true;
+ }
+
+ if (needValue) {
+
checkArgument(group.containsField(PaimonShreddingUtils.VARIANT_VALUE_FIELD_NAME));
+
newFields.add(group.getType(PaimonShreddingUtils.VARIANT_VALUE_FIELD_NAME));
+ }
+
+ if (!clippedTypedFields.isEmpty()) {
+ newFields.add(typedValue.withNewFields(clippedTypedFields));
+ }
+ return group.withNewFields(newFields);
+ }
+
+ private GroupType clipListShreddingRow(GroupType group, PathNode node,
List<Type> newFields) {
+ Type type = group.getType(PaimonShreddingUtils.TYPED_VALUE_FIELD_NAME);
+ GroupType listGroup = type.asGroupType();
+ if (node.arrayElement.keepAll) {
+ // The projection reads the whole array element (e.g. $.arr[0]
read as VARIANT);
+ return group;
+ }
+
+ // If there are also object projections on this Variant (e.g. querying
both $[0].x and
+ // $.y), the typed list columns cannot satisfy the object paths. We
must keep the parent
+ // value fallback so that object-shaped rows can still be read
correctly.
+ if (!node.children.isEmpty()) {
+
checkArgument(group.containsField(PaimonShreddingUtils.VARIANT_VALUE_FIELD_NAME));
+
newFields.add(group.getType(PaimonShreddingUtils.VARIANT_VALUE_FIELD_NAME));
+ }
+
+ Type elementType = parquetListElementType(listGroup);
+ Type clippedElement = clipShreddingRow(elementType.asGroupType(),
node.arrayElement);
+ GroupType repeated = listGroup.getType(0).asGroupType();
+ GroupType clippedRepeated =
+
repeated.withNewFields(Collections.singletonList(clippedElement));
+
newFields.add(listGroup.withNewFields(Collections.singletonList(clippedRepeated)));
+ return group.withNewFields(newFields);
+ }
+
+ /**
+ * Returns true if the given group follows the canonical three-level
Parquet list layout.
+ *
+ * <p>The canonical layout is described in the Parquet spec: <a
+ *
href="https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#lists">LogicalTypes#Lists</a>
+ */
+ private static boolean isCanonicalList(Type type) {
+ if (type.isPrimitive()) {
+ return false;
+ }
+
+ GroupType listGroup = type.asGroupType();
+ // 1. Must be a LIST logical type.
+ if (!(listGroup.getLogicalTypeAnnotation()
+ instanceof LogicalTypeAnnotation.ListLogicalTypeAnnotation)) {
+ return false;
+ }
+
+ // 2. LIST group must have exactly one child named "list".
+ if (listGroup.getFieldCount() != 1) {
+ return false;
+ }
+ Type middle = listGroup.getType(0);
+ if (!LIST_WRAPPER_NAME.equals(middle.getName())) {
+ return false;
+ }
+
+ // 3. The child must be a repeated group.
+ if (middle.isPrimitive() || middle.getRepetition() !=
Type.Repetition.REPEATED) {
+ return false;
+ }
+ GroupType repeatedWrapper = middle.asGroupType();
+
+ // 4. The repeated wrapper must contain exactly one child named
"element".
+ if (repeatedWrapper.getFieldCount() != 1) {
+ return false;
+ }
+
+ Type element = repeatedWrapper.getType(0);
+ return LIST_ELEMENT_NAME.equals(element.getName());
+ }
+
+ /** Returns true if the given group is a plain struct (not a Parquet list
or map). */
+ private static boolean isObjectGroup(Type type) {
+ if (type.isPrimitive()) {
+ return false;
+ }
+ GroupType groupType = type.asGroupType();
+ return groupType.getLogicalTypeAnnotation() == null;
+ }
+}
diff --git
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingReadTest.java
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingReadTest.java
index 7bad5f3909..dffb879fdf 100644
---
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingReadTest.java
+++
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingReadTest.java
@@ -281,6 +281,217 @@ public class VariantShreddingReadTest {
assertThat(result2.get(1).getArray(0).getRow(0,
1).getInt(0)).isEqualTo(5);
}
+ @ParameterizedTest
+ @ValueSource(
+ strings = {
+ "null",
+ "{\"type\":\"ROW\",\"fields\":["
+ + "
{\"name\":\"v\",\"type\":{\"type\":\"ROW\",\"fields\":["
+ + "
{\"name\":\"a\",\"type\":{\"type\":\"ROW\",\"fields\":["
+ + " {\"name\":\"a\",\"type\":\"INT\"},"
+ + " {\"name\":\"b\",\"type\":\"INT\"},"
+ + "
{\"name\":\"c\",\"type\":{\"type\":\"ROW\",\"fields\":["
+ + " {\"name\":\"d\",\"type\":\"STRING\"},"
+ + " {\"name\":\"e\",\"type\":\"INT\"}"
+ + " ]}}"
+ + " ]}},"
+ + "
{\"name\":\"b\",\"type\":{\"type\":\"ROW\",\"fields\":["
+ + " {\"name\":\"a\",\"type\":\"INT\"},"
+ + " {\"name\":\"c\",\"type\":\"INT\"}"
+ + " ]}},"
+ + " {\"name\":\"d\",\"type\":\"INT\"},"
+ + "
{\"name\":\"arr\",\"type\":{\"type\":\"ARRAY\",\"element\":{\"type\":\"ROW\",\"fields\":["
+ + " {\"name\":\"x\",\"type\":\"INT\"},"
+ + " {\"name\":\"y\",\"type\":\"INT\"}"
+ + " ]}}}"
+ + " ]}}"
+ + "]}"
+ })
+ public void testReadNestedVariantWithPruning(String shreddingSchema)
throws Exception {
+ Options options = new Options();
+ if (!shreddingSchema.equals("null")) {
+ options.set("parquet.variant.shreddingSchema", shreddingSchema);
+ }
+ ParquetFileFormat format =
+ new ParquetFileFormat(new
FileFormatFactory.FormatContext(options, 1024, 1024));
+
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "v",
DataTypes.VARIANT()));
+
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+ writeRows(
+ factory,
+ GenericRow.of(
+ GenericVariant.fromJson(
+ " {"
+ + " \"a\": {"
+ + " \"a\": 0,"
+ + " \"b\": 1,"
+ + " \"c\": {"
+ + " \"d\": \"hello\","
+ + " \"e\": 2"
+ + " }"
+ + " },"
+ + " \"b\": {"
+ + " \"a\": 3,"
+ + " \"c\": 4"
+ + " },"
+ + " \"c\": {"
+ + " \"a\": 5,"
+ + " \"c\": 6"
+ + " },"
+ + " \"d\": 7,"
+ + " \"arr\": ["
+ + "
{\"x\":10,\"y\":11,\"z\":12},"
+ + "
{\"x\":12,\"y\":13,\"z\":14}"
+ + " ]"
+ + " }")));
+
+ // case1: multiple nested projections with a missing top-level typed
key ($.c.a).
+ RowType readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a.b")
+ .field(DataTypes.STRING(), "$.a.c.d")
+ .field(DataTypes.INT(), "$.b.a")
+ .field(DataTypes.INT(), "$.c.a")
+ .build()));
+ List<InternalRow> result = readRows(format, readType);
+ assertThat(result).hasSize(1);
+ InternalRow projected = result.get(0).getRow(0, 4);
+ assertThat(projected.getInt(0)).isEqualTo(1);
+ assertThat(projected.getString(1).toString()).isEqualTo("hello");
+ assertThat(projected.getInt(2)).isEqualTo(3);
+ assertThat(projected.getInt(3)).isEqualTo(5);
+
+ // case2: read a whole shredded object ($.b).
+ readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(
+ DataTypes.ROW(
+ DataTypes.FIELD(0,
"a", DataTypes.INT()),
+ DataTypes.FIELD(1,
"c", DataTypes.INT())),
+ "$.b")
+ .build()));
+ result = readRows(format, readType);
+ assertThat(result.get(0).getRow(0, 1).getRow(0,
2).getInt(1)).isEqualTo(4);
+
+ // case3: read an object ($.c) that is not described by the shredding
schema.
+ readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(
+ DataTypes.ROW(
+ DataTypes.FIELD(0,
"a", DataTypes.INT()),
+ DataTypes.FIELD(1,
"c", DataTypes.INT())),
+ "$.c")
+ .build()));
+ result = readRows(format, readType);
+ assertThat(result.get(0).getRow(0, 1).getRow(0,
2).getInt(1)).isEqualTo(6);
+
+ // case4: array index path: nested field inside each element can be
pruned.
+ readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0].x")
+ .build()));
+ result = readRows(format, readType);
+ assertThat(result.get(0).getRow(0, 1).getInt(0)).isEqualTo(10);
+
+ // case5: missing field inside array element falls back to binary
value.
+ readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[1].z")
+ .build()));
+ result = readRows(format, readType);
+ assertThat(result.get(0).getRow(0, 1).getInt(0)).isEqualTo(14);
+
+ // case6: read the whole array element as a VARIANT.
+ readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.VARIANT(), "$.arr[1]")
+ .build()));
+ result = readRows(format, readType);
+ assertThat(result.get(0).getRow(0, 1).getVariant(0).toJson())
+ .isEqualTo("{\"x\":12,\"y\":13,\"z\":14}");
+ }
+
+ @Test
+ public void testReadHeterogeneousVariantObjectTypedAsArray() throws
Exception {
+ // The shredding schema defines "v.a" as an object, but the JSON value
is an array.
+ // Reading $.a[0].x must fall back to the binary value column.
+ Options options = new Options();
+ options.set(
+ "parquet.variant.shreddingSchema",
+
"{\"type\":\"ROW\",\"fields\":[{\"name\":\"v\",\"type\":{\"type\":\"ROW\",\"fields\":[{\"name\":\"a\",\"type\":{\"type\":\"ROW\",\"fields\":[{\"name\":\"b\",\"type\":\"INT\"}]}}]}}]}");
+ ParquetFileFormat format =
+ new ParquetFileFormat(new
FileFormatFactory.FormatContext(options, 1024, 1024));
+
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "v",
DataTypes.VARIANT()));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+ writeRows(factory,
GenericRow.of(GenericVariant.fromJson("{\"a\":[{\"x\":1}]}")));
+
+ RowType readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a[0].x")
+ .build()));
+ List<InternalRow> result = readRows(format, readType);
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getRow(0, 1).getInt(0)).isEqualTo(1);
+ }
+
+ @Test
+ public void testReadHeterogeneousVariantListTypedAsObject() throws
Exception {
+ // The shredding schema defines "v.a" as a list, but the JSON value is
an object.
+ // Reading $.a.x must fall back to the binary value column.
+ Options options = new Options();
+ options.set(
+ "parquet.variant.shreddingSchema",
+
"{\"type\":\"ROW\",\"fields\":[{\"name\":\"v\",\"type\":{\"type\":\"ROW\",\"fields\":[{\"name\":\"a\",\"type\":{\"type\":\"ARRAY\",\"element\":\"INT\"}}]}}]}");
+ ParquetFileFormat format =
+ new ParquetFileFormat(new
FileFormatFactory.FormatContext(options, 1024, 1024));
+
+ RowType writeType = DataTypes.ROW(DataTypes.FIELD(0, "v",
DataTypes.VARIANT()));
+ FormatWriterFactory factory = format.createWriterFactory(writeType);
+ writeRows(factory,
GenericRow.of(GenericVariant.fromJson("{\"a\":{\"x\":2}}")));
+
+ RowType readType =
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "v",
+
VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a.x")
+ .build()));
+ List<InternalRow> result = readRows(format, readType);
+ assertThat(result).hasSize(1);
+ assertThat(result.get(0).getRow(0, 1).getInt(0)).isEqualTo(2);
+ }
+
protected List<InternalRow> readRows(ParquetFileFormat format, RowType
rowType)
throws IOException {
List<InternalRow> result = new ArrayList<>();
diff --git
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingTypePrunerTest.java
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingTypePrunerTest.java
new file mode 100644
index 0000000000..21584c06e9
--- /dev/null
+++
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/reader/VariantShreddingTypePrunerTest.java
@@ -0,0 +1,572 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.format.parquet.reader;
+
+import org.apache.paimon.data.variant.PaimonShreddingUtils;
+import org.apache.paimon.data.variant.VariantMetadataUtils;
+import org.apache.paimon.format.parquet.ParquetSchemaConverter;
+import org.apache.paimon.format.parquet.VariantShreddingTypePruner;
+import org.apache.paimon.types.ArrayType;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.apache.parquet.schema.GroupType;
+import org.apache.parquet.schema.LogicalTypeAnnotation;
+import org.apache.parquet.schema.Type;
+import org.apache.parquet.schema.Types;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName;
+import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.BINARY;
+import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32;
+import static org.apache.parquet.schema.Type.Repetition.OPTIONAL;
+import static org.apache.parquet.schema.Type.Repetition.REPEATED;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Unit tests for {@link VariantShreddingTypePruner}. */
+public class VariantShreddingTypePrunerTest {
+
+ @Test
+ public void testObjectNestedFieldsPrunesTypedValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a.b")
+ .field(DataTypes.INT(), "$.a.c")
+ .field(DataTypes.INT(), "$.d")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(nestedObjectShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("a",
"d");
+
+ RowType physicalA = getRowType(physicalTypedValue, "a");
+ RowType physicalATypedValue = getRowType(physicalA, "typed_value");
+ assertThat(physicalATypedValue.getFieldNames()).containsExactly("b",
"c");
+ }
+
+ @Test
+ public void testObjectMissingTopLevelFieldFallsBackToValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.z")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(nestedObjectShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).contains("value");
+ assertThat(physicalV.getFieldNames()).doesNotContain("typed_value");
+ }
+
+ @Test
+ public void
testObjectMixedExistingAndMissingFieldsKeepsTypedValueAndValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a.b")
+ .field(DataTypes.INT(), "$.z")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(nestedObjectShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).contains("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("a");
+
+ RowType physicalA = getRowType(physicalTypedValue, "a");
+ RowType physicalATypedValue = getRowType(physicalA, "typed_value");
+ assertThat(physicalATypedValue.getFieldNames()).containsExactly("b");
+ }
+
+ @Test
+ public void testObjectKeepAllKeepsWholeField() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "b",
DataTypes.INT()),
+ DataTypes.FIELD(1, "c",
DataTypes.INT())),
+ "$.a")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(nestedObjectShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("a");
+ RowType physicalA = getRowType(physicalTypedValue, "a");
+ RowType physicalATypedValue = getRowType(physicalA, "typed_value");
+ assertThat(physicalATypedValue.getFieldNames()).containsExactly("b",
"c");
+ }
+
+ private static RowType nestedObjectShreddingSchema() {
+ return PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "a",
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "b",
DataTypes.INT()),
+ DataTypes.FIELD(1, "c",
DataTypes.INT()))),
+ DataTypes.FIELD(1, "d", DataTypes.INT()),
+ DataTypes.FIELD(2, "e", DataTypes.INT())));
+ }
+
+ @Test
+ public void testCaseSensitive() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.a")
+ .build();
+
+ GroupType parquetType =
+ createParquetVariantType(
+ PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "a",
DataTypes.INT()),
+ DataTypes.FIELD(1, "A",
DataTypes.INT()))));
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("a");
+ }
+
+ @Test
+ public void testArrayKeepAllArrayKeepsElementValueAndTypedValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.ARRAY(DataTypes.VARIANT()), "$.arr")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(arrayShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("arr");
+
+ RowType physicalArr = getRowType(physicalTypedValue, "arr");
+ ArrayType physicalArrList =
+ (ArrayType)
physicalArr.getTypeAt(physicalArr.getFieldIndex("typed_value"));
+ RowType physicalArrElement = (RowType)
physicalArrList.getElementType();
+ assertThat(physicalArrElement.getFieldNames()).contains("value",
"typed_value");
+ RowType physicalArrElementTypedValue = getRowType(physicalArrElement,
"typed_value");
+
assertThat(physicalArrElementTypedValue.getFieldNames()).containsExactly("x",
"y");
+ }
+
+ @Test
+ public void testArrayKeepAllElementKeepsElementValueAndTypedValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.VARIANT(), "$.arr[0]")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(arrayShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("arr");
+
+ RowType physicalArr = getRowType(physicalTypedValue, "arr");
+ ArrayType physicalArrList =
+ (ArrayType)
physicalArr.getTypeAt(physicalArr.getFieldIndex("typed_value"));
+ RowType physicalArrElement = (RowType)
physicalArrList.getElementType();
+ assertThat(physicalArrElement.getFieldNames()).contains("value",
"typed_value");
+ RowType physicalArrElementTypedValue = getRowType(physicalArrElement,
"typed_value");
+
assertThat(physicalArrElementTypedValue.getFieldNames()).containsExactly("x",
"y");
+ }
+
+ @Test
+ public void testArrayElementFieldPrunesElementTypedValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0].x")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(arrayShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("arr");
+
+ RowType physicalArr = getRowType(physicalTypedValue, "arr");
+ ArrayType physicalArrList =
+ (ArrayType)
physicalArr.getTypeAt(physicalArr.getFieldIndex("typed_value"));
+ RowType physicalArrElement = (RowType)
physicalArrList.getElementType();
+ RowType physicalArrElementTypedValue = getRowType(physicalArrElement,
"typed_value");
+
assertThat(physicalArrElementTypedValue.getFieldNames()).containsExactly("x");
+ }
+
+ @Test
+ public void testArrayElementFieldMissingFallsBackToElementValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0].z")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(arrayShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("arr");
+
+ RowType physicalArr = getRowType(physicalTypedValue, "arr");
+ ArrayType physicalArrList =
+ (ArrayType)
physicalArr.getTypeAt(physicalArr.getFieldIndex("typed_value"));
+ RowType physicalArrElement = (RowType)
physicalArrList.getElementType();
+ assertThat(physicalArrElement.getFieldNames()).contains("value");
+
assertThat(physicalArrElement.getFieldNames()).doesNotContain("typed_value");
+ }
+
+ private static RowType arrayShreddingSchema() {
+ return PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ROW(
+ DataTypes.FIELD(
+ 0,
+ "arr",
+ DataTypes.ARRAY(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "x",
DataTypes.INT()),
+ DataTypes.FIELD(1, "y",
DataTypes.INT()))))));
+ }
+
+ @Test
+ public void testArrayPrimitiveElementKeepAllKeepsElementShreddingRow() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0]")
+ .build();
+
+ GroupType parquetType =
createParquetVariantType(primitiveArrayShreddingSchema());
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ RowType physicalTypedValue = getRowType(physicalV, "typed_value");
+ assertThat(physicalTypedValue.getFieldNames()).containsExactly("arr");
+
+ RowType physicalArr = getRowType(physicalTypedValue, "arr");
+ ArrayType physicalArrList =
+ (ArrayType)
physicalArr.getTypeAt(physicalArr.getFieldIndex("typed_value"));
+ RowType physicalArrElement = (RowType)
physicalArrList.getElementType();
+ assertThat(physicalArrElement.getFieldNames()).contains("value",
"typed_value");
+
assertThat(physicalArrElement.getTypeAt(physicalArrElement.getFieldIndex("typed_value")))
+ .isEqualTo(DataTypes.INT());
+ }
+
+ @Test
+ public void testTopLevelArrayElementFieldPrunesElementTypedValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$[0].x")
+ .build();
+
+ GroupType parquetType =
+ createParquetVariantType(
+ PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ARRAY(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "x",
DataTypes.INT()),
+ DataTypes.FIELD(1, "y",
DataTypes.INT())))));
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ ArrayType physicalList =
+ (ArrayType)
physicalV.getTypeAt(physicalV.getFieldIndex("typed_value"));
+ RowType physicalElement = (RowType) physicalList.getElementType();
+ RowType physicalElementTypedValue = getRowType(physicalElement,
"typed_value");
+
assertThat(physicalElementTypedValue.getFieldNames()).containsExactly("x");
+ }
+
+ @Test
+ public void testTopLevelArrayKeepAllVariantKeepsElementValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.VARIANT(), "$[0]")
+ .build();
+
+ GroupType parquetType =
+ createParquetVariantType(
+ PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ARRAY(DataTypes.VARIANT())));
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ assertThat(physicalV.getFieldNames()).contains("value");
+ ArrayType physicalList =
+ (ArrayType)
physicalV.getTypeAt(physicalV.getFieldIndex("typed_value"));
+ RowType physicalElement = (RowType) physicalList.getElementType();
+ assertThat(physicalElement.getFieldNames()).containsExactly("value");
+ }
+
+ @Test
+ public void testTopLevelArrayNestedProjectionKeepsWholeList() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$[0].x")
+ .build();
+
+ GroupType parquetType =
+ createParquetVariantType(
+ PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ARRAY(DataTypes.VARIANT())));
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ // The element has no typed_value column, so we cannot prune inside it.
+ // The whole list must be retained and the assembler reads x from the
binary value.
+ assertThat(physicalV.getFieldNames()).doesNotContain("value");
+ ArrayType physicalList =
+ (ArrayType)
physicalV.getTypeAt(physicalV.getFieldIndex("typed_value"));
+ RowType physicalElement = (RowType) physicalList.getElementType();
+ assertThat(physicalElement.getFieldNames()).containsExactly("value");
+ }
+
+ @Test
+ public void testMixedArrayAndObjectProjectionRetainsValue() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$[0].x")
+ .field(DataTypes.INT(), "$.y")
+ .build();
+
+ GroupType parquetType =
+ createParquetVariantType(
+ PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ARRAY(
+ DataTypes.ROW(
+ DataTypes.FIELD(0, "x",
DataTypes.INT()),
+ DataTypes.FIELD(1, "z",
DataTypes.INT())))));
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ RowType physicalV = toRowType(clipped);
+
+ // The parent value fallback must be kept because the object
projection $.y cannot be
+ // satisfied from the list-typed typed_value columns.
+ assertThat(physicalV.getFieldNames()).contains("value");
+ assertThat(physicalV.getFieldNames()).contains("typed_value");
+
+ ArrayType physicalList =
+ (ArrayType)
physicalV.getTypeAt(physicalV.getFieldIndex("typed_value"));
+ RowType physicalElement = (RowType) physicalList.getElementType();
+ RowType physicalElementTypedValue = getRowType(physicalElement,
"typed_value");
+
assertThat(physicalElementTypedValue.getFieldNames()).containsExactly("x");
+ }
+
+ private static RowType primitiveArrayShreddingSchema() {
+ return PaimonShreddingUtils.variantShreddingSchema(
+ DataTypes.ROW(DataTypes.FIELD(0, "arr",
DataTypes.ARRAY(DataTypes.INT()))));
+ }
+
+ @Test
+ public void testTwoLevelArrayGroupElementKeepsWholeList() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0].x")
+ .build();
+
+ GroupType parquetType = createTwoLevelArrayVariantType();
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ GroupType actual =
+ clipped.asGroupType()
+ .getType("typed_value")
+ .asGroupType()
+ .getType(0)
+ .asGroupType()
+ .getType(1)
+ .asGroupType();
+ GroupType expected =
+ parquetType
+ .asGroupType()
+ .getType("typed_value")
+ .asGroupType()
+ .getType(0)
+ .asGroupType()
+ .getType(1)
+ .asGroupType();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ private static GroupType createTwoLevelArrayVariantType() {
+ // x shredding row: value + typed_value
+ GroupType xShreddingRow =
+ Types.buildGroup(Type.Repetition.REQUIRED)
+ .optional(BINARY)
+ .named("value")
+ .optional(INT32)
+ .named("typed_value")
+ .named("x");
+
+ // y shredding row: value + typed_value
+ GroupType yShreddingRow =
+ Types.buildGroup(Type.Repetition.REQUIRED)
+ .optional(BINARY)
+ .named("value")
+ .optional(INT32)
+ .named("typed_value")
+ .named("y");
+
+ // element shredding row: value + typed_value(x, y)
+ GroupType elementTypedValue =
+ Types.buildGroup(OPTIONAL)
+ .addField(xShreddingRow)
+ .addField(yShreddingRow)
+ .named("typed_value");
+
+ GroupType elementRow =
+ Types.buildGroup(REPEATED)
+ .optional(BINARY)
+ .named("value")
+ .addField(elementTypedValue)
+ .named("element");
+
+ // Two-level list: the list group's immediate child is the repeated
element row.
+ GroupType arrList =
+ Types.buildGroup(OPTIONAL)
+ .as(LogicalTypeAnnotation.listType())
+ .addField(elementRow)
+ .named("typed_value");
+
+ // arr shredding row: value + typed_value(list)
+ GroupType arrShreddingRow =
+ Types.buildGroup(Type.Repetition.REQUIRED)
+ .optional(BINARY)
+ .named("value")
+ .addField(arrList)
+ .named("arr");
+
+ GroupType typedValue =
+
Types.buildGroup(OPTIONAL).addField(arrShreddingRow).named("typed_value");
+
+ return Types.buildGroup(OPTIONAL)
+ .required(BINARY)
+ .named("metadata")
+ .required(BINARY)
+ .named("value")
+ .addField(typedValue)
+ .named("v");
+ }
+
+ @Test
+ public void testTwoLevelPrimitiveArrayKeepAllKeepsWholeList() {
+ RowType variantRowType =
+ VariantMetadataUtils.VariantRowTypeBuilder.builder()
+ .field(DataTypes.INT(), "$.arr[0]")
+ .build();
+
+ GroupType parquetType = createTwoLevelPrimitiveArrayVariantType();
+ Type clipped = VariantShreddingTypePruner.clip(variantRowType,
parquetType);
+ GroupType actual =
+ clipped.asGroupType()
+ .getType("typed_value")
+ .asGroupType()
+ .getType(0)
+ .asGroupType()
+ .getType(1)
+ .asGroupType();
+ GroupType expected =
+ parquetType
+ .asGroupType()
+ .getType("typed_value")
+ .asGroupType()
+ .getType(0)
+ .asGroupType()
+ .getType(1)
+ .asGroupType();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ private static GroupType createTwoLevelPrimitiveArrayVariantType() {
+ // Primitive array elements are still wrapped in a shredding row.
+ GroupType elementRow =
+ Types.buildGroup(Type.Repetition.REPEATED)
+ .optional(BINARY)
+ .id(0)
+ .named("value")
+ .optional(INT32)
+ .id(1)
+ .named("typed_value")
+ .named("element")
+ .withId(123);
+
+ // Two-level list: the list group's immediate child is the repeated
element row.
+ GroupType arrList =
+ Types.buildGroup(Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.listType())
+ .addField(elementRow)
+ .named("typed_value")
+ .withId(1);
+
+ // arr shredding row: value + typed_value(list)
+ GroupType arrShreddingRow =
+ Types.buildGroup(Type.Repetition.REQUIRED)
+ .optional(BINARY)
+ .id(0)
+ .named("value")
+ .addField(arrList)
+ .named("arr")
+ .withId(0);
+
+ GroupType typedValue =
+ Types.buildGroup(Type.Repetition.OPTIONAL)
+ .addField(arrShreddingRow)
+ .named("typed_value")
+ .withId(2);
+
+ return Types.buildGroup(Type.Repetition.OPTIONAL)
+ .required(PrimitiveTypeName.BINARY)
+ .id(0)
+ .named("metadata")
+ .required(PrimitiveTypeName.BINARY)
+ .id(1)
+ .named("value")
+ .addField(typedValue)
+ .named("v")
+ .withId(0);
+ }
+
+ private static GroupType createParquetVariantType(RowType shreddingSchema)
{
+ DataField field = new DataField(0, "v", shreddingSchema);
+ return
ParquetSchemaConverter.convertToParquetType(field).asGroupType();
+ }
+
+ private static RowType toRowType(Type type) {
+ return (RowType)
ParquetSchemaConverter.convertToPaimonField(type).type();
+ }
+
+ private static RowType getRowType(RowType rowType, String fieldName) {
+ return (RowType) rowType.getTypeAt(rowType.getFieldIndex(fieldName));
+ }
+}