This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch site
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/site by this push:
new e71a408ed1 [CALCITE-6764] Field access from a nullable ROW should be
nullable (part 2)
e71a408ed1 is described below
commit e71a408ed158053f882b4e35dbc6e11f8b302944
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Jan 10 17:04:49 2025 -0800
[CALCITE-6764] Field access from a nullable ROW should be nullable (part 2)
Signed-off-by: Mihai Budiu <[email protected]>
---
.../calcite/rel/type/RelDataTypeFactory.java | 29 ++++-
.../calcite/rel/type/RelDataTypeFactoryImpl.java | 25 ++++
.../java/org/apache/calcite/rex/RexBuilder.java | 6 +-
.../org/apache/calcite/sql/type/BasicSqlType.java | 2 +-
.../calcite/sql/type/SqlTypeFactoryImpl.java | 22 ++++
.../calcite/sql/validate/SqlValidatorImpl.java | 8 ++
.../apache/calcite/test/TableInRootSchemaTest.java | 130 +++++++++++++++++++++
site/_docs/history.md | 10 ++
8 files changed, 225 insertions(+), 7 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
index 9821451b9a..5912f78ac7 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
@@ -179,18 +179,37 @@ public interface RelDataTypeFactory {
* Creates a type that is the same as another type but with possibly
* different nullability. The output type may be identical to the input
* type. For type systems without a concept of nullability, the return value
- * is always the same as the input.
+ * is always the same as the input. This function never returns a nullable
+ * record type; when applied to a record type, it recursively makes all
fields
+ * nullable and the record itself is non-nullable.
*
- * @param type input type
- * @param nullable true to request a nullable type; false to request a NOT
- * NULL type
- * @return output type, same as input type except with specified nullability
+ * @param type input type
+ * @param nullable true to request a nullable type; false to request a NOT
NULL type.
+ * @return output type, same as input type except with specified nullability
(see description
+ * for record types).
* @throws NullPointerException if type is null
*/
RelDataType createTypeWithNullability(
RelDataType type,
boolean nullable);
+ /**
+ * Creates a type that is the same as another type but with possibly
+ * different nullability. The output type may be identical to the input
+ * type. For type systems without a concept of nullability, the return value
+ * is always the same as the input. This differs from {@link
+ * #createTypeWithNullability(RelDataType, boolean)} in the handling of
record
+ * types. This function returns a nullable struct.
+ *
+ * @param type input type
+ * @param nullable true to request a nullable type; false to request a NOT
NULL type.
+ * @return output type, same as input type except with specified nullability
+ * @throws NullPointerException if type is null
+ */
+ RelDataType enforceTypeWithNullability(
+ RelDataType type,
+ boolean nullable);
+
/**
* Creates a type that is the same as another type but with possibly
* different charset or collation. For types without a concept of charset or
diff --git
a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
index 56c0a2c2e5..4ea101fd1d 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactoryImpl.java
@@ -410,6 +410,31 @@ public abstract class RelDataTypeFactoryImpl implements
RelDataTypeFactory {
return canonize(newType);
}
+ @Override public RelDataType enforceTypeWithNullability(
+ final RelDataType type,
+ final boolean nullable) {
+ requireNonNull(type, "type");
+ RelDataType newType;
+ if (type.isNullable() == nullable) {
+ newType = type;
+ } else if (type instanceof RelRecordType) {
+ return createStructType(type.getStructKind(),
+ new AbstractList<RelDataType>() {
+ @Override public RelDataType get(int index) {
+ return type.getFieldList().get(index).getType();
+ }
+
+ @Override public int size() {
+ return type.getFieldCount();
+ }
+ },
+ type.getFieldNames(), nullable);
+ } else {
+ newType = copySimpleType(type, nullable);
+ }
+ return canonize(newType);
+ }
+
/**
* Registers a type, or returns the existing type if it is already
* registered.
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index 9491aa6ec3..94bc326c9a 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -246,7 +246,11 @@ public class RexBuilder {
}
return new RexInputRef(
range.getOffset() + field.getIndex(),
- field.getType());
+ fieldType);
+ }
+
+ if (expr.getType().isNullable()) {
+ fieldType = typeFactory.enforceTypeWithNullability(fieldType, true);
}
return new RexFieldAccess(expr, field);
}
diff --git a/core/src/main/java/org/apache/calcite/sql/type/BasicSqlType.java
b/core/src/main/java/org/apache/calcite/sql/type/BasicSqlType.java
index 9441303dad..e0642659f3 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/BasicSqlType.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/BasicSqlType.java
@@ -126,7 +126,7 @@ public class BasicSqlType extends AbstractSqlType {
/**
* Constructs a type with nullablity.
*/
- BasicSqlType createWithNullability(boolean nullable) {
+ public BasicSqlType createWithNullability(boolean nullable) {
if (nullable == this.isNullable) {
return this;
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
index f71804e7a8..82fd14d6f4 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
@@ -253,6 +253,28 @@ public class SqlTypeFactoryImpl extends
RelDataTypeFactoryImpl {
return canonize(newType);
}
+ @Override public RelDataType enforceTypeWithNullability(
+ final RelDataType type,
+ final boolean nullable) {
+ final RelDataType newType;
+ if (type instanceof BasicSqlType) {
+ newType = ((BasicSqlType) type).createWithNullability(nullable);
+ } else if (type instanceof MapSqlType) {
+ newType = copyMapType(type, nullable);
+ } else if (type instanceof ArraySqlType) {
+ newType = copyArrayType(type, nullable);
+ } else if (type instanceof MultisetSqlType) {
+ newType = copyMultisetType(type, nullable);
+ } else if (type instanceof IntervalSqlType) {
+ newType = copyIntervalType(type, nullable);
+ } else if (type instanceof ObjectSqlType) {
+ newType = copyObjectType(type, nullable);
+ } else {
+ return super.enforceTypeWithNullability(type, nullable);
+ }
+ return canonize(newType);
+ }
+
private static void assertBasic(SqlTypeName typeName) {
assert typeName != null;
assert typeName != SqlTypeName.MULTISET
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 1a08b8cdb9..55090f145f 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -7052,7 +7052,15 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
throw newValidationError(id.getComponent(i),
RESOURCE.unknownField(name));
}
+ boolean recordIsNullable = type.isNullable();
type = field.getType();
+ if (recordIsNullable) {
+ // If parent record is nullable, field must also be nullable.
+ // Consider CREATE TABLE T(p ROW(k VARCHAR) NULL);
+ // Since T.p is nullable, T.p.k also has to be nullable, even if
+ // the type of k itself is not nullable.
+ type = getTypeFactory().enforceTypeWithNullability(type, true);
+ }
}
type =
SqlTypeUtil.addCharsetAndCollation(
diff --git
a/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
b/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
index 55b058a2f9..537a562b60 100644
--- a/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
+++ b/core/src/test/java/org/apache/calcite/test/TableInRootSchemaTest.java
@@ -73,4 +73,134 @@ class TableInRootSchemaTest {
connection.close();
}
+ /** Represents a table with no data. An abstract base class,
+ * derived classes need to define the schema. */
+ private abstract static class EmptyTable extends AbstractQueryableTable {
+ protected EmptyTable() {
+ super(Object[].class);
+ }
+
+ @Override public <T> Queryable<T> asQueryable(
+ QueryProvider queryProvider, SchemaPlus schema, String tableName) {
+ return new AbstractTableQueryable<T>(queryProvider, schema, this,
+ tableName) {
+ @Override public Enumerator<T> enumerator() {
+ return new Enumerator<T>() {
+ @Override public T current() {
+ return null;
+ }
+
+ @Override public boolean moveNext() {
+ // Table is empty
+ return false;
+ }
+
+ @Override public void reset() {}
+
+ @Override public void close() {}
+ };
+ }
+ };
+ }
+ }
+
+ /** Helper class for the test for [CALCITE-6764] below. */
+ private static class TableWithNullableRowInMap extends EmptyTable {
+ protected TableWithNullableRowInMap() {
+ super();
+ }
+
+ @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+ final PairList<String, RelDataType> columnDesc =
PairList.withCapacity(1);
+ // Schema contains a column whose type is MAP<VARCHAR, ROW(VARCHAR)>, but
+ // the ROW type can be nullable. This can conceivably be created by a
+ // declaration such as
+ // CREATE TABLE T(P MAP<VARCHAR, ROW(K VARCHAR NON NULL, S VARCHAR
NULL)>);
+ final RelDataType colType =
+
typeFactory.createMapType(typeFactory.createSqlType(SqlTypeName.VARCHAR),
+ new RelRecordType(
+ StructKind.PEEK_FIELDS,
+ ImmutableList.of(
+ new RelDataTypeFieldImpl("K", 0,
+ typeFactory.createSqlType(SqlTypeName.VARCHAR)),
+ new RelDataTypeFieldImpl("S", 1,
+ typeFactory.createTypeWithNullability(
+ typeFactory.createSqlType(SqlTypeName.VARCHAR),
true))),
+ true));
+ columnDesc.add("P", colType);
+ return typeFactory.createStructType(columnDesc);
+ }
+ }
+
+ /** Helper class for the test for [CALCITE-6764] below. */
+ private static class TableWithNullableRowToplevel extends EmptyTable {
+ protected TableWithNullableRowToplevel() {
+ super();
+ }
+
+ @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+ final PairList<String, RelDataType> columnDesc =
PairList.withCapacity(1);
+ // This table can conceivably be created by a declaration such as
+ // CREATE TABLE T(P ROW(K VARCHAR NOT NULL) NULL,
+ // Q ROW(S ROW(L VARCHAR NOT NULL, M VARCHAR NULL) NULL)
NULL);
+ final RelDataType pColType =
+ new RelRecordType(
+ StructKind.PEEK_FIELDS, ImmutableList.of(
+ new RelDataTypeFieldImpl(
+ "K", 0, typeFactory.createSqlType(SqlTypeName.VARCHAR))),
+ true);
+ final RelDataType sType =
+ new RelRecordType(
+ StructKind.PEEK_FIELDS, ImmutableList.of(
+ new RelDataTypeFieldImpl(
+ "L", 0, typeFactory.createSqlType(SqlTypeName.VARCHAR)),
+ new RelDataTypeFieldImpl(
+ "M", 1, typeFactory.createSqlType(SqlTypeName.VARCHAR))),
+ false);
+ final RelDataType qColType =
+ new RelRecordType(
+ StructKind.PEEK_FIELDS, ImmutableList.of(
+ new RelDataTypeFieldImpl("S", 0, sType)),
+ true);
+ columnDesc.add("P", pColType);
+ columnDesc.add("Q", qColType);
+ return typeFactory.createStructType(columnDesc);
+ }
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6764">[CALCITE-6764]
+ * Field access from a nullable ROW should be nullable</a>. */
+ @Test void testNullableRowInMap() throws Exception {
+ Connection connection = DriverManager.getConnection("jdbc:calcite:");
+ CalciteConnection calciteConnection =
connection.unwrap(CalciteConnection.class);
+ calciteConnection.getRootSchema().add("T", new
TableWithNullableRowInMap());
+ Statement statement = calciteConnection.createStatement();
+ // Without the fix to this issue the Validator crashes with an
AssertionFailure:
+ // java.lang.RuntimeException: java.lang.AssertionError:
+ // Conversion to relational algebra failed to preserve datatypes:
+ // validated type:
+ ResultSet resultSet = statement.executeQuery("SELECT P['a'].K, P['a'].S
FROM T");
+ resultSet.close();
+ statement.close();
+ connection.close();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6764">[CALCITE-6764]
+ * Field access from a nullable ROW should be nullable</a>. */
+ @Test void testNullableRowTopLevel() throws Exception {
+ Connection connection = DriverManager.getConnection("jdbc:calcite:");
+ CalciteConnection calciteConnection =
connection.unwrap(CalciteConnection.class);
+ calciteConnection.getRootSchema().add("T", new
TableWithNullableRowToplevel());
+ Statement statement = calciteConnection.createStatement();
+ // Without the fix to this issue the Validator crashes with an
AssertionFailure:
+ // java.lang.RuntimeException: java.lang.AssertionError:
+ // Conversion to relational algebra failed to preserve datatypes:
+ // validated type:
+ ResultSet resultSet = statement.executeQuery("SELECT T.P.K, T.Q.S,
T.Q.S.L, T.Q.S.M FROM T");
+ resultSet.close();
+ statement.close();
+ connection.close();
+ }
}
diff --git a/site/_docs/history.md b/site/_docs/history.md
index feaa06069d..b18f414d46 100644
--- a/site/_docs/history.md
+++ b/site/_docs/history.md
@@ -49,6 +49,16 @@ other software versions as specified in gradle.properties.
#### Breaking Changes
{: #breaking-1-39-0}
+*RelDataTypeFactory interface*. The fix for [<a
+href="https://issues.apache.org/jira/browse/CALCITE-6764">CALCITE-6764</a>]
+introduces a new method
+`RelDataTypeFactory#enforceTypeWithNullability` in the existing
+`RelDataTypeFactory` interface. The behavior of the new function is
+similar to the existing API `createTypeWithNullability`; however, the
+existing implementations of the `createTypeWithNullability` API cannot
+create nullable record (`ROW`) types. Nullable record types are
+legitimate in several SQL dialects.
+
*Checked arithmetic*. [<a
href="https://issues.apache.org/jira/browse/CALCITE-6685">CALCITE-6685</a>]
introduces support for checked arithmetic on short integer types. A