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 fb3a83da82 [CALCITE-7836] Array functions return incorrect results for 
ROW elements
fb3a83da82 is described below

commit fb3a83da8289d4f488cb2dafb1a33ee7079801a6
Author: Kirill Tkalenko <[email protected]>
AuthorDate: Thu Oct 8 07:29:19 2026 +0300

    [CALCITE-7836] Array functions return incorrect results for ROW elements
---
 .../calcite/adapter/enumerable/RexImpTable.java    |   2 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 118 ++++--
 .../org/apache/calcite/util/BuiltInMethod.java     |   1 +
 .../org/apache/calcite/test/SqlFunctionsTest.java  |  82 +++++
 core/src/test/resources/sql/row-equality.iq        | 399 +++++++++++++++++++++
 .../apache/calcite/linq4j/function/Functions.java  | 183 +++++-----
 6 files changed, 671 insertions(+), 114 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 28083c6616..79424fe6e4 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -1109,7 +1109,7 @@ void populate2() {
       defineMethod(MEMBER_OF, BuiltInMethod.MEMBER_OF.method, NullPolicy.NONE);
       defineMethod(ARRAY_APPEND, BuiltInMethod.ARRAY_APPEND.method, 
NullPolicy.ARG0);
       defineMethod(ARRAY_COMPACT, BuiltInMethod.ARRAY_COMPACT.method, 
NullPolicy.STRICT);
-      defineMethod(ARRAY_CONTAINS, BuiltInMethod.LIST_CONTAINS.method, 
NullPolicy.SEMI_STRICT);
+      defineMethod(ARRAY_CONTAINS, BuiltInMethod.ARRAY_CONTAINS.method, 
NullPolicy.SEMI_STRICT);
       defineMethod(ARRAY_DISTINCT, BuiltInMethod.ARRAY_DISTINCT.method, 
NullPolicy.STRICT);
       defineMethod(ARRAY_EXCEPT, BuiltInMethod.ARRAY_EXCEPT.method, 
NullPolicy.SEMI_STRICT);
       defineMethod(ARRAY_JOIN, BuiltInMethod.ARRAY_TO_STRING.method,
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 9a522661bb..5e1988a996 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -7096,12 +7096,12 @@ private static AtomicLong getAtomicLong(String key) {
     final List bigger = list2;
     boolean hasNull = false;
     if (!smaller.isEmpty() && !bigger.isEmpty()) {
-      final Set smallestSet = new HashSet(smaller);
+      final Set<@Nullable Object> smallestSet = toDeepEqualitySet(smaller);
       hasNull = smallestSet.remove(null);
       for (Object element : bigger) {
         if (element == null) {
           hasNull = true;
-        } else if (smallestSet.contains(element)) {
+        } else if (smallestSet.contains(wrapForDeepEquality(element))) {
           return true;
         }
       }
@@ -7156,13 +7156,9 @@ public static List arrayAppend(List list, Object 
element) {
     return result;
   }
 
-  /** Support the ARRAY_DISTINCT function.
-   *
-   * <p>Note: If the list does not contain null,
-   * {@link Util#distinctList(List)} is probably faster. */
+  /** Support the ARRAY_DISTINCT function. */
   public static List distinct(List list) {
-    Set result = new LinkedHashSet<>(list);
-    return new ArrayList<>(result);
+    return unwrapDeepEqualitySet(toDeepEqualitySet(list));
   }
 
   /** Support the ARRAY_MAX function. */
@@ -7197,11 +7193,24 @@ public static List arrayPrepend(List list, Object 
element) {
     return result;
   }
 
+  /** Support the ARRAY_CONTAINS function. */
+  public static boolean arrayContains(List list, Object element) {
+    for (Object value : list) {
+      if (Functions.deepEquals(value, element)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /** Support the ARRAY_POSITION function. */
   public static Long arrayPosition(List list, Object element) {
-    final int index = list.indexOf(element);
-    if (index != -1) {
-      return index + 1L;
+    long position = 1;
+    for (Object value : list) {
+      if (Functions.deepEquals(value, element)) {
+        return position;
+      }
+      position++;
     }
     return 0L;
   }
@@ -7210,7 +7219,7 @@ public static Long arrayPosition(List list, Object 
element) {
   public static List arrayRemove(List list, Object element) {
     final List result = new ArrayList();
     for (Object obj : list) {
-      if (obj == null || !obj.equals(element)) {
+      if (obj == null || !Functions.deepEquals(obj, element)) {
         result.add(obj);
       }
     }
@@ -7231,9 +7240,9 @@ public static List arrayRemove(List list, Object element) 
{
 
   /** Support the ARRAY_EXCEPT function. */
   public static List arrayExcept(List list1, List list2) {
-    final Set result = new LinkedHashSet<>(list1);
-    result.removeAll(list2);
-    return new ArrayList<>(result);
+    final Set<@Nullable Object> result = toDeepEqualitySet(list1);
+    result.removeAll(toDeepEqualitySet(list2));
+    return unwrapDeepEqualitySet(result);
   }
 
   /** Support the ARRAY_INSERT function. */
@@ -7333,17 +7342,82 @@ public static List arrayExcept(List list1, List list2) {
 
   /** Support the ARRAY_INTERSECT function. */
   public static List arrayIntersect(List list1, List list2) {
-    final Set result = new LinkedHashSet<>(list1);
-    result.retainAll(list2);
-    return new ArrayList<>(result);
+    final Set<@Nullable Object> result = toDeepEqualitySet(list1);
+    result.retainAll(toDeepEqualitySet(list2));
+    return unwrapDeepEqualitySet(result);
   }
 
   /** Support the ARRAY_UNION function. */
   public static List arrayUnion(List list1, List list2) {
-    final Set result = new LinkedHashSet<>();
-    result.addAll(list1);
-    result.addAll(list2);
-    return new ArrayList<>(result);
+    final Set<@Nullable Object> result = toDeepEqualitySet(list1);
+    for (Object element : list2) {
+      result.add(wrapForDeepEquality(element));
+    }
+    return unwrapDeepEqualitySet(result);
+  }
+
+  /** Creates a set using deep equality, preserving element encounter order.
+   * This preserves the ordering required by ARRAY_DISTINCT. */
+  private static Set<@Nullable Object> toDeepEqualitySet(List<?> list) {
+    final Set<@Nullable Object> set = new LinkedHashSet<>();
+    for (Object value : list) {
+      set.add(wrapForDeepEquality(value));
+    }
+    return set;
+  }
+
+  /** Wraps composite values, which may contain ROW values, but leaves scalar
+   * values and null unchanged. */
+  private static @Nullable Object wrapForDeepEquality(@Nullable Object value) {
+    if (value == null) {
+      return null;
+    } else if (value instanceof List || value instanceof Map || 
value.getClass().isArray()) {
+      return new DeepEqualityWrapper(value);
+    }
+    return value;
+  }
+
+  /** Creates a list of the set's elements in iteration order, replacing
+   * deep-equality wrappers with their original values. */
+  private static List<@Nullable Object> unwrapDeepEqualitySet(Set<@Nullable 
Object> set) {
+    final List<@Nullable Object> values = new ArrayList<>(set.size());
+    for (Object element : set) {
+      values.add(element instanceof DeepEqualityWrapper
+          ? ((DeepEqualityWrapper) element).value : element);
+    }
+    return values;
+  }
+
+  /** A wrapper providing deep equality and hashing for composite values.
+   * Assumes values are immutable. */
+  private static final class DeepEqualityWrapper {
+    private final Object value;
+    // Benign races may compute the same hash more than once.
+    private int hashCode;
+
+    private DeepEqualityWrapper(Object value) {
+      this.value = value;
+    }
+
+    @Override public boolean equals(@Nullable Object obj) {
+      return this == obj
+          || obj instanceof DeepEqualityWrapper
+          && Functions.deepEquals(value, ((DeepEqualityWrapper) obj).value);
+    }
+
+    @Override public int hashCode() {
+      int hash = hashCode;
+      if (hash == 0) {
+        hash = Functions.deepHashCode(value);
+        // Zero means not yet computed; cache a computed zero as 1 to avoid
+        // recomputing it on every call.
+        if (hash == 0) {
+          hash = 1;
+        }
+        hashCode = hash;
+      }
+      return hash;
+    }
   }
 
   /** Transforms a list, applying a function to each element. */
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 788fe26d29..004537b2ad 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -924,6 +924,7 @@ public enum BuiltInMethod {
       Collection.class),
   ARRAY_APPEND(SqlFunctions.class, "arrayAppend", List.class, Object.class),
   ARRAY_COMPACT(SqlFunctions.class, "compact", List.class),
+  ARRAY_CONTAINS(SqlFunctions.class, "arrayContains", List.class, 
Object.class),
   ARRAY_DISTINCT(SqlFunctions.class, "distinct", List.class),
   ARRAY_MAX(SqlFunctions.class, "arrayMax", List.class),
   ARRAY_MIN(SqlFunctions.class, "arrayMin", List.class),
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 a52274f8fb..fa29ceebbf 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -166,6 +166,64 @@ private static void checkArrayRowComparison(Object 
specialRow, Object normalRow,
     assertThat(SqlFunctions.sortArray(new ArrayList<>(rows), false), 
is(list(max, min, null)));
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7836";>[CALCITE-7836]
+   * Array functions return incorrect results for ROW elements</a>. */
+  @Test void testArrayFunctionsWithRows() {
+    // A custom Python script ran the SQL regression cases in 
sql/row-equality.iq
+    // against Apache Spark 3.5.5 (adapting ARRAY and ROW syntax) and confirmed
+    // all results. The cases below add Java representation, MAP and hash
+    // collision coverage that was not checked against Spark.
+    final Object[][] rows = {
+        {1, "Foo"},
+        {null, "Foo"},
+        {new Object[] {1, "Foo"}, 10},
+        {list(new Object[] {1, "Foo"}, new Object[] {2, null}), 10},
+        {Collections.singletonMap("key", new Object[] {1, null}), 10},
+        {-31}, // Deep hash code is zero.
+        {-30} // Deep hash code collides with the replacement for zero.
+    };
+    for (Object[] row : rows) {
+      // Copy the outer row; the nested containers are checked separately 
below.
+      checkArrayFunctionsWithRows(row, row.clone());
+    }
+    checkArrayFunctionsWithRows(new Object[] {new Object[] {1, null}, 10},
+        new Object[] {list(1, null), 10});
+    checkArrayFunctionsWithRows(
+        new Object[] {list(new Object[] {1, "Foo"}, new Object[] {2, null}), 
10},
+        new Object[] {list(new Object[] {1, "Foo"}, new Object[] {2, null}), 
10});
+    checkArrayFunctionsWithRows(
+        new Object[] {Collections.singletonMap("key", new Object[] {1, null})},
+        new Object[] {Collections.singletonMap("key", new Object[] {1, 
null})});
+    assertThat(SqlFunctions.distinct(list(new Object[] {-31}, new Object[] 
{-30})).size(),
+        is(2));
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7836";>[CALCITE-7836]
+   * Array functions return incorrect results for ROW elements</a>. */
+  @Test void testArrayFunctionsWithMixedValues() {
+    // These Java representation and mixed-type checks are Calcite-specific.
+    // SQL regression cases for all eight functions were checked separately
+    // against Apache Spark 3.5.5 using a custom Python script; see 
sql/row-equality.iq.
+    checkArrayFunctionsWithRows(1, 1);
+    checkArrayFunctionsWithRows("Foo", "Foo");
+    checkArrayFunctionsWithRows(new byte[] {1, 2}, new byte[] {1, 2});
+    checkArrayFunctionsWithRows(list(1, null), new Object[] {1, null});
+    checkArrayFunctionsWithRows(
+        Collections.singletonMap("key", new Object[] {1, null}),
+        Collections.singletonMap("key", new Object[] {1, null}));
+
+    final Object row = new Object[] {1, "Foo"};
+    final List<Object> input = new ArrayList<>(list(2, row, null, "Foo", 2));
+    final List<?> result = SqlFunctions.distinct(input);
+    input.clear();
+    assertThat(result, is(list(2, row, null, "Foo")));
+    assertThat(result.get(1), is(row));
+    assertThat(result.subList(1, 3), is(list(row, null)));
+    assertThat(Arrays.asList(result.toArray()), is(list(2, row, null, "Foo")));
+  }
+
   @Test void testArraysOverlap() {
     final List<Object> listWithOnlyNull = new ArrayList<>();
     listWithOnlyNull.add(null);
@@ -2507,4 +2565,28 @@ private static List<List<Comparable>> rowArray() {
     assertThat(fn.apply(null).toList(),
         is(Collections.singletonList(null)));
   }
+
+  private static void checkArrayFunctionsWithRows(Object row, Object equalRow) 
{
+    // The Spark-validated SQL results are recorded in sql/row-equality.iq.
+    // This helper also exercises Java-specific inputs; its full set of inputs
+    // and assertions was not run against Spark.
+    final Object other = new Object[] {9, "Other"};
+    final List<Object> input = list(row, null, other, equalRow);
+    assertThat(SqlFunctions.arrayContains(input, equalRow), is(true));
+    assertThat(SqlFunctions.arrayContains(list(other), row), is(false));
+    assertThat(SqlFunctions.arrayPosition(input, equalRow), is(1L));
+    assertThat(SqlFunctions.arrayPosition(list(other), row), is(0L));
+    assertThat(SqlFunctions.arrayRemove(input, equalRow), is(list(null, 
other)));
+    assertThat(SqlFunctions.distinct(input), is(list(row, null, other)));
+    assertThat(SqlFunctions.arrayUnion(input, list(equalRow, other, null)),
+        is(list(row, null, other)));
+    assertThat(SqlFunctions.arrayIntersect(input, list(equalRow, null)),
+        is(list(row, null)));
+    assertThat(SqlFunctions.arrayExcept(input, list(equalRow, null)), 
is(list(other)));
+    assertThat(arraysOverlap(input, list(equalRow)), is(true));
+    assertThat(arraysOverlap(list(row), list(other)), is(false));
+    assertThat(arraysOverlap(list(row, null), list(other)), is(nullValue()));
+    assertThat(arraysOverlap(list(), input), is(false));
+    assertThat(input, is(list(row, null, other, equalRow)));
+  }
 }
diff --git a/core/src/test/resources/sql/row-equality.iq 
b/core/src/test/resources/sql/row-equality.iq
index a61e38704a..b83302a006 100644
--- a/core/src/test/resources/sql/row-equality.iq
+++ b/core/src/test/resources/sql/row-equality.iq
@@ -612,4 +612,403 @@ ORDER BY c;
 
 !ok
 
+# Test cases for https://issues.apache.org/jira/browse/CALCITE-7836
+# [CALCITE-7836] Array functions return incorrect results for ROW elements
+# All 32 queries below were checked against Apache Spark 3.5.5 using a custom
+# Python script in Spark Playground. All 64 results matched, including array
+# contents and element order. The script used array(...) for ARRAY[...] and
+# named_struct(...) for ROW(...), with matching field names.
+!use sparkfunc
+
+# Flat ROW values.
+SELECT ARRAY_CONTAINS(ARRAY[ROW(x, y)], ROW(z, w)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_POSITION(ARRAY[ROW(x, y)], ROW(z, w)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++---+
+| R |
++---+
+| 1 |
+| 0 |
++---+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_REMOVE(ARRAY[ROW(x, y), ROW(z, w)], ROW(z, w)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------+
+| R          |
++------------+
+| []         |
+| [{2, Bar}] |
++------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_DISTINCT(ARRAY[ROW(x, y), ROW(z, w)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----------------------+
+| R                    |
++----------------------+
+| [{1, Foo}]           |
+| [{2, Bar}, {2, Car}] |
++----------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_UNION(ARRAY[ROW(x, y)], ARRAY[ROW(z, w)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----------------------+
+| R                    |
++----------------------+
+| [{1, Foo}]           |
+| [{2, Bar}, {2, Car}] |
++----------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_INTERSECT(ARRAY[ROW(x, y)], ARRAY[ROW(z, w)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------+
+| R          |
++------------+
+| [{1, Foo}] |
+| []         |
++------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_EXCEPT(ARRAY[ROW(x, y)], ARRAY[ROW(z, w)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------+
+| R          |
++------------+
+| []         |
+| [{2, Bar}] |
++------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAYS_OVERLAP(ARRAY[ROW(x, y)], ARRAY[ROW(z, w)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+# Nested ROW values.
+
+SELECT ARRAY_CONTAINS(ARRAY[ROW(ROW(x, y), 10)], ROW(ROW(z, w), 10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_POSITION(ARRAY[ROW(ROW(x, y), 10)], ROW(ROW(z, w), 10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++---+
+| R |
++---+
+| 1 |
+| 0 |
++---+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_REMOVE(ARRAY[ROW(ROW(x, y), 10), ROW(ROW(z, w), 10)], ROW(ROW(z, 
w), 10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------------+
+| R                |
++------------------+
+| []               |
+| [{{2, Bar}, 10}] |
++------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_DISTINCT(ARRAY[ROW(ROW(x, y), 10), ROW(ROW(z, w), 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----------------------------------+
+| R                                |
++----------------------------------+
+| [{{1, Foo}, 10}]                 |
+| [{{2, Bar}, 10}, {{2, Car}, 10}] |
++----------------------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_UNION(ARRAY[ROW(ROW(x, y), 10)], ARRAY[ROW(ROW(z, w), 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----------------------------------+
+| R                                |
++----------------------------------+
+| [{{1, Foo}, 10}]                 |
+| [{{2, Bar}, 10}, {{2, Car}, 10}] |
++----------------------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_INTERSECT(ARRAY[ROW(ROW(x, y), 10)], ARRAY[ROW(ROW(z, w), 10)]) 
AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------------+
+| R                |
++------------------+
+| [{{1, Foo}, 10}] |
+| []               |
++------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_EXCEPT(ARRAY[ROW(ROW(x, y), 10)], ARRAY[ROW(ROW(z, w), 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------------------+
+| R                |
++------------------+
+| []               |
+| [{{2, Bar}, 10}] |
++------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAYS_OVERLAP(ARRAY[ROW(ROW(x, y), 10)], ARRAY[ROW(ROW(z, w), 10)]) AS 
r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+# ROW values containing arrays of ROW values.
+
+SELECT ARRAY_CONTAINS(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], ROW(ARRAY[ROW(z, w)], 
10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_POSITION(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], ROW(ARRAY[ROW(z, w)], 
10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++---+
+| R |
++---+
+| 1 |
+| 0 |
++---+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_REMOVE(ARRAY[ROW(ARRAY[ROW(x, y)], 10), ROW(ARRAY[ROW(z, w)], 
10)], ROW(ARRAY[ROW(z, w)], 10)) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++--------------------+
+| R                  |
++--------------------+
+| []                 |
+| [{[{2, Bar}], 10}] |
++--------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_DISTINCT(ARRAY[ROW(ARRAY[ROW(x, y)], 10), ROW(ARRAY[ROW(z, w)], 
10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++--------------------------------------+
+| R                                    |
++--------------------------------------+
+| [{[{1, Foo}], 10}]                   |
+| [{[{2, Bar}], 10}, {[{2, Car}], 10}] |
++--------------------------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_UNION(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], ARRAY[ROW(ARRAY[ROW(z, 
w)], 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++--------------------------------------+
+| R                                    |
++--------------------------------------+
+| [{[{1, Foo}], 10}]                   |
+| [{[{2, Bar}], 10}, {[{2, Car}], 10}] |
++--------------------------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_INTERSECT(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], 
ARRAY[ROW(ARRAY[ROW(z, w)], 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++--------------------+
+| R                  |
++--------------------+
+| [{[{1, Foo}], 10}] |
+| []                 |
++--------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_EXCEPT(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], ARRAY[ROW(ARRAY[ROW(z, 
w)], 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++--------------------+
+| R                  |
++--------------------+
+| []                 |
+| [{[{2, Bar}], 10}] |
++--------------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAYS_OVERLAP(ARRAY[ROW(ARRAY[ROW(x, y)], 10)], ARRAY[ROW(ARRAY[ROW(z, 
w)], 10)]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------+
+| R     |
++-------+
+| true  |
+| false |
++-------+
+(2 rows)
+
+!ok
+
+# ROW values with NULL fields.
+
+SELECT ARRAY_CONTAINS(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ROW(z, CAST(NULL 
AS INTEGER))) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------+
+| R    |
++------+
+| true |
+| true |
++------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_POSITION(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ROW(z, CAST(NULL 
AS INTEGER))) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++---+
+| R |
++---+
+| 1 |
+| 1 |
++---+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_REMOVE(ARRAY[ROW(x, CAST(NULL AS INTEGER)), ROW(z, CAST(NULL AS 
INTEGER))], ROW(z, CAST(NULL AS INTEGER))) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----+
+| R  |
++----+
+| [] |
+| [] |
++----+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_DISTINCT(ARRAY[ROW(x, CAST(NULL AS INTEGER)), ROW(z, CAST(NULL AS 
INTEGER))]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------------+
+| R           |
++-------------+
+| [{1, null}] |
+| [{2, null}] |
++-------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_UNION(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ARRAY[ROW(z, 
CAST(NULL AS INTEGER))]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------------+
+| R           |
++-------------+
+| [{1, null}] |
+| [{2, null}] |
++-------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_INTERSECT(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ARRAY[ROW(z, 
CAST(NULL AS INTEGER))]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++-------------+
+| R           |
++-------------+
+| [{1, null}] |
+| [{2, null}] |
++-------------+
+(2 rows)
+
+!ok
+
+SELECT ARRAY_EXCEPT(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ARRAY[ROW(z, 
CAST(NULL AS INTEGER))]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++----+
+| R  |
++----+
+| [] |
+| [] |
++----+
+(2 rows)
+
+!ok
+
+SELECT ARRAYS_OVERLAP(ARRAY[ROW(x, CAST(NULL AS INTEGER))], ARRAY[ROW(z, 
CAST(NULL AS INTEGER))]) AS r
+FROM (VALUES (1, 'Foo', 1, 'Foo'), (2, 'Bar', 2, 'Car')) AS t(x, y, z, w);
++------+
+| R    |
++------+
+| true |
+| true |
++------+
+(2 rows)
+
+!ok
+
 # End row-equality.iq
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 686045af70..fe68dfd1a6 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
@@ -527,115 +527,116 @@ private static class DeepEqualityComparer
     @Override public int hashCode(@Nullable Object t) {
       return deepHashCode(t);
     }
+  }
 
-    private static boolean deepEquals(@Nullable Object v1, @Nullable Object 
v2) {
-      if (v1 == v2) {
-        return true;
-      }
-      if (v1 == null || v2 == null) {
-        return false;
-      }
-      // Normalize both to List: an ARRAY of ROW is a List of Object[],
-      // and each element is normalized in turn.
-      final @Nullable List<?> list1 = asListOrNull(v1);
-      final @Nullable List<?> list2 = asListOrNull(v2);
-      if (list1 != null && list2 != null) {
-        final int n = list1.size();
-        if (n != list2.size()) {
-          return false;
-        }
-        for (int i = 0; i < n; i++) {
-          if (!deepEquals(list1.get(i), list2.get(i))) {
-            return false;
-          }
-        }
-        return true;
-      }
-      if (list1 != null || list2 != null) {
-        return false;
-      }
-      if (v1 instanceof Map && v2 instanceof Map) {
-        return mapDeepEquals((Map<?, ?>) v1, (Map<?, ?>) v2);
-      }
-      if (v1.getClass().isArray() && v2.getClass().isArray()) {
-        // Primitive arrays (e.g. byte[] for BINARY values).
-        return Arrays.deepEquals(new Object[] {v1}, new Object[] {v2});
-      }
-      return v1.equals(v2);
+  /** Compares values recursively; see {@link #deepComparer()}. */
+  public static boolean deepEquals(@Nullable Object v1, @Nullable Object v2) {
+    if (v1 == v2) {
+      return true;
     }
-
-    /** Compares two maps as unordered sets of entries, comparing keys and
-     * values deeply.
-     *
-     * <p>Java {@link Map#equals} is already order-independent, but it looks a
-     * key up by that key's own hashCode and equals, which matches a struct key
-     * only by reference; hence the scan. */
-    private static boolean mapDeepEquals(Map<?, ?> m1, Map<?, ?> m2) {
-      if (m1.size() != m2.size()) {
+    if (v1 == null || v2 == null) {
+      return false;
+    }
+    // Normalize both to List: an ARRAY of ROW is a List of Object[],
+    // and each element is normalized in turn.
+    final @Nullable List<?> list1 = asListOrNull(v1);
+    final @Nullable List<?> list2 = asListOrNull(v2);
+    if (list1 != null && list2 != null) {
+      final int n = list1.size();
+      if (n != list2.size()) {
         return false;
       }
-      // Remove on match, so that keys that are deep-equal but distinct to
-      // Java, as two Object[] with the same contents are, pair up one to one.
-      final List<Map.Entry<?, ?>> unmatched = new ArrayList<>(m2.entrySet());
-      for (Map.Entry<?, ?> e1 : m1.entrySet()) {
-        boolean found = false;
-        for (int i = 0; i < unmatched.size(); i++) {
-          final Map.Entry<?, ?> e2 = unmatched.get(i);
-          if (deepEquals(e1.getKey(), e2.getKey())
-              && deepEquals(e1.getValue(), e2.getValue())) {
-            unmatched.remove(i);
-            found = true;
-            break;
-          }
-        }
-        if (!found) {
+      for (int i = 0; i < n; i++) {
+        if (!deepEquals(list1.get(i), list2.get(i))) {
           return false;
         }
       }
       return true;
     }
+    if (list1 != null || list2 != null) {
+      return false;
+    }
+    if (v1 instanceof Map && v2 instanceof Map) {
+      return mapDeepEquals((Map<?, ?>) v1, (Map<?, ?>) v2);
+    }
+    if (v1.getClass().isArray() && v2.getClass().isArray()) {
+      // Primitive arrays (e.g. byte[] for BINARY values).
+      return Arrays.deepEquals(new Object[] {v1}, new Object[] {v2});
+    }
+    return v1.equals(v2);
+  }
 
-    /** Computes a hash code that is equal for values that
-     * {@link #deepEquals} considers equal; in particular, an
-     * {@code Object[]} and a {@link List} with equal elements hash alike. */
-    private static int deepHashCode(@Nullable Object o) {
-      if (o == null) {
-        return 0x789d;
-      }
-      final @Nullable List<?> list = asListOrNull(o);
-      if (list != null) {
-        int h = 1;
-        for (Object element : list) {
-          h = 31 * h + deepHashCode(element);
-        }
-        return h;
-      }
-      if (o instanceof Map) {
-        // Sum of per-entry hashes, as Map.hashCode does, so that the hash
-        // ignores entry order just as mapDeepEquals does.
-        int h = 0;
-        for (Map.Entry<?, ?> e : ((Map<?, ?>) o).entrySet()) {
-          h += deepHashCode(e.getKey()) ^ deepHashCode(e.getValue());
+  /** Compares two maps as unordered sets of entries, comparing keys and
+   * values deeply.
+   *
+   * <p>Java {@link Map#equals} is already order-independent, but it looks a
+   * key up by that key's own hashCode and equals, which matches a struct key
+   * only by reference; hence the scan. */
+  private static boolean mapDeepEquals(Map<?, ?> m1, Map<?, ?> m2) {
+    if (m1.size() != m2.size()) {
+      return false;
+    }
+    // Remove on match, so that keys that are deep-equal but distinct to
+    // Java, as two Object[] with the same contents are, pair up one to one.
+    final List<Map.Entry<?, ?>> unmatched = new ArrayList<>(m2.entrySet());
+    for (Map.Entry<?, ?> e1 : m1.entrySet()) {
+      boolean found = false;
+      for (int i = 0; i < unmatched.size(); i++) {
+        final Map.Entry<?, ?> e2 = unmatched.get(i);
+        if (deepEquals(e1.getKey(), e2.getKey())
+            && deepEquals(e1.getValue(), e2.getValue())) {
+          unmatched.remove(i);
+          found = true;
+          break;
         }
-        return h;
       }
-      if (o.getClass().isArray()) {
-        return Arrays.deepHashCode(new Object[] {o});
+      if (!found) {
+        return false;
       }
-      return o.hashCode();
     }
+    return true;
+  }
 
-    /** Views {@code o} as a list if it is a {@code List} or an
-     * {@code Object[]}; returns null otherwise. */
-    private static @Nullable List<?> asListOrNull(Object o) {
-      if (o instanceof List) {
-        return (List<?>) o;
+  /** Computes a hash code that is equal for values that
+   * {@link #deepEquals} considers equal; in particular, an
+   * {@code Object[]} and a {@link List} with equal elements hash alike. */
+  public static int deepHashCode(@Nullable Object o) {
+    if (o == null) {
+      return 0x789d;
+    }
+    final @Nullable List<?> list = asListOrNull(o);
+    if (list != null) {
+      int h = 1;
+      for (Object element : list) {
+        h = 31 * h + deepHashCode(element);
       }
-      if (o instanceof Object[]) {
-        return Arrays.asList((Object[]) o);
+      return h;
+    }
+    if (o instanceof Map) {
+      // Sum of per-entry hashes, as Map.hashCode does, so that the hash
+      // ignores entry order just as mapDeepEquals does.
+      int h = 0;
+      for (Map.Entry<?, ?> e : ((Map<?, ?>) o).entrySet()) {
+        h += deepHashCode(e.getKey()) ^ deepHashCode(e.getValue());
       }
-      return null;
+      return h;
+    }
+    if (o.getClass().isArray()) {
+      return Arrays.deepHashCode(new Object[] {o});
+    }
+    return o.hashCode();
+  }
+
+  /** Views {@code o} as a list if it is a {@code List} or an
+   * {@code Object[]}; returns null otherwise. */
+  private static @Nullable List<?> asListOrNull(Object o) {
+    if (o instanceof List) {
+      return (List<?>) o;
+    }
+    if (o instanceof Object[]) {
+      return Arrays.asList((Object[]) o);
     }
+    return null;
   }
 
   /** Identity equality comparer. */

Reply via email to