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 4c58bdb0e6 [CALCITE-7658] Type checker rejects CAST(ARRAY() AS ROW(x 
INT) ARRAY)
4c58bdb0e6 is described below

commit 4c58bdb0e6d1a1495e51dce5035b03811e5bfca3
Author: Mihai Budiu <[email protected]>
AuthorDate: Wed Jul 15 15:54:25 2026 -0700

    [CALCITE-7658] Type checker rejects CAST(ARRAY() AS ROW(x INT) ARRAY)
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../adapter/enumerable/RexToLixTranslator.java     | 16 ++++
 .../apache/calcite/sql/fun/SqlCastFunction.java    |  2 +-
 .../calcite/sql/type/SqlTypeFactoryImpl.java       | 31 ++++++-
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   | 15 ++--
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java | 14 ++--
 .../calcite/sql/type/SqlTypeFactoryTest.java       | 36 ++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 10 +--
 .../org/apache/calcite/test/SqlOperatorTest.java   | 95 +++++++++++++++++++++-
 8 files changed, 195 insertions(+), 24 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index 5a1e0fee20..a39146ac75 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -341,6 +341,16 @@ Expression translateCast(
     return expressionHandlingSafe(convert3, safe, targetType);
   }
 
+  /** Returns whether every runtime value of {@code type} is null.
+   * This holds for the NULL type, which describes untyped NULL literals,
+   * and for the UNKNOWN type, e.g. the element type inferred for the
+   * no-argument array constructor ARRAY().  Such values are never
+   * created, but the code generated needs to typecheck. */
+  private static boolean valueIsAlwaysNull(RelDataType type) {
+    SqlTypeName typeName = type.getSqlTypeName();
+    return typeName == SqlTypeName.UNKNOWN || typeName == SqlTypeName.NULL;
+  }
+
   private Expression getConvertExpression(
       RelDataType sourceType,
       RelDataType targetType,
@@ -366,6 +376,9 @@ private Expression getConvertExpression(
     }
 
     if (targetType.getSqlTypeName() == SqlTypeName.ROW) {
+      if (valueIsAlwaysNull(sourceType)) {
+        return Expressions.constant(null);
+      }
       assert sourceType.getSqlTypeName() == SqlTypeName.ROW;
       List<RelDataTypeField> targetTypes = targetType.getFieldList();
       List<RelDataTypeField> sourceTypes = sourceType.getFieldList();
@@ -397,6 +410,9 @@ private Expression getConvertExpression(
     switch (targetType.getSqlTypeName()) {
     case ARRAY:
     case MULTISET:
+      if (valueIsAlwaysNull(sourceType)) {
+        return Expressions.constant(null);
+      }
       final RelDataType sourceDataType = sourceType.getComponentType();
       final RelDataType targetDataType = targetType.getComponentType();
       assert sourceDataType != null;
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
index b25a9b8fdd..62ce488718 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastFunction.java
@@ -200,7 +200,7 @@ private static RelDataType 
createTypeWithNullabilityFromExpr(RelDataTypeFactory
       RelDataType valueType =
           createTypeWithNullabilityFromExpr(
               typeFactory, expressionValueType, targetValueType, safe);
-      SqlTypeUtil.createMapType(typeFactory, keyType, valueType, isNullable);
+      return SqlTypeUtil.createMapType(typeFactory, keyType, valueType, 
isNullable);
     }
 
     return typeFactory.createTypeWithNullability(targetType, isNullable);
diff --git 
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
index d0ccab7dfd..544f9ca708 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
@@ -28,6 +28,7 @@
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.nio.charset.Charset;
+import java.util.ArrayList;
 import java.util.List;
 
 import static com.google.common.base.Preconditions.checkArgument;
@@ -191,11 +192,33 @@ public SqlTypeFactoryImpl(RelDataTypeSystem typeSystem) {
 
     RelDataType type0 = types.get(0);
     if (type0.getSqlTypeName() != null) {
-      RelDataType resultType = leastRestrictiveSqlType(types);
-      if (resultType != null) {
-        return resultType;
+      // First preprocess to filter out UNKNOWN types.
+      // leastRestrictive() can be thought as a form of type unification,
+      // and UNKNOWN behaves like an unbound type variable: it unifies with 
any type
+      // without constraining the result.
+      // Note that UNKNOWN can be nullable, so this information is carried 
over to the result.
+      List<RelDataType> knownTypes = new ArrayList<>(types.size());
+      // True if any UNKNOWN type is nullable
+      boolean anyUnknownIsNullable = false;
+      for (RelDataType type : types) {
+        if (type.getSqlTypeName() == SqlTypeName.UNKNOWN) {
+          anyUnknownIsNullable |= type.isNullable();
+        } else {
+          knownTypes.add(type);
+        }
       }
-      return leastRestrictiveByCast(types, mappingRule);
+      if (knownTypes.isEmpty()) {
+        // All types are unknown
+        return createTypeWithNullability(createUnknownType(), 
anyUnknownIsNullable);
+      }
+      RelDataType resultType = leastRestrictiveSqlType(knownTypes);
+      if (resultType == null) {
+        resultType = leastRestrictiveByCast(knownTypes, mappingRule);
+      }
+      if (resultType != null && anyUnknownIsNullable) {
+        resultType = createTypeWithNullability(resultType, true);
+      }
+      return resultType;
     }
 
     return super.leastRestrictive(types, mappingRule);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
index 0298866828..5de00a50b4 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
@@ -1128,8 +1128,9 @@ public static boolean canCastFrom(
             requireNonNull(fromType.getComponentType(), "componentType"),
             typeMappingRule);
       } else if (fromType.getFamily() == SqlTypeFamily.CHARACTER
-          || fromType.getSqlTypeName() == SqlTypeName.NULL) {
-        // Cast from NULL or string to array is legal
+          || fromType.getSqlTypeName() == SqlTypeName.NULL
+          || fromType.getSqlTypeName() == SqlTypeName.UNKNOWN) {
+        // Cast from NULL, UNKNOWN, or string to array is legal
         return true;
       }
       return false;
@@ -1145,8 +1146,9 @@ && canCastFrom(
             requireNonNull(fromType.getValueType(), "valueType"),
             typeMappingRule);
       } else if (fromType.getFamily() == SqlTypeFamily.CHARACTER
-          || fromType.getSqlTypeName() == SqlTypeName.NULL) {
-        // Cast from NULL or string to map is legal
+          || fromType.getSqlTypeName() == SqlTypeName.NULL
+          || fromType.getSqlTypeName() == SqlTypeName.UNKNOWN) {
+        // Cast from NULL, UNKNOWN, or string to map is legal
         return true;
       }
       return false;
@@ -1164,7 +1166,10 @@ && canCastFrom(
             toType, fromType.getFieldList().get(0).getType(), typeMappingRule);
       } else if (toTypeName == SqlTypeName.ROW) {
         if (fromTypeName != SqlTypeName.ROW) {
-          return fromTypeName == SqlTypeName.NULL;
+          // UNKNOWN can arise e.g. as the element type inferred for the
+          // no-argument array constructor ARRAY()
+          return fromTypeName == SqlTypeName.NULL
+              || fromTypeName == SqlTypeName.UNKNOWN;
         }
         int n = toType.getFieldCount();
         if (fromType.getFieldCount() != n) {
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 2ac820e838..b25c213def 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -3032,25 +3032,25 @@ private SqlDialect nonOrdinalDialect() {
         + " as MAP<varchar,varchar> array)";
     final String expectedClickHouse2 =
         "SELECT CAST(array(map('a', '1'), map('b', '2'), map('c', '3'))"
-            + " AS Array(Map(`String`, `Nullable(String)`)))";
+            + " AS Array(Map(`String`, `String`)))";
     sql(query2).withClickHouse().ok(expectedClickHouse2);
 
     final String query3 = "select cast(MAP['a',ARRAY[1,2,3]]"
         + " as MAP<varchar,integer array>)";
     final String expectedClickHouse3 =
-        "SELECT CAST(map('a', array(1, 2, 3)) AS Map(`String`, 
Array(`Nullable(Int32)`)))";
+        "SELECT CAST(map('a', array(1, 2, 3)) AS Map(`String`, 
Array(`Int32`)))";
     sql(query3).withClickHouse().ok(expectedClickHouse3);
 
     final String query4 = "select cast(MAP['a',ARRAY[1.0,2.0,3.0]]"
         + " as MAP<varchar,real array>)";
     final String expectedClickHouse4 =
-        "SELECT CAST(map('a', array(1.0, 2.0, 3.0)) AS Map(`String`, 
Array(`Nullable(Float32)`)))";
+        "SELECT CAST(map('a', array(1.0, 2.0, 3.0)) AS Map(`String`, 
Array(`Float32`)))";
     sql(query4).withClickHouse().ok(expectedClickHouse4);
 
     final String query5 = "select cast(MAP['a',MAP['b','c']]"
         + " as MAP<varchar,MAP<varchar,varchar>>)";
     final String expectedClickHouse5 =
-        "SELECT CAST(map('a', map('b', 'c')) AS Map(`String`, Map(`String`, 
`Nullable(String)`)))";
+        "SELECT CAST(map('a', map('b', 'c')) AS Map(`String`, Map(`String`, 
`String`)))";
     sql(query5).withClickHouse().ok(expectedClickHouse5);
   }
 
@@ -5652,15 +5652,15 @@ private SqlDialect nonOrdinalDialect() {
   @Test void testCastAsMapType() {
     sql("SELECT CAST(MAP['A', 1.0] AS MAP<VARCHAR, DOUBLE>)")
         .ok("SELECT CAST(MAP['A', 1.0] AS "
-            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", DOUBLE NULL >)\n"
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", DOUBLE >)\n"
             + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")");
     sql("SELECT CAST(MAP['A', ARRAY[1, 2, 3]] AS MAP<VARCHAR, INT ARRAY>)")
         .ok("SELECT CAST(MAP['A', ARRAY[1, 2, 3]] AS "
-            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", INTEGER ARRAY NULL 
>)\n"
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", INTEGER ARRAY >)\n"
             + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")");
     sql("SELECT CAST(MAP[ARRAY['A'], MAP[1, 2]] AS MAP<VARCHAR ARRAY, MAP<INT, 
INT>>)")
         .ok("SELECT CAST(MAP[ARRAY['A'], MAP[1, 2]] AS "
-            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\" ARRAY, MAP< INTEGER, 
INTEGER NULL > NULL >)\n"
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\" ARRAY, MAP< INTEGER, 
INTEGER > >)\n"
             + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")");
   }
 
diff --git 
a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java 
b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
index f0ff190d28..68a34eb5ba 100644
--- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeFactoryTest.java
@@ -87,6 +87,42 @@ class SqlTypeFactoryTest {
     assertThat(leastRestrictive.isNullable(), is(true));
   }
 
+  /** UNKNOWN types in leastRestrictive() affect only the result nullability. 
*/
+  @Test void testLeastRestrictiveWithUnknown() {
+    SqlTypeFixture f = new SqlTypeFixture();
+    // UNKNOWN never constrains the result, no matter its position
+    checkUnknownWithType(f, f.sqlBigInt);
+    checkUnknownWithType(f, f.structOfInt);
+    checkUnknownWithType(f, f.arrayBigInt);
+    checkUnknownWithType(f, f.mapOfInt);
+    // A nullable UNKNOWN makes the result nullable
+    RelDataType leastRestrictive =
+        f.typeFactory.leastRestrictive(
+            Lists.newArrayList(f.structOfInt,
+                f.typeFactory.createTypeWithNullability(f.sqlUnknown, true)));
+    assertThat(leastRestrictive, notNullValue());
+    assertThat(leastRestrictive.getSqlTypeName(), is(SqlTypeName.ROW));
+    assertThat(leastRestrictive.isNullable(), is(true));
+    // A list of UNKNOWN unifies to UNKNOWN
+    leastRestrictive =
+        f.typeFactory.leastRestrictive(
+            Lists.newArrayList(f.sqlUnknown, f.sqlUnknown));
+    assertThat(leastRestrictive.getSqlTypeName(), is(SqlTypeName.UNKNOWN));
+  }
+
+  /** Checks that leastRestictive({@code type}, UNKNOWN) yields {@code type},
+   * in either order. */
+  private void checkUnknownWithType(SqlTypeFixture f, RelDataType type) {
+    RelDataType r1 =
+        f.typeFactory.leastRestrictive(Lists.newArrayList(type, f.sqlUnknown));
+    RelDataType r2 =
+        f.typeFactory.leastRestrictive(Lists.newArrayList(f.sqlUnknown, type));
+    assertThat(r1, notNullValue());
+    assertThat(r2, notNullValue());
+    assertThat(r1.getFullTypeString(), is(type.getFullTypeString()));
+    assertThat(r2.getFullTypeString(), is(type.getFullTypeString()));
+  }
+
   @Test void testLeastRestrictiveStructWithNull() {
     SqlTypeFixture f = new SqlTypeFixture();
     RelDataType leastRestrictive =
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index cecb57385e..5e9224364e 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -1527,7 +1527,7 @@ void testLikeAndSimilarFails() {
     expr("cast(ARRAY[1,2,3] AS VARIANT ARRAY)")
         .columnType("VARIANT NOT NULL ARRAY NOT NULL");
     expr("cast(MAP['a','b','c','d'] AS MAP<VARCHAR, VARIANT>)")
-        .columnType("(VARCHAR NOT NULL, VARIANT) MAP NOT NULL");
+        .columnType("(VARCHAR NOT NULL, VARIANT NOT NULL) MAP NOT NULL");
     // Test case for [CALCITE-7293] 
https://issues.apache.org/jira/browse/CALCITE-7293
     // MAP constructor cannot handle VARIANT values that need casts
     expr("MAP['a', CAST('x' AS VARIANT), 'b', CAST(NULL AS VARIANT)]")
@@ -9640,16 +9640,16 @@ void testGroupExpressionEquivalenceParams() {
   @Test void testCastMapType() {
     sql("select cast(\"int2IntMapType\" as map<int,int>) from 
COMPLEXTYPES.CTC_T1")
         .withExtendedCatalog()
-        .columnType("(INTEGER NOT NULL, INTEGER) MAP NOT NULL");
+        .columnType("(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
     sql("select cast(\"int2varcharArrayMapType\" as map<int,varchar array>) "
         + "from COMPLEXTYPES.CTC_T1")
         .withExtendedCatalog()
-        .columnType("(INTEGER NOT NULL, VARCHAR ARRAY) MAP NOT NULL");
+        .columnType("(INTEGER NOT NULL, VARCHAR NOT NULL ARRAY NOT NULL) MAP 
NOT NULL");
     sql("select cast(\"varcharMultiset2IntIntMapType\" as map<varchar(5) 
multiset, map<int, int>>)"
         + " from COMPLEXTYPES.CTC_T1")
         .withExtendedCatalog()
-        .columnType("(VARCHAR(5) MULTISET NOT NULL, "
-            + "(INTEGER NOT NULL, INTEGER) MAP) MAP NOT NULL");
+        .columnType("(VARCHAR(5) NOT NULL MULTISET NOT NULL, "
+            + "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL) MAP NOT 
NULL");
   }
 
   @Test void testCastAsRowType() {
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 d710b516aa..f82222312e 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -1843,6 +1843,39 @@ void testCastToBoolean(CastType castType, 
SqlOperatorFixture f) {
     f.checkNull("cast(null as row(f0 varchar, f1 varchar))");
   }
 
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-7658";>
+   * [CALCITE-7658] Type checker rejects
+   * CAST(ARRAY() AS ROW(x INT) ARRAY)</a>.
+   *
+   * <p>The Spark {@code ARRAY()} function creates an empty array whose
+   * element type is UNKNOWN; such an array can be cast to any array type. */
+  @Test void testCastEmptyArray() {
+    final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.SPARK);
+    f.checkScalar("cast(array() as integer array)", "[]",
+        "INTEGER NOT NULL ARRAY NOT NULL");
+    f.checkScalar("cast(array() as row(x int) array)", "[]",
+        "RecordType(INTEGER NOT NULL X) NOT NULL ARRAY NOT NULL");
+    f.checkScalar("cast(array() as integer array array)", "[]",
+        "INTEGER ARRAY NOT NULL ARRAY NOT NULL");
+    f.checkScalar("cast(array() as map<varchar, integer> array)", "[]",
+        "(VARCHAR NOT NULL, INTEGER) MAP NOT NULL ARRAY NOT NULL");
+    // A non-empty array with UNKNOWN or NULL element type contains only nulls
+    f.checkScalar("cast(array_append(array(), null) as row(x int) array)",
+        "[null]",
+        "RecordType(INTEGER NOT NULL X) ARRAY NOT NULL");
+    f.checkScalar("cast(array(null) as row(x int) array)",
+        "[null]",
+        "RecordType(INTEGER NOT NULL X) ARRAY NOT NULL");
+    // The empty MAP() has UNKNOWN key and value types
+    f.checkScalar("cast(map() as map<varchar, integer>)", "{}",
+        "(VARCHAR NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
+    f.checkScalar("cast(map() as map<varchar, row(x int)>)", "{}",
+        "(VARCHAR NOT NULL, RecordType(INTEGER X) NOT NULL) MAP NOT NULL");
+    f.checkScalar("cast(map() as map<varchar, integer array>)", "{}",
+        "(VARCHAR NOT NULL, INTEGER ARRAY NOT NULL) MAP NOT NULL");
+  }
+
   /** Test cases for
    * <a href="https://issues.apache.org/jira/browse/CALCITE-4918";>
    * [CALCITE-4918] Add a VARIANT data type</a>. */
@@ -8229,7 +8262,7 @@ void checkRegexpExtract(SqlOperatorFixture f0, 
FunctionAlias functionAlias) {
     f.checkScalar("array_append(array(null), null)", "[null, null]",
         "NULL ARRAY NOT NULL");
     f.checkScalar("array_append(array(), null)", "[null]",
-        "UNKNOWN ARRAY NOT NULL");
+        "NULL ARRAY NOT NULL");
     f.checkScalar("array_append(array(), 1)", "[1]",
         "INTEGER NOT NULL ARRAY NOT NULL");
     f.checkScalar("array_append(array[array[1, 2]], array[3, 4])", "[[1, 2], 
[3, 4]]",
@@ -8568,7 +8601,7 @@ void checkRegexpExtract(SqlOperatorFixture f0, 
FunctionAlias functionAlias) {
     f.checkScalar("array_prepend(array(null), null)", "[null, null]",
         "NULL ARRAY NOT NULL");
     f.checkScalar("array_prepend(array(), null)", "[null]",
-        "UNKNOWN ARRAY NOT NULL");
+        "NULL ARRAY NOT NULL");
     f.checkScalar("array_prepend(array(), 1)", "[1]",
         "INTEGER NOT NULL ARRAY NOT NULL");
     f.checkScalar("array_prepend(array[array[1, 2]], array[3, 4])", "[[3, 4], 
[1, 2]]",
@@ -13606,6 +13639,22 @@ private static void 
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
           "RecordType(INTEGER EXPR$0, INTEGER EXPR$1) NOT NULL ARRAY NOT 
NULL");
       f2.checkScalar("array(row(1, 2), row(3, 4))", "[{1, 2}, {3, 4}]",
           "RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT 
NULL ARRAY NOT NULL");
+      // Tests for unification of UNKNOWN with other types; array() has a type
+      // of UNKNOWN ARRAY, yet the type of ARRAY() is inferred from other 
operands.
+      f2.checkScalar("array(array(1), array())", "[[1], []]",
+          "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
+      f2.checkScalar("array(array(), array(1))", "[[], [1]]",
+          "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
+      f2.checkScalar("array(array(row(1, 2)), array())", "[[{1, 2}], []]",
+          "RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT 
NULL "
+              + "ARRAY NOT NULL ARRAY NOT NULL");
+      f2.checkScalar("array(array(), array(row(1, 2)))", "[[], [{1, 2}]]",
+          "RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT NULL EXPR$1) NOT 
NULL "
+              + "ARRAY NOT NULL ARRAY NOT NULL");
+      f2.checkScalar("array(array(array(1)), array())", "[[[1]], []]",
+          "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
+      f2.checkScalar("array(array(), array(array(1)))", "[[], [[1]]]",
+          "INTEGER NOT NULL ARRAY NOT NULL ARRAY NOT NULL ARRAY NOT NULL");
       // checkFails
       f2.checkFails("^array(row(1), row(2, 3))^",
           "Parameters must be of the same type", false);
@@ -13624,6 +13673,32 @@ private static void 
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
     f.forEachLibrary(libraries, consumer);
   }
 
+  /** Tests that empty collections created by the Spark
+   * {@code ARRAY()} and {@code MAP()} functions, whose element
+   * types are UNKNOWN, unify with collections with known types. */
+  @Test void testEmptyCollections() {
+    final SqlOperatorFixture f = fixture().withLibrary(SqlLibrary.SPARK);
+    f.checkScalar("array(map(1, 2), map())", "[{1=2}, {}]",
+        "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL ARRAY NOT NULL");
+    f.checkScalar("array(map(), map(1, 2))", "[{}, {1=2}]",
+        "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL ARRAY NOT NULL");
+    // Nested: empty collections inside a ROW unify field by field
+    f.checkScalar("array(row(array(), map()))", "[{[], {}}]",
+        "RecordType(UNKNOWN NOT NULL ARRAY NOT NULL EXPR$0, "
+            + "(UNKNOWN NOT NULL, UNKNOWN NOT NULL) MAP NOT NULL EXPR$1) "
+            + "NOT NULL ARRAY NOT NULL");
+    f.checkScalar("array(row(array(1), map(1, 2)), row(array(), map()))",
+        "[{[1], {1=2}}, {[], {}}]",
+        "RecordType(INTEGER NOT NULL ARRAY NOT NULL EXPR$0, "
+            + "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL EXPR$1) "
+            + "NOT NULL ARRAY NOT NULL");
+    f.checkScalar("array(row(array(), map()), row(array(1), map(1, 2)))",
+        "[{[], {}}, {[1], {1=2}}]",
+        "RecordType(INTEGER NOT NULL ARRAY NOT NULL EXPR$0, "
+            + "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL EXPR$1) "
+            + "NOT NULL ARRAY NOT NULL");
+  }
+
   @Test void testArrayQueryConstructor() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.ARRAY_QUERY, 
SqlOperatorFixture.VmName.EXPAND);
@@ -13930,6 +14005,22 @@ private static void 
checkArrayConcatAggFuncFails(SqlOperatorFixture t) {
     f1.checkScalar("map('k1', 1, 'k2', 2.0)",
         "{k1=1.0, k2=2.0}",
         "(CHAR(2) NOT NULL, DECIMAL(11, 1) NOT NULL) MAP NOT NULL");
+    f1.checkScalar("map('a', array(1), 'b', array())", "{a=[1], b=[]}",
+        "(CHAR(1) NOT NULL, INTEGER NOT NULL ARRAY NOT NULL) MAP NOT NULL");
+    f1.checkScalar("map('a', array(), 'b', array(1))", "{a=[], b=[1]}",
+        "(CHAR(1) NOT NULL, INTEGER NOT NULL ARRAY NOT NULL) MAP NOT NULL");
+    f1.checkScalar("map('a', map(1, 2), 'b', map())", "{a={1=2}, b={}}",
+        "(CHAR(1) NOT NULL, (INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL) 
MAP NOT NULL");
+    f1.checkScalar("map('a', map(), 'b', map(1, 2))", "{a={}, b={1=2}}",
+        "(CHAR(1) NOT NULL, (INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL) 
MAP NOT NULL");
+    // Avatica's conversion of MAP to STRING is broken, so we only check
+    // the type for the following 2 tests
+    f1.checkType("map('a', array(row(1, 2)), 'b', array())",
+        "(CHAR(1) NOT NULL, RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT 
NULL EXPR$1) "
+            + "NOT NULL ARRAY NOT NULL) MAP NOT NULL");
+    f1.checkType("map('a', array(), 'b', array(row(1, 2)))",
+        "(CHAR(1) NOT NULL, RecordType(INTEGER NOT NULL EXPR$0, INTEGER NOT 
NULL EXPR$1) "
+            + "NOT NULL ARRAY NOT NULL) MAP NOT NULL");
   }
 
   @Test void testMapQueryConstructor() {

Reply via email to