This is an automated email from the ASF dual-hosted git repository.
snuyanzin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new c8fb3fb0973 [FLINK-40794][table] Derive `CAST` result nullability from
the input in the Table API
c8fb3fb0973 is described below
commit c8fb3fb0973c9507489266a219d9af6c51cc177b
Author: Ramin Gharib <[email protected]>
AuthorDate: Fri Sep 25 15:47:06 2026 +0200
[FLINK-40794][table] Derive `CAST` result nullability from the input in the
Table API
---
docs/data/sql_functions.yml | 2 +-
docs/data/sql_functions_zh.yml | 2 ++
.../converter/converters/CastConverter.java | 12 ++------
.../converter/ExpressionConverterTest.java | 33 ++++++++++++++++++++++
4 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/docs/data/sql_functions.yml b/docs/data/sql_functions.yml
index f7517549d1f..ee94bf1794f 100644
--- a/docs/data/sql_functions.yml
+++ b/docs/data/sql_functions.yml
@@ -827,7 +827,7 @@ conditional:
conversion:
- sql: CAST(value AS type)
table: ANY.cast(TYPE)
- description: Returns a new value being cast to type type. A CAST error
throws an exception and fails the job. When performing a cast operation that
may fail, like STRING to INT, one should rather use TRY_CAST, in order to
handle errors. If "table.exec.legacy-cast-behaviour" is enabled, CAST behaves
like TRY_CAST. E.g., CAST('42' AS INT) returns 42; CAST(NULL AS STRING) returns
NULL of type STRING; CAST('non-number' AS INT) throws an exception and fails
the job. Casting BINARY/VARBINA [...]
+ description: Returns a new value being cast to type type. A CAST error
throws an exception and fails the job. When performing a cast operation that
may fail, like STRING to INT, one should rather use TRY_CAST, in order to
handle errors. If "table.exec.legacy-cast-behaviour" is enabled, CAST behaves
like TRY_CAST. E.g., CAST('42' AS INT) returns 42; CAST(NULL AS STRING) returns
NULL of type STRING; CAST('non-number' AS INT) throws an exception and fails
the job. The result is nullable [...]
- sql: TRY_CAST(value AS type)
table: ANY.tryCast(TYPE)
description: Like CAST, but in case of error, returns NULL rather than
failing the job. E.g., TRY_CAST('42' AS INT) returns 42; TRY_CAST(NULL AS
STRING) returns NULL of type STRING; TRY_CAST('non-number' AS INT) returns NULL
of type INT; COALESCE(TRY_CAST('non-number' AS INT), 0) returns 0 of type INT.
diff --git a/docs/data/sql_functions_zh.yml b/docs/data/sql_functions_zh.yml
index a7f9b7bb2f4..a6c6fa074b3 100644
--- a/docs/data/sql_functions_zh.yml
+++ b/docs/data/sql_functions_zh.yml
@@ -965,6 +965,8 @@ conversion:
例如, CAST('42' AS INT) 返回 42; CAST(NULL AS STRING) 返回字符串类型的 `NULL`;
CAST('non-number' AS INT) 抛出异常且作业失败。
+ The result is nullable if the input may be null. In the Table API, the
top-level nullability of TYPE is ignored, while the nullability of nested
types, such as the element type of an array, is taken from TYPE.
+
Casting BINARY/VARBINARY/BYTES to a CHAR/VARCHAR/STRING type validates
that the input is well-formed UTF-8 and throws on invalid sequences. Use
MAKE_VALID_UTF8 to substitute the Unicode replacement character U+FFFD for
invalid bytes, TRY_CAST to return NULL, or set
"table.exec.legacy-bytes-to-string-cast" to "true" to restore the prior
silent-substitution behavior.
- sql: TRY_CAST(value AS type)
table: ANY.tryCast(TYPE)
diff --git
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
index 06f2a01b3b5..d0d70f7dee0 100644
---
a/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
+++
b/flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/expressions/converter/converters/CastConverter.java
@@ -20,11 +20,8 @@ package
org.apache.flink.table.planner.expressions.converter.converters;
import org.apache.flink.annotation.Internal;
import org.apache.flink.table.expressions.CallExpression;
-import org.apache.flink.table.expressions.TypeLiteralExpression;
import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import
org.apache.flink.table.planner.expressions.converter.CallExpressionConvertRule;
-import org.apache.flink.table.types.DataType;
-import org.apache.flink.table.types.logical.LogicalTypeRoot;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
@@ -37,15 +34,10 @@ class CastConverter extends CustomizedConverter {
checkArgumentNumber(call, 2);
final RexNode child = context.toRexNode(call.getChildren().get(0));
- final TypeLiteralExpression targetType = (TypeLiteralExpression)
call.getChildren().get(1);
- final DataType inputType =
call.getResolvedChildren().get(0).getOutputDataType();
- final DataType resultType =
- inputType.getLogicalType().is(LogicalTypeRoot.VARIANT)
- ? call.getOutputDataType()
- : targetType.getOutputDataType();
+ // Not the literal target type: its top-level nullability must not
override the input's.
final RelDataType targetRelDataType =
context.getTypeFactory()
-
.createFieldTypeFromLogicalType(resultType.getLogicalType());
+
.createFieldTypeFromLogicalType(call.getOutputDataType().getLogicalType());
return context.getRelBuilder()
.getRexBuilder()
diff --git
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/expressions/converter/ExpressionConverterTest.java
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/expressions/converter/ExpressionConverterTest.java
index fe2bc4ff716..37507c7a127 100644
---
a/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/expressions/converter/ExpressionConverterTest.java
+++
b/flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/expressions/converter/ExpressionConverterTest.java
@@ -19,9 +19,13 @@
package org.apache.flink.table.planner.expressions.converter;
import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.expressions.CallExpression;
+import org.apache.flink.table.expressions.ResolvedExpression;
import org.apache.flink.table.expressions.TimePointUnit;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import org.apache.flink.table.planner.delegation.PlannerContext;
import org.apache.flink.table.planner.utils.PlannerMocks;
+import org.apache.flink.table.types.DataType;
import org.apache.calcite.avatica.util.TimeUnit;
import org.apache.calcite.rex.RexLiteral;
@@ -40,8 +44,10 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
+import java.util.List;
import java.util.UUID;
+import static
org.apache.flink.table.expressions.ApiExpressionUtils.typeLiteral;
import static
org.apache.flink.table.expressions.ApiExpressionUtils.valueLiteral;
import static org.assertj.core.api.Assertions.assertThat;
@@ -220,4 +226,31 @@ class ExpressionConverterTest {
assertThat(((RexLiteral) rex).getValueAs(UUID.class)).isEqualTo(uuid);
assertThat(rex.getType().getSqlTypeName()).isEqualTo(SqlTypeName.UUID);
}
+
+ @Test
+ void testCastToNotNullTypeFromNullableInput() {
+ final RexNode rex =
+ convertCast(
+ valueLiteral(null, DataTypes.INT()),
+ DataTypes.BIGINT().notNull(),
+ DataTypes.BIGINT());
+ assertThat(rex.getType().isNullable()).isTrue();
+ }
+
+ @Test
+ void testCastToNullableTypeFromNotNullInput() {
+ final RexNode rex =
+ convertCast(valueLiteral(1), DataTypes.BIGINT(),
DataTypes.BIGINT().notNull());
+ assertThat(rex.getType().isNullable()).isFalse();
+ }
+
+ /** The output type is the one the CAST type strategy derives; the RexNode
must follow it. */
+ private RexNode convertCast(
+ ResolvedExpression input, DataType targetType, DataType
outputType) {
+ return converter.visit(
+ CallExpression.permanent(
+ BuiltInFunctionDefinitions.CAST,
+ List.of(input, typeLiteral(targetType)),
+ outputType));
+ }
}