Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions parquet-avro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
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;

Expand Down Expand Up @@ -63,6 +66,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.
Expand Down Expand Up @@ -128,6 +136,18 @@ public ReadContext init(
MessageType projection = fileSchema;
Map<String, String> metadata = new LinkedHashMap<String, String>();

boolean autoDetectListStructure =
configuration.getBoolean(AUTO_DETECT_LIST_STRUCTURE, AUTO_DETECT_LIST_STRUCTURE_DEFAULT);

if (autoDetectListStructure
&& configuration.get(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE) == null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good time to make a shift on the default value of AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE? It has caused a lot of troubles.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to have the more modern encoding as default! though I'm a little nervous about the implications of changing the schema for existing datasets - I know we have some downstream use cases where a single Parquet reader is reading a globbed filepattern matching multiple partitions of the same dataset - not sure what would happen if some partitions used 2-level encoding and some used 3-level encoding. Same concern about Parquet datasets that are the source of truth for external tables in Snowflake/BigQuery/etc.

maybe I can create an issue for this and request a bit more investigation on possible implications of this change?

&& configuration.get(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS) == null) {
if (writesNewListStructure(fileSchema)) {
configuration.setBoolean(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE, false);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This writes the inferred mode back into the shared Configuration. ParquetReader reuses that configuration across files, so after a 3-level file, a later 2-level file (especially with a projection) is still converted as 3-level. Could this stay per-file/read-context instead of mutating the caller's configuration?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I was a bit worried about that. Mutating the caller conf is simplest because we want to have these properties set in both init() (when computing the projection) and prepareForRead() (when computing avroSchema if it's not already set in file footer/read conf). What do you think about modifying metadata instead of conf? like:

public ReadContext init(
      ParquetConfiguration configuration, Map<String, String> keyValueMetaData, MessageType fileSchema) {
    MessageType projection = fileSchema;
    Map<String, String> metadata = new LinkedHashMap<String, String>();

    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) {
      if (writesNewListStructure(fileSchema)) {
-        configuration.setBoolean(AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE, false);
-        configuration.setBoolean(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS, false);
+        metadata.put("inferred." + AvroWriteSupport.WRITE_OLD_LIST_STRUCTURE, "false");
+        metadata.put("inferred." + AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS, "false");
      }
    }

+ // ...read those properties in prepareForRead() and apply to copied Configuration

Or - we could just re-compute writesNewListStructure(fileSchema) in both init() and prepareForRead(). that might be more straightforward overall 🤷‍♀️

configuration.setBoolean(AvroSchemaConverter.ADD_LIST_ELEMENT_RECORDS, false);
}
}

String requestedProjectionString = configuration.get(AVRO_REQUESTED_PROJECTION);
if (requestedProjectionString != null) {
Schema avroRequestedProjection = new Schema.Parser().parse(requestedProjectionString);
Expand Down Expand Up @@ -230,4 +250,39 @@ private GenericData getDataModel(ParquetConfiguration conf, Schema schema) {
return ReflectionUtils.newInstance(suppClass, ConfigurationUtil.createHadoopConfiguration(conf))
.get();
}

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.getFieldCount() != 1) {
return false;
}
Type repeated = group.getType(0);
return !repeated.isPrimitive()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be misremembering the LIST compatibility rules, so I wanted to check this edge case: could a legacy 2-level list also use the list/element names and therefore look like this to allListStructuresAreThreeLevel? If so, would auto-detect change its Avro shape unexpectedly, or is this case ruled out by the spec or writer assumptions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a few more test cases here and the auto-detect correctly returns false for those cases! lmk if you had any other cases in mind...

&& repeated.getName().equals("list")
&& repeated.asGroupType().getFieldCount() == 1
&& repeated.asGroupType().getType(0).getName().equals("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;
}
}
Loading