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);

Reply via email to