Chu Cheng Li created SPARK-58949:
------------------------------------
Summary: [CORE] Variant object field ordering uses UTF-16 instead
of unsigned UTF-8
Key: SPARK-58949
URL: https://issues.apache.org/jira/browse/SPARK-58949
Project: Spark
Issue Type: Bug
Components: Spark Core
Affects Versions: 4.2.0, 4.0.0, 4.1.0
Reporter: Chu Cheng Li
The Parquet Variant specification requires object field IDs and offsets to be
ordered by their corresponding field names using unsigned lexicographic UTF-8
byte ordering. Readers may rely on this invariant for binary search:
https://github.com/apache/parquet-format/blob/24102ed5c56e51b610a4897e5f79e76e43732d1d/VariantEncoding.md#L449-L463
Spark currently uses Java {{String.compareTo}}, which compares UTF-16 code
units, in both places that define this contract:
* {{VariantBuilder.FieldEntry.compareTo}} determines the encoded object-field
order:
https://github.com/apache/spark/blob/9da9f8d673914d1648514f59d85e3adafb300d1a/common/variant/src/main/java/org/apache/spark/types/variant/VariantBuilder.java#L1009-L1010
* {{Variant.getFieldByKey}} uses {{String.compareTo}} while binary-searching
objects containing at least 32 fields:
https://github.com/apache/spark/blob/9da9f8d673914d1648514f59d85e3adafb300d1a/common/variant/src/main/java/org/apache/spark/types/variant/Variant.java#L145-L167
UTF-16 and unsigned UTF-8 order differ for some valid Unicode field names:
{code}
Field name UTF-16 code units UTF-8 bytes
U+FFFF FFFF EF BF BF
U+10000 D800 DC00 F0 90 80 80
{code}
The Variant specification orders U+FFFF before U+10000. Java
{{String.compareTo}} orders U+10000 first because {{D800 < FFFF}}.
Consequences:
* Spark can write Variant objects whose field-ID order violates the Parquet
Variant specification.
* Spec-compliant Variant readers can fail to find fields in Spark-written
objects.
* Spark can fail to find fields in canonical objects written by spec-compliant
implementations.
* The reader problem is hidden for objects with fewer than 32 fields because
Spark uses a linear scan. For objects with at least 32 fields,
{{getFieldByKey}} can silently return null for an existing field.
* Nested objects are affected as well.
This is distinct from SPARK-56637. That issue associated object lookup with the
metadata dictionary's {{sorted_strings}} flag. The investigation in PR #55928
established that object entries must always be name-sorted independently of
metadata dictionary order:
https://github.com/apache/spark/pull/55928#issuecomment-4568094207
The problem here is that Spark uses the wrong comparator when sorting and
searching those mandatory ordered entries.
parquet-java tracks the same issue separately:
* https://github.com/apache/parquet-java/issues/3735
* https://github.com/apache/parquet-java/pull/3736
h3. Suggested fix
# Introduce one shared unsigned UTF-8 field-name comparator.
# Use it when {{VariantBuilder}} sorts object fields.
# Use it when {{Variant.getFieldByKey}} binary-searches object fields.
# Preserve compatibility with existing Spark/parquet-java values that may
already be UTF-16-ordered. Because the object encoding has no comparator
marker, retry the legacy UTF-16 search or use a linear fallback when the
canonical UTF-8 search misses.
# Avoid repeatedly allocating UTF-8 byte arrays during sort comparisons; encode
or cache each key once where practical.
h3. Tests
* Verify an object containing U+FFFF and U+10000 is encoded in unsigned UTF-8
order.
* Build a canonical object containing at least 32 fields and verify
{{getFieldByKey}} finds both affected names.
* Verify lookup of an absent field.
* Verify a legacy UTF-16-ordered object containing at least 32 fields remains
readable.
* Cover an affected nested object.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]