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