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. */