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

morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 4a4b8578ec3 [improvement](cast) Add cast conversion failure analysis 
(#68367)
4a4b8578ec3 is described below

commit 4a4b8578ec32bb22d081254f3f28c43f4d607296
Author: morrySnow <[email protected]>
AuthorDate: Tue Sep 22 18:06:34 2026 +0800

    [improvement](cast) Add cast conversion failure analysis (#68367)
    
    ### What problem does this PR solve?
    
    Problem Summary: `Cast.castNullable` describes the outer result column's
    nullability, but does not tell an optimizer whether converting a
    non-null value can fail. For example, `ARRAY<STRING> -> ARRAY<INT>` can
    leave the outer array non-null while an element conversion fails. This
    PR adds `Cast.mayFailOnNonNullInput` as a reusable type-pair and
    expression-level analysis. It recurses through ARRAY, MAP, and STRUCT,
    recognizes BE conversions proven not to report data-dependent failures,
    and conservatively treats other conversions as possibly failing. A
    strict CAST can report such a failure as an error, while a non-strict
    CAST or TRY_CAST may produce NULL. The new API has no production callers
    yet.
---
 .../doris/nereids/trees/expressions/Cast.java      | 72 ++++++++++++++++++++++
 .../doris/nereids/trees/expressions/CastTest.java  | 65 +++++++++++++++++++
 2 files changed, 137 insertions(+)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/Cast.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/Cast.java
index f31ce955b74..e2316b40a60 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/Cast.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/Cast.java
@@ -27,6 +27,7 @@ import 
org.apache.doris.nereids.trees.expressions.literal.TimeStampNsLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TimestampTzLiteral;
 import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
 import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.ArrayType;
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.DateTimeV2Type;
@@ -34,7 +35,10 @@ import org.apache.doris.nereids.types.DecimalV2Type;
 import org.apache.doris.nereids.types.DecimalV3Type;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.LargeIntType;
+import org.apache.doris.nereids.types.MapType;
 import org.apache.doris.nereids.types.SmallIntType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
 import org.apache.doris.nereids.types.TimeStampNsType;
 import org.apache.doris.nereids.types.TimeStampTzType;
 import org.apache.doris.nereids.types.TinyIntType;
@@ -269,6 +273,74 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
         return false;
     }
 
+    /**
+     * Whether converting a non-null value can fail. A strict CAST reports 
such a failure as an
+     * error; a non-strict CAST may produce NULL, including inside an ARRAY, 
MAP, or STRUCT;
+     * TRY_CAST can turn a reported error into NULL. This property is 
independent of the input's
+     * nullability and of the cast mode. It is conservative for conversions 
without a proven
+     * failure-free BE implementation. It does not cover unrelated execution 
errors.
+     */
+    public boolean mayFailOnNonNullInput() {
+        return mayFailOnNonNullInput(child().getDataType(), targetType);
+    }
+
+    /** Whether the BE conversion from sourceType to targetType can fail on a 
non-null input. */
+    public static boolean mayFailOnNonNullInput(DataType sourceType, DataType 
targetType) {
+        if (sourceType.equals(targetType)) {
+            return false;
+        }
+        if (sourceType instanceof ArrayType && targetType instanceof 
ArrayType) {
+            return mayFailOnNonNullInput(((ArrayType) 
sourceType).getItemType(),
+                    ((ArrayType) targetType).getItemType());
+        }
+        if (sourceType instanceof MapType && targetType instanceof MapType) {
+            MapType sourceMap = (MapType) sourceType;
+            MapType targetMap = (MapType) targetType;
+            return mayFailOnNonNullInput(sourceMap.getKeyType(), 
targetMap.getKeyType())
+                    || mayFailOnNonNullInput(sourceMap.getValueType(), 
targetMap.getValueType());
+        }
+        if (sourceType instanceof StructType && targetType instanceof 
StructType) {
+            List<StructField> sourceFields = ((StructType) 
sourceType).getFields();
+            List<StructField> targetFields = ((StructType) 
targetType).getFields();
+            if (sourceFields.size() != targetFields.size()) {
+                return true;
+            }
+            for (int i = 0; i < sourceFields.size(); i++) {
+                StructField sourceField = sourceFields.get(i);
+                StructField targetField = targetFields.get(i);
+                if ((sourceField.isNullable() && !targetField.isNullable())
+                        || mayFailOnNonNullInput(sourceField.getDataType(), 
targetField.getDataType())) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // BE casts to a character type through to_string_batch for these 
source types.
+        // JSON and VARIANT take separate paths and are intentionally left 
conservative.
+        boolean concreteNumber = (sourceType.isIntegralType() && 
sourceType.width() > 0)
+                || sourceType.isFloatLikeType() || 
sourceType.isDecimalLikeType();
+        if (targetType.isStringLikeType()) {
+            return !(sourceType.isStringLikeType() || 
sourceType.isBooleanType() || concreteNumber
+                    || sourceType.isDateLikeType() || sourceType.isTimeType()
+                    || sourceType.isArrayType() || sourceType.isMapType() || 
sourceType.isStructType());
+        }
+        // The number-to-boolean and number-to-floating BE kernels cannot 
report a conversion
+        // failure. Precision loss (including floating overflow to infinity) 
is not a failure.
+        if (targetType.isBooleanType() || targetType.isFloatLikeType()) {
+            return !(sourceType.isBooleanType() || concreteNumber);
+        }
+        // All Doris integral types are signed; a cast to an equal or wider 
integral type fits.
+        if (sourceType.isIntegralType() && sourceType.width() > 0
+                && targetType.isIntegralType() && targetType.width() > 0) {
+            return sourceType.width() > targetType.width();
+        }
+        if (sourceType.isBooleanType() && targetType.isIntegralType() && 
targetType.width() > 0) {
+            return false;
+        }
+        return true;
+    }
+
     @Override
     public Cast withChildren(List<Expression> children) {
         Preconditions.checkArgument(children.size() == 1);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CastTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CastTest.java
index 7d0ee0f1f26..89f1a9eb610 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CastTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/CastTest.java
@@ -36,8 +36,11 @@ import org.apache.doris.nereids.types.IPv6Type;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.JsonType;
 import org.apache.doris.nereids.types.LargeIntType;
+import org.apache.doris.nereids.types.MapType;
 import org.apache.doris.nereids.types.SmallIntType;
 import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
 import org.apache.doris.nereids.types.TimeStampNsType;
 import org.apache.doris.nereids.types.TimeStampTzType;
 import org.apache.doris.nereids.types.TimeV2Type;
@@ -755,4 +758,66 @@ public class CastTest {
             Assertions.assertTrue(cast.nullable());
         }
     }
+
+    @Test
+    public void testMayFailOnNonNullInputForScalarCasts() {
+        
Assertions.assertFalse(Cast.mayFailOnNonNullInput(IntegerType.INSTANCE, 
IntegerType.INSTANCE));
+        
Assertions.assertFalse(Cast.mayFailOnNonNullInput(IntegerType.INSTANCE, 
BigIntType.INSTANCE));
+        
Assertions.assertFalse(Cast.mayFailOnNonNullInput(BooleanType.INSTANCE, 
IntegerType.INSTANCE));
+        
Assertions.assertFalse(Cast.mayFailOnNonNullInput(LargeIntType.INSTANCE, 
FloatType.INSTANCE));
+        Assertions.assertFalse(Cast.mayFailOnNonNullInput(DoubleType.INSTANCE, 
BooleanType.INSTANCE));
+        Assertions.assertFalse(Cast.mayFailOnNonNullInput(DateType.INSTANCE, 
StringType.INSTANCE));
+        
Assertions.assertFalse(Cast.mayFailOnNonNullInput(VarcharType.SYSTEM_DEFAULT, 
StringType.INSTANCE));
+
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(BigIntType.INSTANCE, 
IntegerType.INSTANCE));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(DoubleType.INSTANCE, 
IntegerType.INSTANCE));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(StringType.INSTANCE, 
IntegerType.INSTANCE));
+        
Assertions.assertTrue(Cast.mayFailOnNonNullInput(TimeStampNsType.INSTANCE, 
DateTimeV2Type.MAX));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(JsonType.INSTANCE, 
StringType.INSTANCE));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(VariantType.INSTANCE, 
StringType.INSTANCE));
+
+        Cast safeCast = new Cast(new SlotReference("slot", 
IntegerType.INSTANCE, true), BigIntType.INSTANCE);
+        Assertions.assertTrue(safeCast.nullable());
+        Assertions.assertFalse(safeCast.mayFailOnNonNullInput());
+        TryCast failingTryCast = new TryCast(new SlotReference("slot", 
StringType.INSTANCE, false),
+                IntegerType.INSTANCE);
+        Assertions.assertTrue(failingTryCast.mayFailOnNonNullInput());
+    }
+
+    @Test
+    public void testMayFailOnNonNullInputForNestedCasts() {
+        ArrayType integers = ArrayType.of(IntegerType.INSTANCE);
+        ArrayType bigInts = ArrayType.of(BigIntType.INSTANCE);
+        ArrayType strings = ArrayType.of(StringType.INSTANCE);
+        Assertions.assertFalse(Cast.mayFailOnNonNullInput(integers, bigInts));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(strings, integers));
+        // The outer ARRAY can stay non-null even when an element cast fails.
+        Assertions.assertFalse(Cast.castNullable(false, strings, integers));
+
+        MapType safeSourceMap = MapType.of(IntegerType.INSTANCE, integers);
+        MapType safeTargetMap = MapType.of(BigIntType.INSTANCE, bigInts);
+        Assertions.assertFalse(Cast.mayFailOnNonNullInput(safeSourceMap, 
safeTargetMap));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(
+                MapType.of(StringType.INSTANCE, integers), safeTargetMap));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(
+                MapType.of(IntegerType.INSTANCE, strings), safeTargetMap));
+
+        StructType safeSourceStruct = new StructType(ImmutableList.of(
+                new StructField("metric", IntegerType.INSTANCE, false, ""),
+                new StructField("attributes", safeSourceMap, true, "")));
+        StructType safeTargetStruct = new StructType(ImmutableList.of(
+                new StructField("metric", BigIntType.INSTANCE, false, ""),
+                new StructField("attributes", safeTargetMap, true, "")));
+        Assertions.assertFalse(Cast.mayFailOnNonNullInput(safeSourceStruct, 
safeTargetStruct));
+        StructType failingSourceStruct = new StructType(ImmutableList.of(
+                new StructField("metric", StringType.INSTANCE, false, ""),
+                new StructField("attributes", safeSourceMap, true, "")));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(failingSourceStruct, 
safeTargetStruct));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(safeSourceStruct,
+                new StructType(ImmutableList.of(new StructField("metric", 
BigIntType.INSTANCE, false, "")))));
+        Assertions.assertTrue(Cast.mayFailOnNonNullInput(safeTargetStruct,
+                new StructType(ImmutableList.of(
+                        new StructField("metric", BigIntType.INSTANCE, false, 
""),
+                        new StructField("attributes", safeTargetMap, false, 
"")))));
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to