This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git
The following commit(s) were added to refs/heads/master by this push:
new b1e2d0358 GH-3816: Automatically detect list encodings in
AvroReadSupport (#3753)
b1e2d0358 is described below
commit b1e2d0358c4f8a652a44307a897944c4ca1e5e73
Author: Claire McGinty <[email protected]>
AuthorDate: Thu Sep 24 00:47:18 2026 -0400
GH-3816: Automatically detect list encodings in AvroReadSupport (#3753)
### Rationale for this change
parquet-avro supports writing both "old" and "new" list encodings via the
[parquet.avro.write-old-list-structure](https://github.com/apache/parquet-java/blob/apache-parquet-1.18.0/parquet-avro/src/main/java/org/apache/parquet/avro/AvroWriteSupport.java#L72-L73)
config. "old" encodings (aka "2-level"), which wrap the list in a `repeated
group array` schema, are the default; "new" encodings (aka "3-level") are
opt-in.
On the reader side, if you're using `ParquetAvroReader` to read data that
was written using `ParquetAvroWriter`, and don't specify a projection, both
type sof list encoding get parsed automatically from a combination of the file
schema + the `parquet.avro.schema` metadata key. There's no need to set
`parquet.avro.write-old-list-structure` key in your Configuration.
However, if you're either:
- specifying a projection (`AvroReadSupport.setRequestedProjection(...)`),
or
- reading data _not_ written using ParquetAvroWriter (and thus not
containing the `parquet.avro.schema` metadata key),
3-levle list encodings will not be parsed correctly - the reader will
inject an extra nested record, named `element`, into the list item type.
As a reader this introduces some pain, since you have to look up the
underlying file metadata of the upstream Parquet file, and risk reading
incorrect data. This PR attempts to automatically detect new list encodings
based on the writer file schema.
lmk what you think of this change. Automatic inference is always a bit
risky, but I tried to be conservative with the approach (only set the list
structure property if _all_ list fields in the schema use 3-level encoding;
don't override `parquet.avro.write-old-list-structure` if the user is already
setting it). any ideas for a better approach here are welcome - this is
becoming more of a pain point as 3-level lists become a more popular option
among other writer sdks.
### What changes are included in this PR?
A new read configuration property
`parquet.avro.read.autoDetectListStructure` (defaulting to true) that will
instruct AvroReadSupport to automatically set List configuration properties
based on parsing the writer file schema.
### Are these changes tested?
Yes, unit tests + locally on real data.
### Are there any user-facing changes?
Yes, since the new property defaults to `true` - it would impact anyone
who's reading 3-level list data without setting the
`parquet.avro.write-old-list-structure` key and who's relying on/working around
the incorrectly formatted data (e.g. `{"locations": [{"element": {"latitude":
0.0, "longitude": 180.0}}, ...]}` instead of `{"locations": [{"latitude": 0.0,
"longitude": 180.0}, ...]}` .
additionally, this change also modifies the underlying Configuration object
to add the properties.
Closes #3816
---
parquet-avro/README.md | 17 +-
.../org/apache/parquet/avro/AvroReadSupport.java | 85 ++++-
.../parquet/avro/TestArrayCompatibility.java | 379 +++++++++++++++++++++
3 files changed, 471 insertions(+), 10 deletions(-)
diff --git a/parquet-avro/README.md b/parquet-avro/README.md
index 644be9bfa..9bed4a6b3 100644
--- a/parquet-avro/README.md
+++ b/parquet-avro/README.md
@@ -26,14 +26,15 @@ Apache Avro integration
### Configuration for reading
-| Name | Type | Description
|
-|-----------------------------------------|-----------|----------------------------------------------------------------------|
-| `parquet.avro.data.supplier` | `Class` | The implementation of
the interface org.apache.parquet.avro.AvroDataSupplier. Available
implementations in the library: GenericDataSupplier, ReflectDataSupplier,
SpecificDataSupplier.<br/>The default value is
`org.apache.parquet.avro.SpecificDataSupplier` |
-| `parquet.avro.read.schema` | `String` | The Avro schema to be
used for reading. It shall be compatible with the file schema. The file schema
will be used directly if not set. |
-| `parquet.avro.projection` | `String` | The Avro schema to be
used for projection. |
-| `parquet.avro.compatible` | `boolean` | Flag for compatibility
mode. `true` for materializing Avro `IndexedRecord` objects, `false` for
materializing the related objects for either generic, specific, or reflect
records.<br/>The default value is `true`. |
-| `parquet.avro.readInt96AsFixed` | `boolean` | Flag for handling the
`INT96` Parquet types. `true` for converting it to the `fixed` Avro type,
`false` for not handling `INT96` types (throwing exception).<br/>The default
value is `false`.<br/>**NOTE: The `INT96` Parquet type is deprecated. This
option is only to support old data.** |
-| `parquet.avro.serializable.classes` | `String` | List of the fully
qualified class names separated by ',' that may be referenced from the Avro
schema by "java-class" or "java-key-class" and are allowed to be loaded. |
+| Name | Type | Description
|
+|---------------------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `parquet.avro.data.supplier` | `Class` | The implementation
of the interface org.apache.parquet.avro.AvroDataSupplier. Available
implementations in the library: GenericDataSupplier, ReflectDataSupplier,
SpecificDataSupplier.<br/>The default value is
`org.apache.parquet.avro.SpecificDataSupplier` |
+| `parquet.avro.read.schema` | `String` | The Avro schema to
be used for reading. It shall be compatible with the file schema. The file
schema will be used directly if not set.
|
+| `parquet.avro.projection` | `String` | The Avro schema to
be used for projection.
|
+| `parquet.avro.compatible` | `boolean` | Flag for
compatibility mode. `true` for materializing Avro `IndexedRecord` objects,
`false` for materializing the related objects for either generic, specific, or
reflect records.<br/>The default value is `true`.
|
+| `parquet.avro.readInt96AsFixed` | `boolean` | Flag for handling
the `INT96` Parquet types. `true` for converting it to the `fixed` Avro type,
`false` for not handling `INT96` types (throwing exception).<br/>The default
value is `false`.<br/>**NOTE: The `INT96` Parquet type is deprecated. This
option is only to support old data.** |
+| `parquet.avro.serializable.classes` | `String` | List of the fully
qualified class names separated by ',' that may be referenced from the Avro
schema by "java-class" or "java-key-class" and are allowed to be loaded.
|
+| `parquet.avro.read.autoDetectListStructure` | `boolean` | Automatically
detect whether the write schema uses 2- or 3-level list encoding and converts
the projection/read schema accordingly.<br/>The default value is `true`.
|
### Configuration for writing
diff --git
a/parquet-avro/src/main/java/org/apache/parquet/avro/AvroReadSupport.java
b/parquet-avro/src/main/java/org/apache/parquet/avro/AvroReadSupport.java
index 6d7ca398a..ee8f9ab27 100644
--- a/parquet-avro/src/main/java/org/apache/parquet/avro/AvroReadSupport.java
+++ b/parquet-avro/src/main/java/org/apache/parquet/avro/AvroReadSupport.java
@@ -28,10 +28,14 @@ import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.util.ReflectionUtils;
import org.apache.parquet.conf.HadoopParquetConfiguration;
import org.apache.parquet.conf.ParquetConfiguration;
+import org.apache.parquet.conf.PlainParquetConfiguration;
import org.apache.parquet.hadoop.api.ReadSupport;
import org.apache.parquet.hadoop.util.ConfigurationUtil;
import org.apache.parquet.io.api.RecordMaterializer;
+import org.apache.parquet.schema.GroupType;
+import org.apache.parquet.schema.LogicalTypeAnnotation;
import org.apache.parquet.schema.MessageType;
+import org.apache.parquet.schema.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -63,6 +67,11 @@ public class AvroReadSupport<T> extends ReadSupport<T> {
public static final String READ_INT96_AS_FIXED =
"parquet.avro.readInt96AsFixed";
public static final boolean READ_INT96_AS_FIXED_DEFAULT = false;
+ // Automatically detect whether a Parquet file uses 2-level or 3-level
encoding;
+ // Ignored if AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE is also set
+ public static final String AUTO_DETECT_LIST_STRUCTURE =
"parquet.avro.read.autoDetectListStructure";
+ static final boolean AUTO_DETECT_LIST_STRUCTURE_DEFAULT = true;
+
/**
* List of the fully qualified class names separated by ',' that may be
referenced from the Avro schema by
* "java-class" or "java-key-class" and are allowed to be loaded.
@@ -131,7 +140,8 @@ public class AvroReadSupport<T> extends ReadSupport<T> {
String requestedProjectionString =
configuration.get(AVRO_REQUESTED_PROJECTION);
if (requestedProjectionString != null) {
Schema avroRequestedProjection = new
Schema.Parser().parse(requestedProjectionString);
- projection = new
AvroSchemaConverter(configuration).convert(avroRequestedProjection);
+ projection = new
AvroSchemaConverter(getDerivedListEncodingConf(configuration, fileSchema))
+ .convert(avroRequestedProjection);
}
String avroReadSchema = configuration.get(AVRO_READ_SCHEMA);
@@ -176,7 +186,8 @@ public class AvroReadSupport<T> extends ReadSupport<T> {
avroSchema = new
Schema.Parser().parse(keyValueMetaData.get(OLD_AVRO_SCHEMA_METADATA_KEY));
} else {
// default to converting the Parquet schema into an Avro schema
- avroSchema = new
AvroSchemaConverter(configuration).convert(parquetSchema);
+ avroSchema = new
AvroSchemaConverter(getDerivedListEncodingConf(configuration, fileSchema))
+ .convert(parquetSchema);
}
GenericData model = getDataModel(configuration, avroSchema);
@@ -230,4 +241,74 @@ public class AvroReadSupport<T> extends ReadSupport<T> {
return ReflectionUtils.newInstance(suppClass,
ConfigurationUtil.createHadoopConfiguration(conf))
.get();
}
+
+ // Returns a ParquetConfiguration with appropriate list-decoding properties
set, inferred from
+ // the file schema as well as user-supplied Configuration properties.
+ // If no configuration changes are required, the original
ParquetConfiguration object will be;
+ // returned; otherwise, a copy will be created with the correct properties.
+ private static ParquetConfiguration getDerivedListEncodingConf(
+ ParquetConfiguration configuration, MessageType fileSchema) {
+ final boolean autoDetectListStructure =
+ configuration.getBoolean(AUTO_DETECT_LIST_STRUCTURE,
AUTO_DETECT_LIST_STRUCTURE_DEFAULT);
+
+ if (!autoDetectListStructure
+ || configuration.get(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE) != null
+ || configuration.get(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS) !=
null
+ || !writesNewListStructure(fileSchema)) {
+ return configuration;
+ }
+
+ // Avoid mutating the original Configuration by creating a copy, with new
properties set
+ final ParquetConfiguration copiedConfiguration = new
PlainParquetConfiguration();
+ for (Map.Entry<String, String> property : configuration) {
+ copiedConfiguration.set(property.getKey(), property.getValue());
+ }
+ copiedConfiguration.setBoolean(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE,
false);
+
copiedConfiguration.setBoolean(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS,
false);
+
+ return copiedConfiguration;
+ }
+
+ private static boolean writesNewListStructure(MessageType schema) {
+ return Boolean.TRUE.equals(allListStructuresAreThreeLevel(schema));
+ }
+
+ // Given a Parquet schema, return true only if the schema:
+ // - contains one or more List fields
+ // - encodes every List field using 3-level list structure
+ private static Boolean allListStructuresAreThreeLevel(Type type) {
+ if (type.isPrimitive()) {
+ return null;
+ }
+ GroupType group = type.asGroupType();
+ if (group.getLogicalTypeAnnotation() instanceof
LogicalTypeAnnotation.ListLogicalTypeAnnotation) {
+ if (group.isRepetition(Type.Repetition.REPEATED) ||
group.getFieldCount() != 1) {
+ return false;
+ }
+ Type repeated = group.getType(0);
+ if (repeated.isPrimitive()
+ || !repeated.isRepetition(Type.Repetition.REPEATED)
+ || !repeated.getName().equals("list")
+ || repeated.asGroupType().getFieldCount() != 1) {
+ return false;
+ }
+ Type element = repeated.asGroupType().getType(0);
+ if (element.isRepetition(Type.Repetition.REPEATED)
+ || !element.getName().equals("element")) {
+ return false;
+ }
+ return !Boolean.FALSE.equals(allListStructuresAreThreeLevel(element));
+ }
+ Boolean result = null;
+ for (Type field : group.getFields()) {
+ Boolean fieldListStructuresAreThreeLevel =
allListStructuresAreThreeLevel(field);
+ if (Boolean.FALSE.equals(fieldListStructuresAreThreeLevel)) {
+ return false;
+ }
+ if (Boolean.TRUE.equals(fieldListStructuresAreThreeLevel)) {
+ result = true;
+ }
+ }
+ return result;
+ }
}
diff --git
a/parquet-avro/src/test/java/org/apache/parquet/avro/TestArrayCompatibility.java
b/parquet-avro/src/test/java/org/apache/parquet/avro/TestArrayCompatibility.java
index 3505b0265..242fd2fcf 100644
---
a/parquet-avro/src/test/java/org/apache/parquet/avro/TestArrayCompatibility.java
+++
b/parquet-avro/src/test/java/org/apache/parquet/avro/TestArrayCompatibility.java
@@ -51,11 +51,14 @@ public class TestArrayCompatibility extends
DirectWriterTest {
public static final Configuration OLD_BEHAVIOR_CONF = new Configuration();
public static final Configuration NEW_BEHAVIOR_CONF = new Configuration();
+ public static final Configuration AUTO_DETECT_CONF = new Configuration();
@BeforeAll
public static void setupNewBehaviorConfiguration() {
OLD_BEHAVIOR_CONF.setBoolean(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS,
true);
+ OLD_BEHAVIOR_CONF.setBoolean(AvroReadSupport.AUTO_DETECT_LIST_STRUCTURE,
false);
NEW_BEHAVIOR_CONF.setBoolean(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS,
false);
+ AUTO_DETECT_CONF.setBoolean(AvroReadSupport.AUTO_DETECT_LIST_STRUCTURE,
true);
}
@Test
@@ -1256,6 +1259,382 @@ public class TestArrayCompatibility extends
DirectWriterTest {
.hasMessage("Parquet/Avro schema mismatch. Avro field 'element' not
found.");
}
+ @Test
+ public void testAutoDetectThreeLevelListEncoding() throws Exception {
+ Path test = writeDirect(
+ "message AutoDetectThreeLevel {"
+ + " optional group locations (LIST) {"
+ + " repeated group list {"
+ + " required group element {"
+ + " required double latitude;"
+ + " required double longitude;"
+ + " }"
+ + " }"
+ + " }"
+ + "}",
+ rc -> {
+ rc.startMessage();
+ rc.startField("locations", 0);
+
+ rc.startGroup();
+ rc.startField("list", 0);
+
+ rc.startGroup();
+ rc.startField("element", 0);
+ rc.startGroup();
+ rc.startField("latitude", 0);
+ rc.addDouble(0.0);
+ rc.endField("latitude", 0);
+ rc.startField("longitude", 1);
+ rc.addDouble(180.0);
+ rc.endField("longitude", 1);
+ rc.endGroup();
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.startGroup();
+ rc.startField("element", 0);
+ rc.startGroup();
+ rc.startField("latitude", 0);
+ rc.addDouble(0.0);
+ rc.endField("latitude", 0);
+ rc.startField("longitude", 1);
+ rc.addDouble(0.0);
+ rc.endField("longitude", 1);
+ rc.endGroup();
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.endField("list", 0);
+ rc.endGroup();
+
+ rc.endField("locations", 0);
+ rc.endMessage();
+ });
+
+ Schema location = record(
+ "element",
+ field("latitude", primitive(Schema.Type.DOUBLE)),
+ field("longitude", primitive(Schema.Type.DOUBLE)));
+
+ // without auto-detect, old behavior wraps repeated group in an extra
"element" record,
+ // e.g. {"locations": [{"element": {"latitude": 0.0, ... }}]}
+ Schema elementRecord = record("list", field("element", location));
+ Schema oldSchema = record("AutoDetectThreeLevel",
optionalField("locations", array(elementRecord)));
+ GenericRecord oldRecord = instance(
+ oldSchema,
+ "locations",
+ Arrays.asList(
+ instance(elementRecord, "element", instance(location, "latitude",
0.0, "longitude", 180.0)),
+ instance(elementRecord, "element", instance(location, "latitude",
0.0, "longitude", 0.0))));
+
+ Configuration nonAutoDetectConf = new Configuration(OLD_BEHAVIOR_CONF);
+ assertReaderContains(new AvroParquetReader<>(nonAutoDetectConf, test),
oldSchema, oldRecord);
+
+ // with auto-detect, repeated group is represented correctly,
+ // e.g. {"locations": [{"latitude": 0.0, ... }]}
+ Schema newSchema = record("AutoDetectThreeLevel",
optionalField("locations", array(location)));
+ GenericRecord newRecord = instance(
+ newSchema,
+ "locations",
+ Arrays.asList(
+ instance(location, "latitude", 0.0, "longitude", 180.0),
+ instance(location, "latitude", 0.0, "longitude", 0.0)));
+
+ assertReaderContains(autoDetectReader(test), newSchema, newRecord);
+ }
+
+ @Test
+ public void testAutoDetectThreeLevelListEncodingWithProjection() throws
Exception {
+ Path test = writeDirect(
+ "message AutoDetectThreeLevelProjection {"
+ + " required int64 year;"
+ + " optional group locations (LIST) {"
+ + " repeated group list {"
+ + " required group element {"
+ + " required double latitude;"
+ + " required double longitude;"
+ + " }"
+ + " }"
+ + " }"
+ + "}",
+ rc -> {
+ rc.startMessage();
+ rc.startField("year", 0);
+ rc.addLong(2010L);
+ rc.endField("year", 0);
+
+ rc.startField("locations", 1);
+
+ rc.startGroup();
+ rc.startField("list", 0);
+
+ rc.startGroup();
+ rc.startField("element", 0);
+ rc.startGroup();
+ rc.startField("latitude", 0);
+ rc.addDouble(0.0);
+ rc.endField("latitude", 0);
+ rc.startField("longitude", 1);
+ rc.addDouble(180.0);
+ rc.endField("longitude", 1);
+ rc.endGroup();
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.endField("list", 0);
+ rc.endGroup();
+
+ rc.endField("locations", 1);
+ rc.endMessage();
+ });
+
+ Schema location = record(
+ "element",
+ field("latitude", primitive(Schema.Type.DOUBLE)),
+ field("longitude", primitive(Schema.Type.DOUBLE)));
+
+ Schema projectionSchema = record("AutoDetectThreeLevelProjection",
optionalField("locations", array(location)));
+
+ Configuration autoDetectConf = new Configuration(AUTO_DETECT_CONF);
+ AvroReadSupport.setRequestedProjection(autoDetectConf, projectionSchema);
+
+ GenericRecord expectedRecord = instance(
+ projectionSchema, "locations", Arrays.asList(instance(location,
"latitude", 0.0, "longitude", 180.0)));
+
+ assertReaderContains(new AvroParquetReader<>(autoDetectConf, test),
projectionSchema, expectedRecord);
+ }
+
+ @Test
+ public void testAutoDetectMixedSiblingListStructuresUsesOldBehavior() {
+ MessageType fileSchema = MessageTypeParser.parseMessageType("message
MixedSiblingListStructures {"
+ + " optional group three_level (LIST) {"
+ + " repeated group list {"
+ + " required int32 element;"
+ + " }"
+ + " }"
+ + " optional group two_level (LIST) {"
+ + " repeated int32 element;"
+ + " }"
+ + "}");
+
+ assertListStructureIsNotAutoDetected(fileSchema);
+ }
+
+ @Test
+ public void testAutoDetectMixedNestedListStructuresUsesOldBehavior() {
+ MessageType fileSchema = MessageTypeParser.parseMessageType("message
MixedNestedListStructures {"
+ + " optional group outer_list (LIST) {"
+ + " repeated group list {"
+ + " required group element (LIST) {"
+ + " repeated int32 element;"
+ + " }"
+ + " }"
+ + " }"
+ + "}");
+
+ assertListStructureIsNotAutoDetected(fileSchema);
+ }
+
+ @Test
+ public void testAutoDetectMalformedThreeLevelListStructuresUsesOldBehavior()
{
+ for (String schema : Arrays.asList(
+ "message RepeatedOuterList {"
+ + " repeated group items (LIST) {"
+ + " repeated group list {"
+ + " required int32 element;"
+ + " }"
+ + " }"
+ + "}",
+ "message NonRepeatedMiddleList {"
+ + " optional group items (LIST) {"
+ + " optional group list {"
+ + " required int32 element;"
+ + " }"
+ + " }"
+ + "}",
+ "message RepeatedListElement {"
+ + " optional group items (LIST) {"
+ + " repeated group list {"
+ + " repeated int32 element;"
+ + " }"
+ + " }"
+ + "}")) {
+
assertListStructureIsNotAutoDetected(MessageTypeParser.parseMessageType(schema));
+ }
+ }
+
+ @Test
+ public void testAutoDetectTwoLevelListWithArrayGroupName() throws Exception {
+ // A 2-level list where the repeated group is named "array" (a standard
+ // backward-compat name). The "array" name causes isElementType to always
+ // treat the repeated group as the element, regardless of the list
structure flag.
+ Path test = writeDirect(
+ "message TwoLevelListWithArrayGroup {"
+ + " optional group list (LIST) {"
+ + " repeated group array {"
+ + " required int32 str;"
+ + " }"
+ + " }"
+ + "}",
+ rc -> {
+ rc.startMessage();
+ rc.startField("list", 0);
+
+ rc.startGroup();
+ rc.startField("array", 0);
+
+ rc.startGroup();
+ rc.startField("str", 0);
+ rc.addInteger(34);
+ rc.endField("str", 0);
+ rc.endGroup();
+
+ rc.startGroup();
+ rc.startField("str", 0);
+ rc.addInteger(35);
+ rc.endField("str", 0);
+ rc.endGroup();
+
+ rc.endField("array", 0);
+ rc.endGroup();
+
+ rc.endField("list", 0);
+ rc.endMessage();
+ });
+
+ // "array"-named group is always treated as the element type
+ Schema elementRecord = record("array", field("str",
primitive(Schema.Type.INT)));
+ Schema expectedSchema = record("TwoLevelListWithArrayGroup",
optionalField("list", array(elementRecord)));
+ GenericRecord expectedRecord = instance(
+ expectedSchema,
+ "list",
+ Arrays.asList(instance(elementRecord, "str", 34),
instance(elementRecord, "str", 35)));
+
+ // all three modes produce the same result
+ assertReaderContains(oldBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(newBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(new AvroParquetReader<>(newAutoDetectConf(), test),
expectedSchema, expectedRecord);
+ }
+
+ @Test
+ public void testAutoDetectTwoLevelListWithArrayGroupAndElementChild() throws
Exception {
+ // A 2-level list where the repeated group is named "array" and its single
+ // child is named "element". The "array" group name dominates:
isElementType
+ // treats the group as the element regardless of the child's name.
+ Path test = writeDirect(
+ "message TwoLevelListWithArrayGroupAndElementChild {"
+ + " optional group my_list (LIST) {"
+ + " repeated group array {"
+ + " required int32 element;"
+ + " }"
+ + " }"
+ + "}",
+ rc -> {
+ rc.startMessage();
+ rc.startField("my_list", 0);
+
+ rc.startGroup();
+ rc.startField("array", 0);
+
+ rc.startGroup();
+ rc.startField("element", 0);
+ rc.addInteger(34);
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.startGroup();
+ rc.startField("element", 0);
+ rc.addInteger(35);
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.endField("array", 0);
+ rc.endGroup();
+
+ rc.endField("my_list", 0);
+ rc.endMessage();
+ });
+
+ // "array"-named group is always the element, even with child named
"element"
+ Schema elementRecord = record("array", field("element",
primitive(Schema.Type.INT)));
+ Schema expectedSchema =
+ record("TwoLevelListWithArrayGroupAndElementChild",
optionalField("my_list", array(elementRecord)));
+ GenericRecord expectedRecord = instance(
+ expectedSchema,
+ "my_list",
+ Arrays.asList(instance(elementRecord, "element", 34),
instance(elementRecord, "element", 35)));
+
+ // all three modes produce the same result
+ assertReaderContains(oldBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(newBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(new AvroParquetReader<>(newAutoDetectConf(), test),
expectedSchema, expectedRecord);
+ }
+
+ @Test
+ public void testAutoDetectTwoLevelRepeatedPrimitive() throws Exception {
+ // The most basic backward-compat rule: a repeated primitive inside a LIST
+ // group is the element type directly, producing a required list of
+ // non-nullable elements.
+ Path test = writeDirect(
+ "message TwoLevelRepeatedPrimitive {"
+ + " required group my_list (LIST) {"
+ + " repeated int32 element;"
+ + " }"
+ + "}",
+ rc -> {
+ rc.startMessage();
+ rc.startField("my_list", 0);
+
+ rc.startGroup();
+ rc.startField("element", 0);
+
+ rc.addInteger(34);
+ rc.addInteger(35);
+ rc.addInteger(36);
+
+ rc.endField("element", 0);
+ rc.endGroup();
+
+ rc.endField("my_list", 0);
+ rc.endMessage();
+ });
+
+ Schema expectedSchema =
+ record("TwoLevelRepeatedPrimitive", field("my_list",
array(Schema.create(Schema.Type.INT))));
+ GenericRecord expectedRecord = instance(expectedSchema, "my_list",
Arrays.asList(34, 35, 36));
+
+ // all three modes produce the same result for repeated primitives
+ assertReaderContains(oldBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(newBehaviorReader(test), expectedSchema,
expectedRecord);
+ assertReaderContains(new AvroParquetReader<>(newAutoDetectConf(), test),
expectedSchema, expectedRecord);
+ }
+
+ private static Configuration newAutoDetectConf() {
+ Configuration conf = new Configuration();
+ conf.setBoolean(AvroReadSupport.AUTO_DETECT_LIST_STRUCTURE, true);
+ return conf;
+ }
+
+ private static void assertListStructureIsNotAutoDetected(MessageType
fileSchema) {
+ Schema projection = record("Projection", field("items",
array(primitive(Schema.Type.INT))));
+ MessageType expectedProjection = new
AvroSchemaConverter().convert(projection);
+ Configuration conf = newAutoDetectConf();
+ AvroReadSupport.setRequestedProjection(conf, projection);
+
+ assertThat(new AvroReadSupport<>()
+ .init(conf, new HashMap<>(), fileSchema)
+ .getRequestedSchema())
+ .as("Should not infer the new list structure for %s",
fileSchema.getName())
+ .isEqualTo(expectedProjection);
+ assertThat(conf.get(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE)).isNull();
+
assertThat(conf.get(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS)).isNull();
+ }
+
+ public <T extends IndexedRecord> AvroParquetReader<T> autoDetectReader(Path
path) throws IOException {
+ return new AvroParquetReader<T>(AUTO_DETECT_CONF, path);
+ }
+
public <T extends IndexedRecord> AvroParquetReader<T> oldBehaviorReader(Path
path) throws IOException {
return new AvroParquetReader<T>(OLD_BEHAVIOR_CONF, path);
}