[
https://issues.apache.org/jira/browse/FLINK-40265?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Shuo Cheng updated FLINK-40265:
-------------------------------
Description:
h3. Description
{{BinaryStringData.compareTo}} uses two different comparison paths:
* If both values have a non-null {{javaObject}}, it delegates to
{{String.compareTo}}, which compares UTF-16 code units.
* Otherwise, it materializes both values and compares their UTF-8 bytes as
unsigned bytes.
These orders are not equivalent for comparisons between an SMP character and
BMP characters in the range U+E000-U+FFFF. As a result, the comparison result
depends on whether a {{BinaryStringData}} is Java-backed or binary-backed and
can change after a serializer round trip.
For example, compare GRINNING FACE (😀, U+1F600) with FULLWIDTH EXCLAMATION MARK
(!, U+FF01):
* UTF-16: D83D DE00 < FF01, so 😀 < !
* UTF-8: F0 9F 98 80 > EF BC 81, so 😀 > !
h3. Reproduce
{code:java}
@Test
void compareToIsConsistentAfterSerialization() throws IOException {
BinaryStringData grinningFace = BinaryStringData.fromString("😀");
BinaryStringData fullWidthExclamationMark =
BinaryStringData.fromString("!");
DataOutputSerializer output = new DataOutputSerializer(16);
StringDataSerializer.INSTANCE.serialize(grinningFace, output);
BinaryStringData deserializedGrinningFace =
(BinaryStringData)
StringDataSerializer.INSTANCE.deserialize(
new
DataInputDeserializer(output.getCopyOfBuffer()));
assertThat(deserializedGrinningFace).isEqualTo(grinningFace);
assertThat(deserializedGrinningFace.compareTo(fullWidthExclamationMark)).isPositive();
assertThat(grinningFace.compareTo(fullWidthExclamationMark))
.isEqualTo(deserializedGrinningFace.compareTo(fullWidthExclamationMark));
}
{code}
h3. Actual behavior
After serialization:
* {{grinningFace.compareTo(fullWidthExclamationMark)}} is negative because both
values retain Java objects and use {{String.compareTo}}.
* {{deserializedGrinningFace.compareTo(fullWidthExclamationMark)}} is positive
because the deserialized value is binary-backed and comparison uses UTF-8 bytes.
The final assertion fails.
h3. Expected behavior
{{BinaryStringData.compareTo}} should produce the same ordering regardless of
the backing representation and before/after serialization. The class Javadoc
and {{SortUtil.putStringNormalizedKey}} already describe/use UTF-8 byte
ordering, so comparison should follow one canonical ordering.
h3. Impact
This violates the {{Comparable}} contract. The original and deserialized values
compare equal to each other, but have opposite ordering relative to the same
third value. It can cause representation-dependent results in generated string
comparisons, sorting, MIN/MAX, or other operations using {{compareTo}}.
was:
h3. Description
{{BinaryStringData.compareTo}} uses two different comparison paths:
* If both values have a non-null {{javaObject}}, it delegates to
{{String.compareTo}}, which compares UTF-16 code units.
* Otherwise, it materializes both values and compares their UTF-8 bytes as
unsigned bytes.
These orders are not equivalent for comparisons between an SMP character and
BMP characters in the range U+E000-U+FFFF. As a result, the comparison result
depends on whether a {{BinaryStringData}} is Java-backed or binary-backed and
can change after a serializer round trip.
For example, compare GRINNING FACE (😀, U+1F600) with FULLWIDTH EXCLAMATION MARK
(!, U+FF01):
* UTF-16: D83D DE00 < FF01, so 😀 < !
* UTF-8: F0 9F 98 80 > EF BC 81, so 😀 > !
h3. Reproduce
{code:java}
@Test
void compareToIsConsistentAfterSerialization() throws IOException {
BinaryStringData grinningFace = BinaryStringData.fromString("😀");
BinaryStringData fullWidthExclamationMark =
BinaryStringData.fromString("!");
DataOutputSerializer output = new DataOutputSerializer(16);
StringDataSerializer.INSTANCE.serialize(grinningFace, output);
BinaryStringData deserializedGrinningFace =
(BinaryStringData)
StringDataSerializer.INSTANCE.deserialize(
new
DataInputDeserializer(output.getCopyOfBuffer()));
assertThat(deserializedGrinningFace).isEqualTo(grinningFace);
assertThat(deserializedGrinningFace.compareTo(fullWidthExclamationMark)).isPositive();
assertThat(grinningFace.compareTo(fullWidthExclamationMark))
.isEqualTo(deserializedGrinningFace.compareTo(fullWidthExclamationMark));
}
{code}
h3. Actual behavior
After serialization:
* {{grinningFace.compareTo(fullWidthExclamationMark)}} is negative because both
values retain Java objects and use {{String.compareTo}}.
* {{deserializedGrinningFace.compareTo(fullWidthExclamationMark)}} is positive
because the deserialized value is binary-backed and comparison uses UTF-8 bytes.
The final assertion fails.
h3. Expected behavior
{{BinaryStringData.compareTo}} should produce the same ordering regardless of
the backing representation and before/after serialization. The class Javadoc
and {{SortUtil.putStringNormalizedKey}} already describe/use UTF-8 byte
ordering, so comparison should follow one canonical ordering.
h3. Impact
This violates the {{Comparable}} contract. The original and deserialized values
compare equal to each other, but have opposite ordering relative to the same
third value. It can cause representation-dependent results in generated string
comparisons, sorting, MIN/MAX, or other operations using {{compareTo}}.
> BinaryStringData.compareTo uses inconsistent UTF-16 and UTF-8 ordering for
> SMP characters
> -----------------------------------------------------------------------------------------
>
> Key: FLINK-40265
> URL: https://issues.apache.org/jira/browse/FLINK-40265
> Project: Flink
> Issue Type: Bug
> Components: Table SQL / Runtime
> Reporter: Shuo Cheng
> Priority: Major
>
> h3. Description
> {{BinaryStringData.compareTo}} uses two different comparison paths:
> * If both values have a non-null {{javaObject}}, it delegates to
> {{String.compareTo}}, which compares UTF-16 code units.
> * Otherwise, it materializes both values and compares their UTF-8 bytes as
> unsigned bytes.
> These orders are not equivalent for comparisons between an SMP character and
> BMP characters in the range U+E000-U+FFFF. As a result, the comparison result
> depends on whether a {{BinaryStringData}} is Java-backed or binary-backed and
> can change after a serializer round trip.
> For example, compare GRINNING FACE (😀, U+1F600) with FULLWIDTH EXCLAMATION
> MARK (!, U+FF01):
> * UTF-16: D83D DE00 < FF01, so 😀 < !
> * UTF-8: F0 9F 98 80 > EF BC 81, so 😀 > !
> h3. Reproduce
> {code:java}
> @Test
> void compareToIsConsistentAfterSerialization() throws IOException {
> BinaryStringData grinningFace = BinaryStringData.fromString("😀");
> BinaryStringData fullWidthExclamationMark =
> BinaryStringData.fromString("!");
> DataOutputSerializer output = new DataOutputSerializer(16);
> StringDataSerializer.INSTANCE.serialize(grinningFace, output);
> BinaryStringData deserializedGrinningFace =
> (BinaryStringData)
> StringDataSerializer.INSTANCE.deserialize(
> new
> DataInputDeserializer(output.getCopyOfBuffer()));
> assertThat(deserializedGrinningFace).isEqualTo(grinningFace);
>
> assertThat(deserializedGrinningFace.compareTo(fullWidthExclamationMark)).isPositive();
> assertThat(grinningFace.compareTo(fullWidthExclamationMark))
>
> .isEqualTo(deserializedGrinningFace.compareTo(fullWidthExclamationMark));
> }
> {code}
> h3. Actual behavior
> After serialization:
> * {{grinningFace.compareTo(fullWidthExclamationMark)}} is negative because
> both values retain Java objects and use {{String.compareTo}}.
> * {{deserializedGrinningFace.compareTo(fullWidthExclamationMark)}} is
> positive because the deserialized value is binary-backed and comparison uses
> UTF-8 bytes.
> The final assertion fails.
> h3. Expected behavior
> {{BinaryStringData.compareTo}} should produce the same ordering regardless of
> the backing representation and before/after serialization. The class Javadoc
> and {{SortUtil.putStringNormalizedKey}} already describe/use UTF-8 byte
> ordering, so comparison should follow one canonical ordering.
> h3. Impact
> This violates the {{Comparable}} contract. The original and deserialized
> values compare equal to each other, but have opposite ordering relative to
> the same third value. It can cause representation-dependent results in
> generated string comparisons, sorting, MIN/MAX, or other operations using
> {{compareTo}}.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)