From 392ce836cd29ac2c844023937b9bf9c54cf0c442 Mon Sep 17 00:00:00 2001 From: peterxcli Date: Tue, 25 Aug 2026 08:22:00 +0800 Subject: [PATCH] Add Variant objects with supplementary-character field names The Variant spec orders object fields by unsigned lexicographic UTF-8 bytes, which differs from UTF-16 code-unit order for supplementary characters: UTF-16 sorts U+10000 (surrogate pair D800 DC00) before U+E000 and U+FFFF, while UTF-8 byte order sorts it after both. This is an easy mistake in languages whose native string comparison operates on UTF-16 code units (e.g. Java, JavaScript, C#), and an implementation that sorts or binary-searches object fields that way will silently mishandle such objects. Add two examples encoding the same object, whose field names straddle the surrogate range ($ 0 A a ~ U+00A2 U+20AC U+E000 U+FFFF U+10000 U+1F600 U+10FFFF), so implementations can verify they order and look up object fields consistently: * object_unicode_keys_sorted: sorted metadata dictionary (sorted_strings = 1), field ids in dictionary order * object_unicode_keys_unsorted: deliberately scrambled dictionary, so field ids are non-monotonic and readers must compare key bytes Each field value is a short string spelling the field name's code point, making a lookup that lands on the wrong field self-evident. The files are generated directly from the spec by the new standalone regen_unicode_keys.py script. Also fix a pre-existing trailing comma that made data_dictionary.json invalid JSON. --- variant/README.md | 21 +++- variant/data_dictionary.json | 30 ++++- variant/object_unicode_keys_sorted.metadata | Bin 0 -> 43 bytes variant/object_unicode_keys_sorted.value | Bin 0 -> 115 bytes variant/object_unicode_keys_unsorted.metadata | Bin 0 -> 43 bytes variant/object_unicode_keys_unsorted.value | Bin 0 -> 115 bytes variant/regen_unicode_keys.py | 119 ++++++++++++++++++ 7 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 variant/object_unicode_keys_sorted.metadata create mode 100644 variant/object_unicode_keys_sorted.value create mode 100644 variant/object_unicode_keys_unsorted.metadata create mode 100644 variant/object_unicode_keys_unsorted.value create mode 100644 variant/regen_unicode_keys.py diff --git a/variant/README.md b/variant/README.md index c09c49a..6988cb4 100644 --- a/variant/README.md +++ b/variant/README.md @@ -39,7 +39,17 @@ Each example consists of 2 files: 3. `object_empty` -- Example of object (`basic_type` = 3) with no fields 3. `object_primitive` -- Example of object with only primitive fields 4. `object_nested` -- Example of object with other objects in fields -5. `array_empty` -- Example of array (`basic_type` = 4) with no elements +5. `object_unicode_keys_sorted` / `object_unicode_keys_unsorted` -- Examples of an object + whose field names include code points on both sides of the UTF-16 surrogate range + (e.g. `U+E000`, `U+FFFF`, `U+10000`, `U+1F600`, `U+10FFFF`), where the unsigned + lexicographic UTF-8 byte order required by the spec differs from UTF-16 code-unit + order (e.g. Java's `String.compareTo`). Both encode the same object; the `_sorted` + file uses a sorted metadata dictionary (`sorted_strings = 1`) while the `_unsorted` + file uses a deliberately scrambled dictionary, so its object field ids are + non-monotonic and readers must compare the referenced field name bytes. Each field + value is a short string spelling the field name's code point (e.g. `"U+FFFF"`), so + a lookup that lands on the wrong field is self-evident. +6. `array_empty` -- Example of array (`basic_type` = 4) with no elements 5. `array_primitive` -- Example of array with only primitive elements 6. `array_nested` -- Example of an with objects and other arrays in the elements @@ -73,5 +83,14 @@ echo -n 'a' | tr a '\0' > primitive_null.value Currently, Spark [does not support](https://github.com/apache/spark/blob/master/common/variant/README.md) Variant values containing UUID, Time, or nanosecond-precision Timestamp. the `primitive_time.[metadata/value]`, `primitive_timestamp_nanos.[metadata/value]`, `primitive_timestampntz_nanos.[metadata/value]` and `primitive_uuid.[metadata/data]` was generated by [Iceberg test code](https://github.com/apache/iceberg/blob/3a4215dbb714477c89681ab94f1197b6ebcbdfff/parquet/src/test/java/org/apache/iceberg/parquet/TestVariantReaders.java#L355) +### Modification 3: Created `object_unicode_keys_sorted` and `object_unicode_keys_unsorted` with [`regen_unicode_keys.py`](regen_unicode_keys.py) + +These examples exercise the unsigned lexicographic UTF-8 byte ordering of object +field names required by the spec, which differs from UTF-16 code-unit order for +supplementary characters — an easy mistake in languages whose native string +comparison operates on UTF-16 code units (e.g. Java, JavaScript, C#). They are +generated directly from the spec by the standalone +[`regen_unicode_keys.py`](regen_unicode_keys.py) script rather than `regen.py`. + [Variant]: https://github.com/apache/parquet-format/blob/master/VariantEncoding.md [primitive types listed in the spec]: https://github.com/apache/parquet-format/blob/master/VariantEncoding.md#value-data-for-primitive-type-basic_type0 diff --git a/variant/data_dictionary.json b/variant/data_dictionary.json index 8a1faae..c548142 100644 --- a/variant/data_dictionary.json +++ b/variant/data_dictionary.json @@ -52,6 +52,34 @@ "string_field": "Apache Parquet", "timestamp_field": "2025-04-16T12:34:56.78" }, + "object_unicode_keys_sorted": { + "$": "U+0024", + "0": "U+0030", + "A": "U+0041", + "a": "U+0061", + "~": "U+007E", + "\u00a2": "U+00A2", + "\u20ac": "U+20AC", + "\ue000": "U+E000", + "\uffff": "U+FFFF", + "\ud800\udc00": "U+10000", + "\ud83d\ude00": "U+1F600", + "\udbff\udfff": "U+10FFFF" + }, + "object_unicode_keys_unsorted": { + "$": "U+0024", + "0": "U+0030", + "A": "U+0041", + "a": "U+0061", + "~": "U+007E", + "\u00a2": "U+00A2", + "\u20ac": "U+20AC", + "\ue000": "U+E000", + "\uffff": "U+FFFF", + "\ud800\udc00": "U+10000", + "\ud83d\ude00": "U+1F600", + "\udbff\udfff": "U+10FFFF" + }, "primitive_binary": "AxM33q2+78r+", "primitive_boolean_false": false, "primitive_boolean_true": true, @@ -73,5 +101,5 @@ "primitive_timestamp_nanos": "2024-11-07T12:33:54.123456789+00:00", "primitive_timestampntz_nanos": "2024-11-07T12:33:54.123456789", "primitive_uuid": "f24f9b64-81fa-49d1-b74e-8c09a6e31c56", - "short_string": "Less than 64 bytes (\u2764\ufe0f with utf8)", + "short_string": "Less than 64 bytes (\u2764\ufe0f with utf8)" } diff --git a/variant/object_unicode_keys_sorted.metadata b/variant/object_unicode_keys_sorted.metadata new file mode 100644 index 0000000000000000000000000000000000000000..337d4b26ded6ee43ae5073c980089da94588536a GIT binary patch literal 43 zcmV+`0M!2x3;+QF0|W&J3Jnkx7#t)pL1BKvqT+(A?tp;rzrXO1fPnCyn1J+;zrXu5 B5)J?W literal 0 HcmV?d00001 diff --git a/variant/object_unicode_keys_sorted.value b/variant/object_unicode_keys_sorted.value new file mode 100644 index 0000000000000000000000000000000000000000..8cc0ce566e321d5f5d5ef3e2b05a6a81356de991 GIT binary patch literal 115 zcmZSLVPIrpW?^Mx=iubxW?<(Nl~L9*w6J&gkB|)2HZU+U0aL~XV9LZ0OqoF_b5}6W m(FjNx88|uvDOUpnkV-cokPX!~1o8|(w40fMfg*?xk^le!5){M$ literal 0 HcmV?d00001 diff --git a/variant/object_unicode_keys_unsorted.metadata b/variant/object_unicode_keys_unsorted.metadata new file mode 100644 index 0000000000000000000000000000000000000000..445fc7682cc27978d3a2096f1dc62ef1ac62018d GIT binary patch literal 43 zcmV+`0M!2h3;+ZL2?`Al5EK;{865DCfPi82kH5bp@4vsoqA>8En1FunfPg{bf~*`U B5;p(< literal 0 HcmV?d00001 diff --git a/variant/object_unicode_keys_unsorted.value b/variant/object_unicode_keys_unsorted.value new file mode 100644 index 0000000000000000000000000000000000000000..607908b96db838a4005a4673a0be85f75daa84dc GIT binary patch literal 115 zcmZSLVP@lE