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() {