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

Reply via email to