[ 
https://issues.apache.org/jira/browse/FLINK-40265?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18100920#comment-18100920
 ] 

Qiu Yanjun commented on FLINK-40265:
------------------------------------

Hi [~icshuo], I investigated this issue on the latest master (6ce85191d154) and 
reproduced the representation-dependent ordering.

*Root cause*

{{BinaryStringData.compareTo}} has two comparison paths:

* When both instances retain {{javaObject}}, it calls {{String.compareTo}}, 
which compares UTF-16 code units.
* Otherwise it materializes the values and compares unsigned UTF-8 bytes.

For valid Unicode strings, these orders differ when an SMP code point is 
compared with a BMP code point in U+E000-U+FFFF. For U+1F600 versus U+FF01, the 
observed signs are:

{code}
UTF-8 bytes: f09f9880 versus efbc81
java/java     = -1
binary/java   =  1
java/binary   =  1
binary/binary =  1
before serialization = -1
after serialization  =  1
{code}

{{StringDataSerializer.serialize}} materializes UTF-8 bytes but leaves the 
original Java object intact, while deserialization creates a binary-backed 
value through {{StringData.fromBytes}}. Therefore a serializer round trip 
changes which comparison path is selected. The original and deserialized values 
compare equal to each other, but have opposite ordering relative to the same 
third value, violating the {{Comparable}} consistency requirement.

*Proposed fix*

Use one canonical unsigned UTF-8 byte ordering for every backing 
representation. The minimal fix is to remove the 
{{javaObject.compareTo(other.javaObject)}} shortcut and always use the existing 
materialized binary comparison. This matches the {{compareTo}} Javadoc and 
{{SortUtil.putStringNormalizedKey}}, which already uses raw UTF-8 bytes. It 
does not require a serialization-format or public-API change.

Because {{compareTo}} is on a sorting hot path, I would also evaluate the 
Java-backed materialization cost. If preserving a no-allocation fast path is 
necessary, it must implement ordering equivalent to encoded UTF-8, including 
malformed-surrogate replacement behavior, rather than delegating to 
{{String.compareTo}}.

*Test plan*

* Cover U+1F600 versus U+FF01 across Java/Java, Java/binary, binary/Java, 
single-segment, and multi-segment representations.
* Assert that a {{StringDataSerializer}} round trip preserves the comparison 
sign and the {{Comparable}} invariant.
* Assert that {{compareTo}} agrees with the string normalized-key order.
* Verify red/green behavior with {{BinaryStringDataTest}}.

One small correction to the reproduction snippet: it currently uses ASCII {{!}} 
(U+0021). It should use {{\uFF01}} or the literal fullwidth character {{!}}; 
ASCII {{!}} does not expose the ordering difference.

Could you please assign FLINK-40265 to me? I would like to work on the fix 
after confirming the approach. Thanks.

> 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
>
> {{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)

Reply via email to