Skip to content

[FLINK-39677][table] Fix ARRAY_SORT comparator contract violation#28155

Open
jnh5y wants to merge 1 commit into
apache:masterfrom
jnh5y:FLINK-39677-array-sort-comparator-contract
Open

[FLINK-39677][table] Fix ARRAY_SORT comparator contract violation#28155
jnh5y wants to merge 1 commit into
apache:masterfrom
jnh5y:FLINK-39677-array-sort-comparator-contract

Conversation

@jnh5y
Copy link
Copy Markdown
Contributor

@jnh5y jnh5y commented May 13, 2026

The comparator built from a single SQL > probe returned +1 or -1 and never 0, so for equal elements compare(a,b) == compare(b,a) == -1. That violates antisymmetry and trips TimSort's contract check once an array is large enough to take the merge path (>= 32 elements with duplicates):

java.lang.IllegalArgumentException: Comparison method violates its
general contract!
    at java.util.TimSort.mergeHi(TimSort.java:903)
    ...
    at ArraySortFunction.eval(ArraySortFunction.java:91)

Derive ordering from two greater-than probes - if neither side is greater, treat the elements as equal and return 0. This satisfies both antisymmetry and (for total orders) transitivity.

Coverage: a new 64-element BIGINT case in CollectionFunctionsITCase exercises the TimSort merge path with duplicates.

Generated-by: Claude (Opus 4.7)

Verifying this change

This change added tests which fails without the fix.

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): no
  • The serializers: no
  • The runtime per-record code paths (performance sensitive): no
  • 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

Was generative AI tooling used to co-author this PR?

Generated-by: Claude (Opus 4.7)

@flinkbot
Copy link
Copy Markdown
Collaborator

flinkbot commented May 13, 2026

CI report:

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

private Stream<TestSetSpec> arraySortBigIntDuplicatesTestCases() {
final int size = 64;
Long[] allEqual = new Long[size];
java.util.Arrays.fill(allEqual, 42L);
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.

absolute name

manyDuplicates[i] = (long) (i % 4);
}
Long[] manyDuplicatesSortedAsc = manyDuplicates.clone();
java.util.Arrays.sort(manyDuplicatesSortedAsc);
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.

absolute name

Comment on lines +1618 to +1622
for (int i = 0, j = size - 1; i < j; i++, j--) {
Long tmp = manyDuplicatesSortedDesc[i];
manyDuplicatesSortedDesc[i] = manyDuplicatesSortedDesc[j];
manyDuplicatesSortedDesc[j] = tmp;
}
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.

Can we just use ArrayUtils.reverse()?

Comment on lines +113 to +118
if ((boolean) greaterHandle.invoke(o1, o2)) {
return isAscending ? 1 : -1;
}
if ((boolean) greaterHandle.invoke(o2, o1)) {
return isAscending ? -1 : 1;
}
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.

instead of having 2 invocations we could have 2 evaluators

  1. for isGreaterOrEqueal
  2. for isLessOrEqual

WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What would this get us?

It seems like it would only add more MethodHandles in this class?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I mean, I could see adding a builtin COMPARE(a, b) → -1/0/+1 function in BuiltInFunctionDefinitions. Would that be better?

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.

we don't have such function right?
adding a new function ideally requires research what other vendors provide to follow the most common syntax.

From existing we could use CASE ...WHEN which is lazy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I thinking of an internal-only function like $HASH_CODE$1 as an option.

How would CASE ...WHEN work here?

Are either of these ideas worth it? Or should we just go with the current approach?

The comparator built from a single SQL > probe returned +1 or -1 and
never 0, so for equal elements compare(a,b) == compare(b,a) == -1.
That violates antisymmetry and trips TimSort's contract check once an
array is large enough to take the merge path (>= 32 elements with
duplicates):

    java.lang.IllegalArgumentException: Comparison method violates its
    general contract!
        at java.util.TimSort.mergeHi(TimSort.java:903)
        ...
        at ArraySortFunction.eval(ArraySortFunction.java:91)

Derive ordering from two greater-than probes - if neither side is
greater, treat the elements as equal and return 0. This satisfies both
antisymmetry and (for total orders) transitivity.

Coverage: a new 64-element BIGINT case in CollectionFunctionsITCase
exercises the TimSort merge path with duplicates.

Generated-by: Claude (Opus 4.7)
@jnh5y jnh5y force-pushed the FLINK-39677-array-sort-comparator-contract branch from 2725b9e to fd4d7aa Compare May 13, 2026 20:30
Copy link
Copy Markdown
Contributor

@dylanhz dylanhz left a comment

Choose a reason for hiding this comment

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

I have a related question: does Flink have a unified and documented ordering semantics for special floating-point values such as NaN, +Infinity, and -Infinity?

@github-actions github-actions Bot added the community-reviewed PR has been reviewed by the community. label May 14, 2026
@jnh5y
Copy link
Copy Markdown
Contributor Author

jnh5y commented May 14, 2026

I have a related question: does Flink have a unified and documented ordering semantics for special floating-point values such as NaN, +Infinity, and -Infinity?

That's a great question. Separately, I did look for other issues in other ARRAY functions and found some questions about how NaNs are handled in ARRAY_MIN/MAX.

I'd like to hold that as a separate thread/issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-reviewed PR has been reviewed by the community.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants