diff --git a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java index 2874849bb56a..ef421706d006 100644 --- a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java +++ b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java @@ -77,6 +77,7 @@ import static org.apache.paimon.data.columnar.ColumnVectorUtils.createParquetWritableColumnVector; import static org.apache.paimon.format.parquet.ParquetSchemaConverter.PAIMON_SCHEMA; +import static org.apache.paimon.format.parquet.ParquetSchemaConverter.isThreeLevelList; import static org.apache.paimon.format.parquet.ParquetSchemaConverter.parquetListElementType; import static org.apache.paimon.format.parquet.ParquetSchemaConverter.parquetMapKeyValueType; import static org.apache.paimon.format.parquet.reader.ParquetReaderUtil.buildFieldsList; @@ -336,11 +337,10 @@ private Type clipParquetType(DataType readType, Type parquetType) { // There are two representations for array type in parquet. // See link: // https://impala.apache.org/docs/build/html/topics/impala_parquet_array_resolution.html. - int level = arrayGroup.getType(0) instanceof GroupType ? 3 : 2; Type elementType = clipParquetType(elementReadType, parquetListElementType(arrayGroup)); - if (level == 3) { + if (isThreeLevelList(arrayGroup)) { // In case that the name in middle level is not "list". Type groupMiddle = new GroupType( diff --git a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSchemaConverter.java b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSchemaConverter.java index 912baec0721c..45b7e6673f84 100644 --- a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSchemaConverter.java +++ b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetSchemaConverter.java @@ -50,6 +50,7 @@ import java.util.List; import java.util.stream.Collectors; +import static org.apache.paimon.utils.Preconditions.checkArgument; import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY; import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT32; import static org.apache.parquet.schema.PrimitiveType.PrimitiveTypeName.INT64; @@ -63,6 +64,8 @@ public class ParquetSchemaConverter { public static final String MAP_KEY_NAME = "key"; public static final String MAP_VALUE_NAME = "value"; public static final String LIST_ELEMENT_NAME = "element"; + private static final String LIST_WRAPPER_NAME = "list"; + private static final String LEGACY_LIST_ARRAY_NAME = "array"; /** * Whether {@code type} is an unsigned integer. Such a column stores a signed value whose bits @@ -490,20 +493,107 @@ public static DataField convertToPaimonField(Type parquetType) { return new DataField(parquetType.getId().intValue(), parquetType.getName(), paimonDataType); } + /** Returns true if the given group is annotated as a Parquet LIST logical type. */ + public static boolean isList(GroupType listType) { + return listType.getLogicalTypeAnnotation() + instanceof LogicalTypeAnnotation.ListLogicalTypeAnnotation; + } + + /** + * Returns true if the given group is a three-level Parquet list. + * + *
In a three-level list the immediate repeated child is a wrapper group whose single + * non-repeated child is the actual element type. This covers the canonical layout ({@code list + * -> element}) as well as legacy wrappers such as Hive's {@code bag} layout. + * + *
This corresponds to the Parquet spec's backward-compatibility Rule 5: a repeated + * group that contains exactly one non-repeated child is a wrapper, unless it matches one of + * Rules 1-4. + * + *
The compatibility encodings that are not three-level are: + * + *
See the Parquet spec: LogicalTypes#Backward-compatibility-rules
+ */
+ public static boolean isThreeLevelList(GroupType listType) {
+ if (!isList(listType)) {
+ return false;
+ }
+
+ // A list must have exactly one repeated child (the middle level).
+ if (listType.getFieldCount() != 1) {
+ return false;
+ }
+ Type middle = listType.getType(0);
+ if (middle.isPrimitive() || middle.getRepetition() != Type.Repetition.REPEATED) {
+ return false;
+ }
+ GroupType repeatedGroup = middle.asGroupType();
+
+ // Rule 5: the repeated group is a wrapper containing exactly one non-repeated child.
+ if (repeatedGroup.getFieldCount() != 1
+ || repeatedGroup.getType(0).getRepetition() == Type.Repetition.REPEATED) {
+ return false;
+ }
+
+ // Rule 4: legacy "array" and " The canonical layout is described in the Parquet spec: LogicalTypes#Lists
+ */
+ public static boolean isCanonicalList(Type type) {
+ if (type.isPrimitive()) {
+ return false;
+ }
+
+ GroupType listGroup = type.asGroupType();
+ if (!isThreeLevelList(listGroup)) {
+ return false;
+ }
+
+ Type middle = listGroup.getType(0);
+ Type element = middle.asGroupType().getType(0);
+ return LIST_WRAPPER_NAME.equals(middle.getName())
+ && LIST_ELEMENT_NAME.equals(element.getName());
+ }
+
+ /**
+ * Returns the element type of the given LIST-annotated group according to the Parquet spec's
+ * backward-compatibility rules for lists.
+ *
+ * For a three-level list (Rule 5) the returned type is the single child of the repeated
+ * wrapper. For Rules 1-4 the repeated field itself is returned because it is the element type.
+ */
public static Type parquetListElementType(GroupType listType) {
- int level = listType.getType(0) instanceof GroupType ? 3 : 2;
- if (level == 3) {
- // Level 3 representation of list type.
- // List type should only have one middle group type, which is repeated, and one element
- // type, which is optional.
+ checkArgument(
+ listType.getLogicalTypeAnnotation()
+ instanceof LogicalTypeAnnotation.ListLogicalTypeAnnotation,
+ "Expected LIST-annotated group but got: %s",
+ listType);
+
+ if (isThreeLevelList(listType)) {
return listType.getType(0).asGroupType().getType(0);
- } else if (level == 2) {
- // Level 2 representation of list type
- return listType.getType(0);
- } else {
- throw new UnsupportedOperationException(
- "Parquet list type only have two level representation and three level representation.");
}
+
+ return listType.getType(0);
}
public static Pair_tuple" encodings are not wrappers; the repeated
+ // group itself is the element type.
+ return !LEGACY_LIST_ARRAY_NAME.equals(repeatedGroup.getName())
+ && !(listType.getName() + "_tuple").equals(repeatedGroup.getName());
+ }
+
+ /**
+ * Returns true if the given group follows the canonical three-level Parquet list layout ({@code
+ * list -> element}).
+ *
+ *
_tuple" with one field is the element type.
+ @Test
+ public void testParquetListElementTypeLegacyListTupleWrapper() {
+ GroupType tupleGroup =
+ Types.buildGroup(Type.Repetition.REPEATED)
+ .required(BINARY)
+ .as(LogicalTypeAnnotation.stringType())
+ .named("str")
+ .named("my_list_tuple");
+ GroupType list =
+ Types.buildGroup(Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.listType())
+ .addField(tupleGroup)
+ .named("my_list");
+
+ Type element = ParquetSchemaConverter.parquetListElementType(list);
+ assertThat(element.isPrimitive()).isEqualTo(false);
+ assertThat(element.getName()).isEqualTo("my_list_tuple");
+ assertThat(element.asGroupType().getFieldCount()).isEqualTo(1);
+ assertThat(element.asGroupType().getType(0).getName()).isEqualTo("str");
+ }
+
+ // Rule 5: a repeated group with a single non-repeated field that is neither "array" nor
+ // "
_tuple" unwraps to the single child (e.g. Hive's bag/array_element encoding).
+ @Test
+ public void testParquetListElementTypeLegacyBagWrapper() {
+ GroupType bagGroup =
+ Types.buildGroup(Type.Repetition.REPEATED)
+ .optional(INT32)
+ .named("array_element")
+ .named("bag");
+ GroupType list =
+ Types.buildGroup(Type.Repetition.OPTIONAL)
+ .as(LogicalTypeAnnotation.listType())
+ .addField(bagGroup)
+ .named("arr");
+
+ Assertions.assertThat(ParquetSchemaConverter.isThreeLevelList(list)).isTrue();
+ Assertions.assertThat(ParquetSchemaConverter.isCanonicalList(list)).isFalse();
+
+ Type element = ParquetSchemaConverter.parquetListElementType(list);
+ assertThat(element.isPrimitive()).isEqualTo(true);
+ assertThat(element.getName()).isEqualTo("array_element");
+ }
}