diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java index 171a8da68aa..18da6587d4d 100644 --- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java +++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java @@ -741,7 +741,7 @@ ExInst illegalArgumentForTableFunctionCall(String a0, @BaseMessage("Map requires an even number of arguments") ExInst mapRequiresEvenArgCount(); - @BaseMessage("Function ''{0}'' should all be of type map, but it is ''{1}''") + @BaseMessage("Arguments of function ''{0}'' should all be of type MAP, but ''{1}'' was found") ExInst typesShouldAllBeMap(String funcName, String type); @BaseMessage("Incompatible types") diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java index 2479a92ba24..539d6b327fd 100644 --- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java +++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java @@ -1763,23 +1763,47 @@ private static RelDataType deriveTypeArraysZip(SqlOperatorBinding opBinding) { OperandTypes.ARRAY.or(OperandTypes.ARRAY_BOOLEAN_LITERAL)); private static RelDataType deriveTypeMapConcat(SqlOperatorBinding opBinding) { + final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); if (opBinding.getOperandCount() == 0) { - final RelDataTypeFactory typeFactory = opBinding.getTypeFactory(); - final RelDataType type = typeFactory.createSqlType(SqlTypeName.VARCHAR); - requireNonNull(type, "type"); + final RelDataType type = typeFactory.createSqlType(SqlTypeName.ANY); return SqlTypeUtil.createMapType(typeFactory, type, type, true); - } else { - final List operandTypes = opBinding.collectOperandTypes(); - for (RelDataType operandType : operandTypes) { - if (!SqlTypeUtil.isMap(operandType)) { - throw opBinding.newError( - RESOURCE.typesShouldAllBeMap( - opBinding.getOperator().getName(), - operandType.getFullTypeString())); - } + } + final List operandTypes = opBinding.collectOperandTypes(); + final List mapTypes = new ArrayList<>(); + boolean hasNull = false; + for (RelDataType operandType : operandTypes) { + if (operandType.getSqlTypeName() == SqlTypeName.NULL) { + hasNull = true; + } else if (SqlTypeUtil.isMap(operandType)) { + mapTypes.add(operandType); + } else { + throw opBinding.newError( + RESOURCE.typesShouldAllBeMap( + opBinding.getOperator().getName(), + operandType.getFullTypeString())); + } + } + if (mapTypes.isEmpty() || hasNull) { + // All arguments are NULL literals, or at least one null argument; + // the result is NULL. + return typeFactory.createSqlType(SqlTypeName.NULL); + } + // If there are MAP placeholders (e.g. from map_concat() with no + // arguments) alongside more specific MAP types, ignore the placeholders and + // infer the type from the specific maps. + final List concreteMapTypes = new ArrayList<>(); + for (RelDataType mapType : mapTypes) { + final RelDataType keyType = requireNonNull(mapType.getKeyType()); + final RelDataType valueType = requireNonNull(mapType.getValueType()); + if (keyType.getSqlTypeName() == SqlTypeName.ANY + && valueType.getSqlTypeName() == SqlTypeName.ANY) { + continue; } - return requireNonNull(opBinding.getTypeFactory().leastRestrictive(operandTypes)); + concreteMapTypes.add(mapType); } + final List typesToUse = + concreteMapTypes.isEmpty() ? mapTypes : concreteMapTypes; + return requireNonNull(typeFactory.leastRestrictive(typesToUse)); } /** The "MAP_CONCAT(map [, map]*)" function. */ diff --git a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties index 4096537b7fb..0451d63133a 100644 --- a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties +++ b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties @@ -244,7 +244,7 @@ DuplicateNameInColumnList=Duplicate name ''{0}'' in column list RequireAtLeastOneArg=Require at least 1 argument MapRequiresTwoOrMoreArgs=Map requires at least 2 arguments MapRequiresEvenArgCount=Map requires an even number of arguments -TypesShouldAllBeMap=Function ''{0}'' should all be of type map, but it is ''{1}'' +TypesShouldAllBeMap=Arguments of function ''{0}'' should all be of type MAP, but ''{1}'' was found IncompatibleTypes=Incompatible types ColumnCountMismatch=Number of columns must match number of query columns DuplicateColumnAndNoColumnList=Column has duplicate column name ''{0}'' and no column list specified 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 080a704a8cc..2119f79df20 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -9230,7 +9230,9 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, SqlFunction function, "(INTEGER NOT NULL, INTEGER) MAP NOT NULL"); // test zero arg, but it should return empty map. f.checkScalar("map_concat()", "{}", - "(VARCHAR NOT NULL, VARCHAR NOT NULL) MAP"); + "(ANY NOT NULL, ANY NOT NULL) MAP"); + f.checkScalar("map_concat(map_concat(), map[1, 2])", "{1=2}", + "(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL"); // after calcite supports cast(null as map), it should add these tests. if (TODO) { @@ -9244,16 +9246,18 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, SqlFunction function, // test only has one operand, but it is not map type. f.checkFails("^map_concat(1)^", - "Function 'MAP_CONCAT' should all be of type map, but it is 'INTEGER NOT NULL'", false); - f.checkFails("^map_concat(null)^", - "Function 'MAP_CONCAT' should all be of type map, but it is 'NULL'", false); + "Arguments of function 'MAP_CONCAT' should all be of type MAP, " + + "but 'INTEGER NOT NULL' was found", false); + // test operand is the NULL literal. + f.checkNull("map_concat(null)"); // test operands in same type family, but it is not map type. f.checkFails("^map_concat(array[1], array[1])^", - "Function 'MAP_CONCAT' should all be of type map, " - + "but it is 'INTEGER NOT NULL ARRAY NOT NULL'", false); - f.checkFails("^map_concat(map['foo', 1], null)^", - "Function 'MAP_CONCAT' should all be of type map, " - + "but it is 'NULL'", false); + "Arguments of function 'MAP_CONCAT' should all be of type MAP, " + + "but 'INTEGER NOT NULL ARRAY NOT NULL' was found", false); + // test map operand with NULL literal. + f.checkNull("map_concat(map['foo', 1], null)"); + f.checkType("map_concat(map['foo', 1], null)", "NULL"); + f.checkNull("map_concat(null, map['foo', 1])"); // test operands not in same type family. f.checkFails("^map_concat(map[1, null], array[1])^", "Parameters must be of the same type", false); @@ -9272,6 +9276,9 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, SqlFunction function, "(INTEGER NOT NULL, INTEGER) MAP NOT NULL"); f1.checkScalar("map_concat(map('foo', 1), map())", "{foo=1}", "(CHAR(3) NOT NULL, INTEGER NOT NULL) MAP NOT NULL"); + // test zero arg, but it should return empty map. + f1.checkScalar("map_concat()", "{}", + "(ANY NOT NULL, ANY NOT NULL) MAP"); // test operand is null map f1.checkNull("map_concat(map('foo', 1), cast(null as map))"); @@ -9281,9 +9288,9 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, SqlFunction function, f1.checkType("map_concat(cast(null as map), map['foo', 1])", "(VARCHAR NOT NULL, INTEGER) MAP"); - f1.checkFails("^map_concat(map('foo', 1), null)^", - "Function 'MAP_CONCAT' should all be of type map, " - + "but it is 'NULL'", false); + f1.checkNull("map_concat(map('foo', 1), null)"); + f1.checkType("map_concat(map('foo', 1), null)", "NULL"); + f1.checkNull("map_concat(null, map('foo', 1))"); // test operands not in same type family. f1.checkFails("^map_concat(map(1, null), array[1])^", "Parameters must be of the same type", false);