Skip to content

[FLINK-41180][core] Sort VARIANT object fields by UTF-8 bytes - #29607

Merged
raminqaf merged 3 commits into
apache:masterfrom
raminqaf:FLINK-41180-sort-utf-8
Oct 26, 2026
Merged

raminqaf merged 3 commits into
apache:masterfrom
raminqaf:FLINK-41180-sort-utf-8

Conversation

@raminqaf

Copy link
Copy Markdown
Contributor

Builds on #29580 (FLINK-41110). Its commit is the first one here. Please review only the last two commits: the diff on top of #29580.

What is the purpose of the change

The Variant spec sorts the fields of an object by the unsigned UTF-8 bytes of their names. Flink sorted and binary-searched them with String.compareTo, which is UTF-16 order. The two orders disagree when one name has a character from U+E000 to U+FFFF and the other one from U+10000 up:

Key UTF-8 UTF-16
。 U+FF61 EF BD A1 FF61
😀 U+1F600 F0 9F 98 80 D83D DE00

Take an object with the keys k00 to k29, 。 and 😀, so 32 fields. From 32 fields on, getField binary-searches:

Case Before After
Field order the builder writes ..., 😀, 。 ..., 。, 😀, the spec order
getField("😀") on a spec-ordered object, e.g. from another engine null found
getField("😀") / getField("。") on an object older Flink wrote found found
getField("a\uD800"), stored as a?, in 32+ fields null found
{"k?": 1, "k\uD800": 2}, duplicates not allowed two fields named k? fails with VARIANT_DUPLICATE_KEY

Brief change log

  • The builder sorts fields by their stored UTF-8 key bytes. Keys stored as the same bytes are duplicates.
  • getField binary-searches in byte order. A miss for a key with a code point from U+E000 up falls back to a linear scan, so objects in UTF-16 order stay readable. For any other key both orders agree, so a miss is final.
  • [hotfix][docs] syncs the zh VARIANT section, which is kept in English, with the en page. The FLINK-41180 commit then states the key order on both pages.

Verifying this change

This change added tests to BinaryVariantTest:

  • Field order of a builder-written object, which fails before the change.
  • Lookups in a 32-field object in both byte order and UTF-16 order. The fixture covers a 2-byte, a 3-byte, a U+E000 and a 4-byte key, so a signed compare or a fallback threshold of 0xEF fails it.
  • Unpaired surrogate lookup at 1 and 32 fields.
  • Duplicate keys with the same UTF-8 bytes, with the field order asserted after compaction.

Trade-offs

  • The fallback scans all fields, because an object doesn't record which order it uses. Only misses for keys from U+E000 up pay for it, in objects of 32+ fields. A rough nanoTime loop, not JMH, on 1000 fields: an absent emoji key went from 112 ns to 4988 ns. An absent ASCII key went from 120 ns to 79 ns, because the binary search no longer decodes a String per probe. A follow-up can replace the scan with a second binary search in UTF-16 order, as Spark does in SPARK-58949.
  • Compatibility: values that older versions wrote stay readable. An older version can miss keys from U+E000 up in objects of 32+ fields that this version writes. BinaryVariant.equals compares bytes, so an object that mixes the two key ranges and was stored before the upgrade is not equal to the same object built after it. The release note on FLINK-41180 says this.
  • This targets master (2.5). No backport proposed.

Does this pull request potentially affect one of the following parts:

  • Dependencies (does it add or upgrade a dependency): no
  • The public API, i.e., is any changed class annotated with @Public(Evolving): yes, behavior only. BinaryVariantBuilder treats keys with the same UTF-8 bytes as duplicates. No signature changes.
  • The serializers: no
  • The runtime per-record code paths (performance sensitive): yes, getField and object building.
  • Anything that affects deployment or recovery: JobManager (and its components), Checkpointing, Kubernetes/Yarn, ZooKeeper: no
  • The S3 file system connector: no

Documentation

  • Does this pull request introduce a new feature? no
  • If yes, how is the feature documented? not applicable. data-types.md (en and zh) now names the key order.

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code 2.1.310 (Claude Opus 5.5)

Variant.getField decoded each key it compared into a new String. Finding a field in a 13-key object could create 13 throwaway strings.

For objects below BINARY_SEARCH_THRESHOLD fields, getField now encodes the wanted key once and compares it with the UTF-8 bytes of the metadata dictionary. Larger objects still decode each key and compare Strings, because the binary search depends on the order in which the builder sorts the keys.

The wanted key is encoded the same way the builder encodes the keys it writes. So below the threshold, getField("a\uD800") now finds the field stored as "a?". Larger objects still miss such a key.

Metadata keys that are not valid UTF-8 no longer match the U+FFFD name that getFieldNames lists for them, in objects below the threshold. Flink does not write such keys, and distinct malformed keys already decoded to the same name.

JMH on JDK 17, 4 lookups per row of 13 keys: 285 ns to 132 ns per row, 1368 to 296 bytes allocated.

Generated-by: Claude Code 2.1.309 (Claude Opus 5.5)
The zh page keeps the VARIANT section in English, but it missed later changes: the TIME bullet, the cast of a whole ARRAY, MAP, ROW or STRUCTURED value into a VARIANT, the matching cells of the cast matrix, and note 8. Copy them from the en page.

Generated-by: Claude Code 2.1.310 (Claude Opus 5.5)
The variant spec orders the fields of an object by the unsigned UTF-8 bytes of their names. The builder sorted them with String.compareTo, which is UTF-16 order, and getField binary-searched objects of 32 or more fields in the same order. The two orders differ when, at the first differing character, one name has a code point from U+E000 to U+FFFF and the other one from U+10000 up. In a spec-ordered object of 32 fields with the keys U+FF61 and U+1F600, getField(U+1F600) returned null.

The builder now sorts fields by their stored key bytes, and getField binary-searches in the same order. Objects written in UTF-16 order stay readable: after a miss for a key with a code point from U+E000 up, getField scans the fields linearly. For any other key both orders agree, so the miss stands.

Keys that differ as Strings but are stored as the same bytes, such as "a\uD800" and "a?", are now duplicates. Such an object held two fields with the same name before. getField("a\uD800") now finds the field in objects of any size.

Objects with keys from U+E000 up can have a different field order now, which shows in getFieldNames and in CAST to STRING. A Flink version without this change can miss such keys in objects of 32 or more fields. Such an object stored before the upgrade is not byte-equal to the same object built after it.

Generated-by: Claude Code 2.1.310 (Claude Opus 5.5)
@flinkbot

flinkbot commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands The @flinkbot bot supports the following commands:
  • @flinkbot run azure re-run the last Azure build

@twalthr twalthr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, thanks @raminqaf

@raminqaf
raminqaf merged commit dab40d3 into apache:master Oct 26, 2026
1 check passed
@raminqaf
raminqaf deleted the FLINK-41180-sort-utf-8 branch October 26, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants