This is an automated email from the ASF dual-hosted git repository.
xuzifu666 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 dec8196154 [CALCITE-7615] MAP_CONCAT does not accept NULL as an
argument
dec8196154 is described below
commit dec8196154b5b8793eee05c0f3ec3fc67c6f2bbf
Author: Yu Xu <[email protected]>
AuthorDate: Tue Jun 23 18:32:49 2026 +0800
[CALCITE-7615] MAP_CONCAT does not accept NULL as an argument
---
.../apache/calcite/runtime/CalciteResource.java | 2 +-
.../calcite/sql/fun/SqlLibraryOperators.java | 50 ++++++++++++++++------
.../calcite/runtime/CalciteResource.properties | 2 +-
.../org/apache/calcite/test/SqlOperatorTest.java | 31 ++++++++------
4 files changed, 58 insertions(+), 27 deletions(-)
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 ac2ed85019..ea8e1772c6 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<CalciteException>
illegalArgumentForTableFunctionCall(String a0,
@BaseMessage("Map requires an even number of arguments")
ExInst<SqlValidatorException> 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<SqlValidatorException> 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 2479a92ba2..539d6b327f 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<RelDataType> operandTypes = opBinding.collectOperandTypes();
- for (RelDataType operandType : operandTypes) {
- if (!SqlTypeUtil.isMap(operandType)) {
- throw opBinding.newError(
- RESOURCE.typesShouldAllBeMap(
- opBinding.getOperator().getName(),
- operandType.getFullTypeString()));
- }
+ }
+ final List<RelDataType> operandTypes = opBinding.collectOperandTypes();
+ final List<RelDataType> 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<ANY, ANY> 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<RelDataType> 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<RelDataType> 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 0799d8d6cc..49552b4198 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 080a704a8c..2119f79df2 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<string, int>), 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<varchar, int>))");
@@ -9281,9 +9288,9 @@ void checkArrayReverseFunc(SqlOperatorFixture f0,
SqlFunction function,
f1.checkType("map_concat(cast(null as map<varchar, int>), 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);