This is an automated email from the ASF dual-hosted git repository.

zhenchen 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 3719447e12 [CALCITE-7062] Row type of SetOp may ignore a column's 
nullability
3719447e12 is described below

commit 3719447e125982aaa47f0a7b2564f94b9900539e
Author: Aleksey Plekhanov <[email protected]>
AuthorDate: Fri Jul 4 12:06:03 2025 +0300

    [CALCITE-7062] Row type of SetOp may ignore a column's nullability
---
 .../calcite/sql/SqlCollectionTypeNameSpec.java     | 22 +++++++-
 .../org/apache/calcite/sql/SqlMapTypeNameSpec.java | 12 ++---
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   |  7 ++-
 .../validate/implicit/AbstractTypeCoercion.java    | 12 ++---
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  7 +--
 .../org/apache/calcite/test/SqlValidatorTest.java  | 32 ++++++++++++
 .../org/apache/calcite/test/TypeCoercionTest.java  |  6 ++-
 core/src/test/resources/sql/cast.iq                | 60 ++++++++++++++++++++++
 .../java/org/apache/calcite/test/PigRelOpTest.java |  2 +-
 9 files changed, 134 insertions(+), 26 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/SqlCollectionTypeNameSpec.java 
b/core/src/main/java/org/apache/calcite/sql/SqlCollectionTypeNameSpec.java
index 05be5ca3d4..16fb3b5832 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlCollectionTypeNameSpec.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlCollectionTypeNameSpec.java
@@ -58,32 +58,50 @@
  */
 public class SqlCollectionTypeNameSpec extends SqlTypeNameSpec {
   private final SqlTypeNameSpec elementTypeName;
+  private final boolean elementTypeNullable;
   private final SqlTypeName collectionTypeName;
 
   /**
    * Creates a {@code SqlCollectionTypeNameSpec}.
    *
    * @param elementTypeName    Type of the collection element
+   * @param elementTypeNullable Type of the collection element is nullable
    * @param collectionTypeName Collection type name
    * @param pos                Parser position, must not be null
    */
   public SqlCollectionTypeNameSpec(SqlTypeNameSpec elementTypeName,
+      boolean elementTypeNullable,
       SqlTypeName collectionTypeName,
       SqlParserPos pos) {
     super(new SqlIdentifier(collectionTypeName.name(), pos), pos);
     this.elementTypeName = requireNonNull(elementTypeName, "elementTypeName");
+    this.elementTypeNullable = elementTypeNullable;
     this.collectionTypeName =
         requireNonNull(collectionTypeName, "collectionTypeName");
   }
 
+  /**
+   * Creates a {@code SqlCollectionTypeNameSpec}.
+   *
+   * @param elementTypeName    Type of the collection element
+   * @param collectionTypeName Collection type name
+   * @param pos                Parser position, must not be null
+   */
+  public SqlCollectionTypeNameSpec(SqlTypeNameSpec elementTypeName,
+      SqlTypeName collectionTypeName,
+      SqlParserPos pos) {
+    this(elementTypeName, true, collectionTypeName, pos);
+  }
+
   public SqlTypeNameSpec getElementTypeName() {
     return elementTypeName;
   }
 
   @Override public RelDataType deriveType(SqlValidator validator) {
     RelDataType type = elementTypeName.deriveType(validator);
-    // We have to assume that elements may be nullable
-    type = validator.getTypeFactory().enforceTypeWithNullability(type, true);
+    if (elementTypeNullable) {
+      type = validator.getTypeFactory().enforceTypeWithNullability(type, true);
+    }
     return createCollectionType(type, validator.getTypeFactory());
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlMapTypeNameSpec.java 
b/core/src/main/java/org/apache/calcite/sql/SqlMapTypeNameSpec.java
index 146d510f5d..f80e5b6b1f 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlMapTypeNameSpec.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlMapTypeNameSpec.java
@@ -55,15 +55,11 @@ public SqlDataTypeSpec getValType() {
   }
 
   @Override public RelDataType deriveType(SqlValidator validator) {
-    RelDataType kType = keyType.deriveType(validator);
-    if (validator.getTypeFactory().getTypeSystem().mapKeysCanBeNullable()) {
-      // We have to assume that keys may be nullable
-      kType = validator.getTypeFactory().enforceTypeWithNullability(kType, 
true);
-    }
+    boolean keyCanBeNullable = 
validator.getTypeFactory().getTypeSystem().mapKeysCanBeNullable();
+    RelDataType kType =
+        keyType.deriveType(validator, keyCanBeNullable);
 
-    RelDataType valueType = valType.deriveType(validator);
-    // We have to assume that values may be nullable
-    valueType = 
validator.getTypeFactory().enforceTypeWithNullability(valueType, true);
+    RelDataType valueType = valType.deriveType(validator, true);
 
     return validator
         .getTypeFactory()
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 ace4b6bbd2..add5190f2e 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
@@ -1209,9 +1209,11 @@ public static SqlDataTypeSpec 
convertTypeToSpec(RelDataType type,
           new SqlBasicTypeNameSpec(typeName, precision, scale, charSetName,
               SqlParserPos.ZERO);
     } else if (isCollection(type)) {
+      RelDataType componentType = getComponentTypeOrThrow(type);
       typeNameSpec =
           new SqlCollectionTypeNameSpec(
-              
convertTypeToSpec(getComponentTypeOrThrow(type)).getTypeNameSpec(),
+              convertTypeToSpec(componentType).getTypeNameSpec(),
+              componentType.isNullable(),
               typeName, SqlParserPos.ZERO);
     } else if (isRow(type)) {
       RelRecordType recordType = (RelRecordType) type;
@@ -1242,7 +1244,8 @@ public static SqlDataTypeSpec 
convertTypeToSpec(RelDataType type,
     // REVIEW angel 11-Jan-2006:
     // Use neg numbers to indicate unspecified precision/scale
 
-    return new SqlDataTypeSpec(typeNameSpec, SqlParserPos.ZERO);
+    return new SqlDataTypeSpec(typeNameSpec, SqlParserPos.ZERO)
+        .withNullable(type.isNullable());
   }
 
   /**
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
index fe882eac75..1e1ac765db 100644
--- 
a/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
+++ 
b/core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java
@@ -380,15 +380,9 @@ protected void updateInferredColumnType(
     }
     // If one type is with Null type name: returns the other.
     if (SqlTypeUtil.isNull(type1)) {
-      if (SqlTypeUtil.isMap(type2) || SqlTypeUtil.isRow(type2) || 
SqlTypeUtil.isArray(type2)) {
-        return type2;
-      }
       return factory.createTypeWithNullability(type2, type1.isNullable());
     }
     if (SqlTypeUtil.isNull(type2)) {
-      if (SqlTypeUtil.isMap(type1) || SqlTypeUtil.isRow(type1) || 
SqlTypeUtil.isArray(type1)) {
-        return type1;
-      }
       return factory.createTypeWithNullability(type1, type2.isNullable());
     }
     RelDataType resultType = null;
@@ -407,10 +401,12 @@ protected void updateInferredColumnType(
     }
     // Date + Timestamp -> Timestamp.
     if (SqlTypeUtil.isDate(type1) && SqlTypeUtil.isTimestamp(type2)) {
-      resultType = type2;
+      return factory.createTypeWithNullability(type2,
+          type1.isNullable() || type2.isNullable());
     }
     if (SqlTypeUtil.isDate(type2) && SqlTypeUtil.isTimestamp(type1)) {
-      resultType = type1;
+      return factory.createTypeWithNullability(type1,
+          type1.isNullable() || type2.isNullable());
     }
 
     if (type1.isStruct() && type2.isStruct()) {
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 8be953d2ba..6157c24f9a 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
@@ -5073,15 +5073,16 @@ 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 >)\n"
+        .ok("SELECT CAST(MAP['A', 1.0] AS "
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", DOUBLE NULL >)\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 >)\n"
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\", INTEGER ARRAY NULL 
>)\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 > >)\n"
+            + "MAP< VARCHAR CHARACTER SET \"ISO-8859-1\" ARRAY, MAP< INTEGER, 
INTEGER NULL > NULL >)\n"
             + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")");
   }
 
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 ec4cd2b84b..ccf76671c1 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -5345,6 +5345,38 @@ private ImmutableList<ImmutableBitSet> 
cube(ImmutableBitSet... sets) {
         + "select deptno, name, deptno from dept").ok();
   }
 
+  @Test void testUnionNullableTypeDerivation() {
+    sql("SELECT CAST(NULL AS DATE) UNION ALL SELECT TIMESTAMP '2025-07-04 
10:00:00'")
+        .columnType("TIMESTAMP(0)");
+
+    sql("SELECT DATE '2025-07-05' UNION ALL SELECT TIMESTAMP '2025-07-04 
10:00:00'")
+        .columnType("TIMESTAMP(0) NOT NULL");
+
+    sql("SELECT ARRAY[1, 2, cast(null as integer)] UNION ALL SELECT NULL")
+        .columnType("INTEGER ARRAY");
+
+    sql("SELECT ARRAY[1, 2, 3] UNION ALL SELECT NULL")
+        .columnType("INTEGER NOT NULL ARRAY");
+
+    sql("SELECT ARRAY[1, 2] UNION ALL SELECT ARRAY[3, cast(null as integer)]")
+        .columnType("INTEGER ARRAY NOT NULL");
+
+    sql("SELECT ARRAY[1, 2, 3] UNION ALL SELECT ARRAY[4, 5]")
+        .columnType("INTEGER NOT NULL ARRAY NOT NULL");
+
+    sql("SELECT MAP[1, 2, 3, 4] UNION ALL SELECT NULL")
+        .columnType("(INTEGER NOT NULL, INTEGER NOT NULL) MAP");
+
+    sql("SELECT MAP[1, 2, 3, cast(null as integer)] UNION ALL SELECT NULL")
+        .columnType("(INTEGER NOT NULL, INTEGER) MAP");
+
+    sql("SELECT MAP[1, 2, 3, cast(null as integer)] UNION ALL SELECT MAP[5, 
6]")
+        .columnType("(INTEGER NOT NULL, INTEGER) MAP NOT NULL");
+
+    sql("SELECT MAP[1, 2, 3, 4] UNION ALL SELECT MAP[5, 6]")
+        .columnType("(INTEGER NOT NULL, INTEGER NOT NULL) MAP NOT NULL");
+  }
+
   @Test void testValuesTypeMismatchFails() {
     sql("^values (1), ('a')^")
         .fails("Values passed to VALUES operator must have compatible types");
diff --git a/core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java 
b/core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java
index c422832454..e4f521d0bb 100644
--- a/core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java
@@ -152,9 +152,11 @@ private static ImmutableList<RelDataType> combine(
     f.checkCommonType(f.varcharType, f.timestampType, null, true);
     // STRUCT
     f.checkCommonType(f.nullType, f.mapType(f.intType, f.charType),
-        f.mapType(f.intType, f.charType), true);
+        f.typeFactory.createTypeWithNullability(f.mapType(f.intType, 
f.charType), true),
+        true);
     f.checkCommonType(f.nullType, f.recordType(ImmutableList.of()),
-        f.recordType(ImmutableList.of()), true);
+        
f.typeFactory.createTypeWithNullability(f.recordType(ImmutableList.of()), true),
+        true);
     f.checkCommonType(f.charType, f.mapType(f.intType, f.charType), null, 
true);
     f.checkCommonType(f.arrayType(f.intType), f.recordType(ImmutableList.of()),
         null, true);
diff --git a/core/src/test/resources/sql/cast.iq 
b/core/src/test/resources/sql/cast.iq
index 72367acfff..95d11e5b5a 100644
--- a/core/src/test/resources/sql/cast.iq
+++ b/core/src/test/resources/sql/cast.iq
@@ -1918,4 +1918,64 @@ EnumerableCalc(expr#0=[{inputs}], 
expr#1=[1.11:DECIMAL(3, 2)], expr#2=[2.22:DECI
   EnumerableValues(tuples=[[{ 0 }]])
 !plan
 
+# [CALCITE-7062] Row type of SetOp may ignore a column's nullability
+# Common type for nullable date and not nullable timestamp
+SELECT CAST(NULL AS DATE) UNION ALL SELECT TIMESTAMP '2025-07-04 10:00:00';
++---------------------+
+| EXPR$0              |
++---------------------+
+|                     |
+| 2025-07-04 10:00:00 |
++---------------------+
+(2 rows)
+
+!ok
+EXPR$0 TIMESTAMP
+!type
+
+# [CALCITE-7062] Row type of SetOp may ignore a column's nullability
+# Common type for null and not nullable array;
+SELECT ARRAY[1, 2, 3] UNION ALL SELECT NULL;
++-----------+
+| EXPR$0    |
++-----------+
+| [1, 2, 3] |
+|           |
++-----------+
+(2 rows)
+
+!ok
+EXPR$0 INTEGER ARRAY
+!type
+
+# [CALCITE-7062] Row type of SetOp may ignore a column's nullability
+# Common type for null and not nullable map
+SELECT MAP[1, 2, 3, 4] UNION ALL SELECT NULL;
++------------+
+| EXPR$0     |
++------------+
+| {1=2, 3=4} |
+|            |
++------------+
+(2 rows)
+
+!ok
+EXPR$0 (INTEGER, INTEGER) MAP
+!type
+
+# [CALCITE-7062] Row type of SetOp may ignore a column's nullability
+# Common type for arrays with nullable and not nullable elements
+SELECT ARRAY[cast(null as integer), cast(null as integer), cast(null as 
integer)] UNION ALL SELECT ARRAY[1, 2, 3];
++--------------------+
+| EXPR$0             |
++--------------------+
+| [null, null, null] |
+| [1, 2, 3]          |
++--------------------+
+(2 rows)
+
+!ok
+EXPR$0 INTEGER ARRAY NOT NULL
+!type
+
 # End cast.iq
diff --git a/piglet/src/test/java/org/apache/calcite/test/PigRelOpTest.java 
b/piglet/src/test/java/org/apache/calcite/test/PigRelOpTest.java
index 85d64e5a4d..5067503b89 100644
--- a/piglet/src/test/java/org/apache/calcite/test/PigRelOpTest.java
+++ b/piglet/src/test/java/org/apache/calcite/test/PigRelOpTest.java
@@ -1547,7 +1547,7 @@ private Fluent pig(String script) {
         + "        LogicalTableScan(table=[[scott, EMP]])\n";
     final String sql = ""
         + "SELECT DEPTNO, MGR, HIREDATE, CAST(COUNT(*) AS BIGINT) AS $f3, 1 AS 
newCol, "
-        + "CAST(COLLECT(COMM) AS ROW(COMM DECIMAL(19, 0)) MULTISET) AS 
comArray, CAST(SUM(SAL) AS DECIMAL(19, 0)) AS salSum\n"
+        + "CAST(COLLECT(COMM) AS ROW(COMM DECIMAL(19, 0) NULL) MULTISET) AS 
comArray, CAST(SUM(SAL) AS DECIMAL(19, 0)) AS salSum\n"
         + "FROM scott.EMP\n"
         + "GROUP BY DEPTNO, MGR, HIREDATE\n"
         + "ORDER BY 7";

Reply via email to