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]",

Reply via email to