This is an automated email from the ASF dual-hosted git repository.
mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 23f01e27c7 [CALCITE-7837] ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw
ClassCastException for ROW elements
23f01e27c7 is described below
commit 23f01e27c75911a0972125deaf02721a8b202267
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Mon Oct 5 22:33:11 2026 +0300
[CALCITE-7837] ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException
for ROW elements
---
.../org/apache/calcite/runtime/SqlFunctions.java | 20 ++++----
.../org/apache/calcite/test/SqlFunctionsTest.java | 49 +++++++++++++++++++
.../apache/calcite/linq4j/function/Functions.java | 14 +++++-
.../calcite/linq4j/function/FunctionTest.java | 43 ++++++++++++++++
.../org/apache/calcite/test/SqlOperatorTest.java | 57 +++++++++++++++++++++-
5 files changed, 169 insertions(+), 14 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 4e77144243..36dc21358a 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -7163,13 +7163,11 @@ public static List distinct(List list) {
}
/** Support the ARRAY_MAX function. */
- public static @Nullable <T extends Object & Comparable<? super T>> T
arrayMax(
- List<? extends T> list) {
-
+ public static @Nullable <T> T arrayMax(List<? extends T> list) {
T max = null;
for (int i = 0; i < list.size(); i++) {
T item = list.get(i);
- if (item != null && (max == null || item.compareTo(max) > 0)) {
+ if (item != null && (max == null || compareNullable(item, max,
"ARRAY_MAX") > 0)) {
max = item;
}
}
@@ -7177,13 +7175,11 @@ public static List distinct(List list) {
}
/** Support the ARRAY_MIN function. */
- public static @Nullable <T extends Object & Comparable<? super T>> T
arrayMin(
- List<? extends T> list) {
-
+ public static @Nullable <T> T arrayMin(List<? extends T> list) {
T min = null;
for (int i = 0; i < list.size(); i++) {
T item = list.get(i);
- if (item != null && (min == null || item.compareTo(min) < 0)) {
+ if (item != null && (min == null || compareNullable(item, min,
"ARRAY_MIN") < 0)) {
min = item;
}
}
@@ -7383,9 +7379,11 @@ private static class TransformingList<F, T> extends
AbstractList<T> {
/** Support the SORT_ARRAY function. */
public static List sortArray(List list, boolean ascending) {
- Comparator comparator = ascending
- ? Comparator.nullsFirst(Comparator.naturalOrder())
- : Comparator.nullsLast(Comparator.reverseOrder());
+ final Comparator<@Nullable Object> naturalComparator =
+ (a, b) -> compareNullable(a, b, "SORT_ARRAY");
+ final Comparator<@Nullable Object> comparator = ascending
+ ? Comparator.nullsFirst(naturalComparator)
+ : Comparator.nullsLast(naturalComparator.reversed());
list.sort(comparator);
return list;
}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index 08d684f031..a52274f8fb 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -117,6 +117,55 @@ static <E> List<E> list() {
return ImmutableList.of();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
+ @Test void testArrayComparisonWithSpecialFloatingPointValues() {
+ final List<Double> values =
+ list(Double.NaN, null, Double.NEGATIVE_INFINITY, 1D,
Double.POSITIVE_INFINITY);
+ assertThat(SqlFunctions.arrayMax(values), is(Double.NaN));
+ assertThat(SqlFunctions.arrayMin(values), is(Double.NEGATIVE_INFINITY));
+ assertThat(SqlFunctions.sortArray(values, true),
+ is(list(null, Double.NEGATIVE_INFINITY, 1D, Double.POSITIVE_INFINITY,
Double.NaN)));
+ assertThat(SqlFunctions.sortArray(values, false),
+ is(list(Double.NaN, Double.POSITIVE_INFINITY, 1D,
Double.NEGATIVE_INFINITY, null)));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
+ @Test void testArrayRowComparisonWithSpecialFloatingPointValues() {
+ for (double special : new double[] {
+ Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY}) {
+ final boolean specialFirst = Double.compare(special, 1D) < 0;
+ checkArrayRowComparison(FlatLists.ofComparable(list(special)),
+ FlatLists.ofComparable(list(1D)), specialFirst);
+ checkArrayRowComparison(new Object[] {special}, new Object[] {1D},
specialFirst);
+ checkArrayRowComparison(new Object[] {new Object[] {special}},
+ new Object[] {new Object[] {1D}}, specialFirst);
+ }
+ for (float special : new float[] {
+ Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY}) {
+ final boolean specialFirst = Float.compare(special, 1F) < 0;
+ checkArrayRowComparison(FlatLists.ofComparable(list(special)),
+ FlatLists.ofComparable(list(1F)), specialFirst);
+ checkArrayRowComparison(new Object[] {special}, new Object[] {1F},
specialFirst);
+ checkArrayRowComparison(new Object[] {new Object[] {special}},
+ new Object[] {new Object[] {1F}}, specialFirst);
+ }
+ }
+
+ private static void checkArrayRowComparison(Object specialRow, Object
normalRow,
+ boolean specialFirst) {
+ final List<Object> rows = list(specialRow, normalRow, null);
+ final Object min = specialFirst ? specialRow : normalRow;
+ final Object max = specialFirst ? normalRow : specialRow;
+ assertSame(max, SqlFunctions.arrayMax(rows));
+ assertSame(min, SqlFunctions.arrayMin(rows));
+ assertThat(SqlFunctions.sortArray(new ArrayList<>(rows), true),
is(list(null, min, max)));
+ assertThat(SqlFunctions.sortArray(new ArrayList<>(rows), false),
is(list(max, min, null)));
+ }
+
@Test void testArraysOverlap() {
final List<Object> listWithOnlyNull = new ArrayList<>();
listWithOnlyNull.add(null);
diff --git
a/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java
b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java
index 89ee4699e1..686045af70 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/function/Functions.java
@@ -784,7 +784,9 @@ private static BigDecimal toBigDecimal(Number number) {
/** Compares two values as elements of a list, array or row: nested
* collections and arrays are compared element-wise, numbers are compared by
- * value regardless of their Java type, and nulls sort last. */
+ * value regardless of their Java type, and nulls sort last. Floating-point
+ * values of the same Java type use their natural order, with signed zeros
+ * comparing equal. */
public static int compareListItems(@Nullable Object item0, @Nullable Object
item1) {
if (item0 == item1) {
return 0;
@@ -803,6 +805,16 @@ public static int compareListItems(@Nullable Object item0,
@Nullable Object item
} else if (item0 instanceof Object[] && item1 instanceof Object[]) {
return compareObjectArrays((Object[]) item0, (Object[]) item1);
} else if (item0 instanceof Number && item1 instanceof Number) {
+ if (item0 instanceof Double && item1 instanceof Double) {
+ final double d0 = (Double) item0;
+ final double d1 = (Double) item1;
+ return d0 == d1 ? 0 : Double.compare(d0, d1);
+ }
+ if (item0 instanceof Float && item1 instanceof Float) {
+ final float f0 = (Float) item0;
+ final float f1 = (Float) item1;
+ return f0 == f1 ? 0 : Float.compare(f0, f1);
+ }
final BigDecimal d0 = toBigDecimal((Number) item0);
final BigDecimal d1 = toBigDecimal((Number) item1);
return d0.compareTo(d1);
diff --git
a/linq4j/src/test/java/org/apache/calcite/linq4j/function/FunctionTest.java
b/linq4j/src/test/java/org/apache/calcite/linq4j/function/FunctionTest.java
index 8c6f758b1b..c92b90bffc 100644
--- a/linq4j/src/test/java/org/apache/calcite/linq4j/function/FunctionTest.java
+++ b/linq4j/src/test/java/org/apache/calcite/linq4j/function/FunctionTest.java
@@ -18,6 +18,8 @@
import org.junit.jupiter.api.Test;
+import java.math.BigDecimal;
+import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -83,6 +85,47 @@ class FunctionTest {
Functions.all(empty, Functions.truePredicate1()));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
+ @Test void testCompareNumbersWithSpecialFloatingPointValues() {
+ final List<List<Number>> types =
+ Arrays.asList(
+ Arrays.asList(Double.NEGATIVE_INFINITY, -1D, 1D,
Double.POSITIVE_INFINITY, Double.NaN),
+ Arrays.asList(Float.NEGATIVE_INFINITY, -1F, 1F,
Float.POSITIVE_INFINITY, Float.NaN));
+ for (List<Number> values : types) {
+ for (int i = 0; i < values.size(); i++) {
+ for (int j = 0; j < values.size(); j++) {
+ assertThat(values.get(i) + " compared with " + values.get(j),
+ Integer.signum(Functions.compareListItems(values.get(i),
values.get(j))),
+ is(Integer.compare(i, j)));
+ }
+ }
+ }
+ assertThat(Functions.compareListItems(-0D, 0D), is(0));
+ assertThat(Functions.compareListItems(0D, -0D), is(0));
+ assertThat(Functions.compareListItems(-0F, 0F), is(0));
+ assertThat(Functions.compareListItems(0F, -0F), is(0));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
+ @Test void testCompareNumbersOfDifferentTypes() {
+ final List<Number> ones = Arrays.asList(1, 1L, 1F, 1D, BigInteger.ONE,
BigDecimal.ONE);
+ for (Number a : ones) {
+ for (Number b : ones) {
+ assertThat(Functions.compareListItems(a, b), is(0));
+ }
+ }
+ // Finite numbers must still be compared without losing decimal precision.
+ assertTrue(Functions.compareListItems(1D, new
BigDecimal("1.0000000000000000001")) < 0);
+ assertTrue(Functions.compareListItems(Long.MAX_VALUE, Long.MAX_VALUE - 1)
> 0);
+ assertTrue(Functions.compareListItems(Double.MAX_VALUE, new
BigDecimal("1E400")) < 0);
+ assertThat(Functions.compareListItems(BigInteger.TEN.pow(400), new
BigDecimal("1E400")),
+ is(0));
+ }
+
/** Unit test for {@link Functions#compareMaps}. Maps are unordered, so the
* comparison must not depend on insertion order, and must return 0 exactly
* when the maps have equal contents; see
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 7d9dd1946c..53d093144b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -8588,7 +8588,9 @@ void checkRegexpExtract(SqlOperatorFixture f0,
FunctionAlias functionAlias) {
f0.forEachLibrary(libraries, consumer);
}
- /** Tests {@code ARRAY_MAX} function from Spark, Hive. */
+ /** Tests {@code ARRAY_MAX} function from Spark, Hive, including
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
@Test void testArrayMaxFunc() {
final SqlOperatorFixture f0 =
fixture().setFor(SqlLibraryOperators.ARRAY_MAX);
f0.checkFails("^array_max(array[1, 2])^",
@@ -8599,6 +8601,18 @@ void checkRegexpExtract(SqlOperatorFixture f0,
FunctionAlias functionAlias) {
f.checkScalar("array_max(array[1, 2])", "2", "INTEGER");
f.checkScalar("array_max(array[1, 2, null])", "2", "INTEGER");
f.checkScalar("array_max(array[1])", "1", "INTEGER");
+ f.checkScalar("array_max(array[row(1, 'Foo'), row(2, 'Bar')])", "{2,
Bar}",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1)");
+ f.checkScalar("array_max(array[row(1, 'Bar'), row(1, 'Foo'), null])",
"{1, Foo}",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1)");
+ f.checkScalar("array_max(array[row(1, row(2, 'Bar')), row(1, row(2,
'Foo'))])",
+ "{1, {2, Foo}}",
+ "RecordType(INTEGER EXPR$0, RecordType(INTEGER EXPR$0, CHAR(3)
EXPR$1) EXPR$1)");
+ f.checkScalar("array_max(array[row(1, 2), row(1, null)])", "{1, null}",
+ "RecordType(INTEGER EXPR$0, INTEGER EXPR$1)");
+ f.checkScalar("array_max(array[row(1, 2)])", "{1, 2}",
+ "RecordType(INTEGER EXPR$0, INTEGER EXPR$1)");
+ f.checkNull("array_max(array[cast(null as row(x integer, y integer))])");
f.checkType("array_max(array())", "UNKNOWN");
f.checkNull("array_max(array())");
f.checkNull("array_max(cast(null as integer array))");
@@ -8613,7 +8627,9 @@ void checkRegexpExtract(SqlOperatorFixture f0,
FunctionAlias functionAlias) {
f0.forEachLibrary(libraries, consumer);
}
- /** Tests {@code ARRAY_MIN} function from Spark, Hive. */
+ /** Tests {@code ARRAY_MIN} function from Spark, Hive, including
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-7837">[CALCITE-7837]
+ * ARRAY_MIN, ARRAY_MAX and SORT_ARRAY throw ClassCastException for ROW
elements</a>. */
@Test void testArrayMinFunc() {
final SqlOperatorFixture f0 =
fixture().setFor(SqlLibraryOperators.ARRAY_MIN);
f0.checkFails("^array_min(array[1, 2])^",
@@ -8622,6 +8638,18 @@ void checkRegexpExtract(SqlOperatorFixture f0,
FunctionAlias functionAlias) {
ImmutableList.of(SqlLibrary.HIVE, SqlLibrary.SPARK);
final Consumer<SqlOperatorFixture> consumer = f -> {
f.checkScalar("array_min(array[1, 2])", "1", "INTEGER");
+ f.checkScalar("array_min(array[row(2, 'Bar'), row(1, 'Foo')])", "{1,
Foo}",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1)");
+ f.checkScalar("array_min(array[null, row(1, 'Foo'), row(1, 'Bar')])",
"{1, Bar}",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1)");
+ f.checkScalar("array_min(array[row(1, row(2, 'Foo')), row(1, row(2,
'Bar'))])",
+ "{1, {2, Bar}}",
+ "RecordType(INTEGER EXPR$0, RecordType(INTEGER EXPR$0, CHAR(3)
EXPR$1) EXPR$1)");
+ f.checkScalar("array_min(array[row(1, null), row(1, 2)])", "{1, 2}",
+ "RecordType(INTEGER EXPR$0, INTEGER EXPR$1)");
+ f.checkScalar("array_min(array[row(1, 2)])", "{1, 2}",
+ "RecordType(INTEGER EXPR$0, INTEGER EXPR$1)");
+ f.checkNull("array_min(array[cast(null as row(x integer, y integer))])");
f.checkScalar("array_min(array[1, 2, null])", "1", "INTEGER");
f.checkType("array_min(array())", "UNKNOWN");
f.checkNull("array_min(array())");
@@ -9319,6 +9347,31 @@ void checkArrayReverseFunc(SqlOperatorFixture f0,
SqlFunction function,
f.checkScalar("sort_array(array())", "[]",
"UNKNOWN NOT NULL ARRAY NOT NULL");
f.checkNull("sort_array(null)");
+ f.checkScalar("sort_array(array[row(2, 'Bar'), row(1, 'Foo')])",
+ "[{1, Foo}, {2, Bar}]",
+ "RecordType(INTEGER NOT NULL EXPR$0, CHAR(3) NOT NULL EXPR$1) NOT NULL
ARRAY NOT NULL");
+ f.checkScalar("sort_array(array[row(1, 'Foo'), null, row(1, 'Bar')])",
+ "[null, {1, Bar}, {1, Foo}]",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1) ARRAY NOT NULL");
+ f.checkScalar("sort_array(array[row(1, 'Bar'), null, row(1, 'Foo')],
false)",
+ "[{1, Foo}, {1, Bar}, null]",
+ "RecordType(INTEGER EXPR$0, CHAR(3) EXPR$1) ARRAY NOT NULL");
+ f.checkScalar("sort_array(array[row(1, row(2, 'Foo')), row(1, row(2,
'Bar'))])",
+ "[{1, {2, Bar}}, {1, {2, Foo}}]",
+ "RecordType(INTEGER NOT NULL EXPR$0, "
+ + "RecordType(INTEGER NOT NULL EXPR$0, CHAR(3) NOT NULL EXPR$1)
NOT NULL EXPR$1) "
+ + "NOT NULL ARRAY NOT NULL");
+ f.checkScalar("sort_array(array[row(1, row(2, 'Bar')), row(1, row(2,
'Foo'))], false)",
+ "[{1, {2, Foo}}, {1, {2, Bar}}]",
+ "RecordType(INTEGER NOT NULL EXPR$0, "
+ + "RecordType(INTEGER NOT NULL EXPR$0, CHAR(3) NOT NULL EXPR$1)
NOT NULL EXPR$1) "
+ + "NOT NULL ARRAY NOT NULL");
+ f.checkScalar("sort_array(array[row(1, null), row(1, 2)])",
+ "[{1, 2}, {1, null}]",
+ "RecordType(INTEGER NOT NULL EXPR$0, INTEGER EXPR$1) NOT NULL ARRAY
NOT NULL");
+ f.checkScalar("sort_array(array[row(1, 2), row(1, null)], false)",
+ "[{1, null}, {1, 2}]",
+ "RecordType(INTEGER NOT NULL EXPR$0, INTEGER EXPR$1) NOT NULL ARRAY
NOT NULL");
// elements cast
f.checkScalar("sort_array(array[cast(1 as tinyint), 2])", "[1, 2]",