Skip to content

Conversation

@martin-traverse
Copy link
Contributor

@martin-traverse martin-traverse commented Apr 14, 2025

Hi @lidavidm - here is part 2 in my Avro series, apologies for the delay, it's the usual work / contention story!

What's Changed

This PR relates to #698 and is the second in a series intended to provide full Avro read / write support in native Java. It adds round-trip tests for both schemas (Arrow schema -> Avro -> Arrow) and data (Arrow VSR -> Avro block -> Arrow VSR). It also adds a number of fixes and improvements to the Avro Consumers so that data arrives back in its original form after a round trip. The main changes are:

  • Added a top level method in AvroToArrow to convert Avro schema directly to Arrow schema (this may exist elsewhere, but is needed to provide an API that matches the logic of this implementation)
  • Avro unions of [ type, null ] or [ null, type ] now have special handling, these are interpreted as a single nullable type rather than a union. Setting legacyMode = false in the AvroToArrowConfig object is required to enable this behaviour, otherwise unions are interpreted literally. Unions with more than 2 elements are always interpreted literally (but, per [Java] Type-ids in UnionVector are erroneously coupled to the Arrow types of the underlying vectors #108, in practice Java's current Union implementation is probably not usable with Avro atm).
  • Added support for new logical types (decimal 256, timestamp nano and 3 local timestamp types)
  • Existing timestamp-mills and timestamp-micros times now interpreted as zone-aware (previously they were interpreted as local, but now the local timestamp types are interpreted as local - I think this is correct per the Avro spec). Requires setting legacyMode = false.
  • Removed namespaces from generated Arrow field names in complex types. E.g. the Avro field myNamepsace.outerRecord.structField.intField should be called just "intField" inside the Arrow struct. This doesn't affect the skip field logic, which still works using the qualified names. This requires setting legacyMode = false.
  • Remove unexpected metadata in generated Arrow fields (empty alias lists and attributes interpreted as part of the field schema). This requires setting legacyMode = false.
  • Use the expected child vector names for Arrow LIST and MAP types when reading. For LIST, the default child vector is called "$data$" which is illegal in Avro, so the child field name is also changed to "item" in the producers. This requires setting legacyMode = false.

Breaking changes have been removed from this PR.

Per discussion below, all breaking changes are now behind a "legacyMode" flag in the AvroToArrowConfig object, which is enabled by default in all the original code paths.

Closes #698 .

This change is meant to allow for round trip of schemas and individual Avro data blocks (one Avro data block -> one VSR). File-level capabilities are not included. I have not included anything to recycle the VSR as part of the read API, this feels like it belongs with the file-level piece. Also I have not done anything specific for enums / dict encoding as of yet.

@github-actions

This comment has been minimized.

@lidavidm lidavidm added the enhancement PRs that add or improve features. label Apr 15, 2025
@github-actions github-actions bot added this to the 18.3.0 milestone Apr 15, 2025
Copy link
Member

@lidavidm lidavidm left a comment

Choose a reason for hiding this comment

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

I think in the interest of trying to keep semver, we should avoid breaking changes if possible. Any thoughts @jbonofre @laurentgo? Or we could just call the next release 19.0.0...

If it helps we could just have a single flag for "old" behavior?

case List:
case FixedSizeList:
return buildArraySchema(builder.array(), field, namespace);
// Arrow uses "$data$" as the field name for list items, that is not a valid Avro name
Copy link
Member

Choose a reason for hiding this comment

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

The funny thing is, arrow-java shouldn't be doing that, it was just never corrected...

return buildArraySchema(builder.array(), field, namespace);
// Arrow uses "$data$" as the field name for list items, that is not a valid Avro name
Field itemField = field.getChildren().get(0);
if (ListVector.DATA_VECTOR_NAME.equals(itemField.getName())) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we perhaps want to check for invalid names more generally and mangle/normalize them?

Copy link
Member

Choose a reason for hiding this comment

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

Or just normalize all field names to something consistent in Avro?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, I think for list / map types using the constant defined names for children makes sense, with "item" instead of "$data$" for list items. More generally, we could normalise illegal chars to "_" to match the Avro name rules. Per my understanding similar rules are already enforced in C++, but are not part of the Arrow spec or Java implementation.

Very happy to put the normalisation in, it's probably a more useful behaviour than throwing an error in the adapter. Would you like me to do it?

/**
* Producer wrapper which producers nullable types to an avro encoder. Write the data to the
* underlying {@link FieldVector}.
* Producer wrapper which producers nullable types to an avro encoder. Reed data from the underlying
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Producer wrapper which producers nullable types to an avro encoder. Reed data from the underlying
* Producer wrapper which produces nullable types to an avro encoder. Read data from the underlying

Copy link
Contributor Author

Choose a reason for hiding this comment

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

;-)

return skipFieldNames;
}

public boolean isHandleNullable() {
Copy link
Member

Choose a reason for hiding this comment

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

nit: could we perhaps get a more descriptive name for this parameter overall? "handleUnionOfNullAsNullable"? (As enterprise-java-y as that is...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is now part of the legacyMode parameter

Copy link
Member

Choose a reason for hiding this comment

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

Is there an opportunity to structure this as a parameterized test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have factored out the common code as a helper. Don't think it can go all the way to being parameterised because the types need to be set up differently.

new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroDecimalConsumer.BytesDecimalConsumer((DecimalVector) vector);
if (decimalType.getPrecision() <= 38) {
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, it's technically possible to have a decimal256 with a smaller precision though

Copy link
Member

Choose a reason for hiding this comment

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

I guess in that case it would round-trip to the smaller type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For FIXED decimals I am using the fixedSize to choose the decimal type. For BYTES decimals yes they would just come back as the smaller type if the precision fits.

I used FIXED as the default output for decimals in the producers, because it is closer to the Arrow representation, but on reflection Avro as a format is very focused on keeping data compact, maybe BYTES makes more sense. It seems to be the default choice in Avro. Do you think we should go with that?

Copy link
Member

Choose a reason for hiding this comment

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

I think FIXED is okay to allow round-trip by default even if it's not technically as compact.

@martin-traverse
Copy link
Contributor Author

I think in the interest of trying to keep semver, we should avoid breaking changes if possible. Any thoughts @jbonofre @laurentgo? Or we could just call the next release 19.0.0...

If it helps we could just have a single flag for "old" behavior?

A major version bump feels a bit extreme for something as small as this! Let me try the single flag approach. There might be complications with the change for zone-aware vs local timestamps, because the types are different, but I think so long as the check happens before the types are decided it should be ok.

@martin-traverse
Copy link
Contributor Author

Ok, here is an update. I have put a flag "legacyMode" in the config object and used that to control all the places where the old logic is impacted. I have allowed decimal 256 to come through in legacy mode and also timestamp-nanos with the old semantics. The local-timestamp-xxx types do not come through in legacy mode, because timestamp-xxx is already treated as local. I have replaced the original code for the logical types test and those are all passing.

@martin-traverse
Copy link
Contributor Author

I have removed the breaking changes text from the headline text but am not able to remove the label.

new FieldType(nullable, arrowType, /* dictionary= */ null, getMetaData(schema));
vector = createVector(consumerVector, fieldType, name, allocator);
consumer = new AvroDecimalConsumer.BytesDecimalConsumer((DecimalVector) vector);
if (decimalType.getPrecision() <= 38) {
Copy link
Member

Choose a reason for hiding this comment

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

I think FIXED is okay to allow round-trip by default even if it's not technically as compact.

@lidavidm
Copy link
Member

Looks like there are some lint errors to be fixed

@martin-traverse
Copy link
Contributor Author

Looks like there are some lint errors to be fixed

Apologies - I have reapplied spotless, should be ok now!

@lidavidm lidavidm mentioned this pull request Apr 22, 2025
5 tasks
@lidavidm lidavidm merged commit d2465c3 into apache:main Apr 23, 2025
25 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement PRs that add or improve features.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Avro support - Improve existing read capabilities

2 participants