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 bd521031bcc [fix](cast) Correct injective cast classification (#68134)
bd521031bcc is described below

commit bd521031bcc0c25d13d64324eaff61b586a6ffda
Author: morrySnow <[email protected]>
AuthorDate: Sun Sep 20 14:54:19 2026 +0800

    [fix](cast) Correct injective cast classification (#68134)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: Nereids used incomplete or overly optimistic cast
    metadata when deciding whether a cast is injective and whether its
    result can be non-nullable. This could allow DISTINCT/GROUP BY and other
    rewrites to remove or reorder casts, or make FE expect a non-nullable
    result while BE returned a nullable column.
    
    This PR:
    
    - Treats every cast involving deprecated DECIMALV2 as non-injective,
    while keeping DECIMALV2 execution-domain constants for CAST nullability
    analysis.
    - Aligns DECIMALV2-to-DECIMALV3 nullability with the BE kernel, using
    the original DECIMALV2 precision and scale for its physical
    nullable-wrapper decision.
    - Corrects CAST/TRY_CAST nullability for datetime, timestamp, decimal,
    integral, and related conversions to match BE behavior.
    - Completes and tightens injective-cast rules for numeric, date/time,
    IP, binary, and nested types, including precision/range limits and
    propagation through arrays, maps, and structs.
    - Removes unsafe complex-type-to-string injectivity assumptions because
    serialized values can be ambiguous.
    - Adds boundary coverage for injectivity and CAST/TRY_CAST nullability,
    including rewrite tests for DISTINCT/GROUP BY and UNION project
    pushdown.
    
    ### Release note
    
    Fix incorrect cast injectivity and nullability analysis in Nereids,
    especially for deprecated DECIMALV2 and nested/temporal casts.
---
 .../doris/nereids/trees/expressions/Cast.java      | 40 ++++++++---
 .../org/apache/doris/nereids/types/ArrayType.java  |  6 +-
 .../apache/doris/nereids/types/BooleanType.java    |  5 +-
 .../org/apache/doris/nereids/types/DataType.java   |  7 ++
 .../apache/doris/nereids/types/DateTimeType.java   |  9 +--
 .../apache/doris/nereids/types/DateTimeV2Type.java |  6 ++
 .../org/apache/doris/nereids/types/DateType.java   |  8 ++-
 .../org/apache/doris/nereids/types/DateV2Type.java | 12 ++++
 .../apache/doris/nereids/types/DecimalV2Type.java  | 22 +++---
 .../apache/doris/nereids/types/DecimalV3Type.java  | 21 +++++-
 .../org/apache/doris/nereids/types/FloatType.java  |  8 +++
 .../org/apache/doris/nereids/types/IPv4Type.java   |  7 ++
 .../org/apache/doris/nereids/types/IPv6Type.java   |  7 ++
 .../org/apache/doris/nereids/types/MapType.java    | 13 ++--
 .../org/apache/doris/nereids/types/NullType.java   | 11 +++
 .../org/apache/doris/nereids/types/StructType.java |  6 +-
 .../org/apache/doris/nereids/types/TimeV2Type.java | 11 +++
 .../apache/doris/nereids/types/VarBinaryType.java  |  8 +++
 .../nereids/types/coercion/CharacterType.java      |  5 +-
 .../doris/nereids/types/coercion/IntegralType.java | 27 +++++--
 .../rules/rewrite/PushProjectThroughUnionTest.java | 17 +++++
 .../rules/rewrite/SimplifyAggGroupByTest.java      |  6 +-
 .../doris/nereids/trees/expressions/CastTest.java  | 60 +++++++++++-----
 .../nereids/trees/expressions/TryCastTest.java     | 15 ++--
 .../apache/doris/nereids/types/DataTypeTest.java   | 82 ++++++++++++++++++++--
 25 files changed, 347 insertions(+), 72 deletions(-)

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 d853806c827..f31ce955b74 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
@@ -139,10 +139,14 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
                 && targetType instanceof TimeStampNsType) {
             // Temporal inputs can fail because TIMESTAMP_NS has a narrower 
signed epoch-nanos range.
             return true;
-        } else if ((childDataType.isDateTimeType() || 
childDataType.isDateTimeV2Type()
-                || childDataType.isTimeStampTzType())
-                && (targetType.isDateTimeType() || 
targetType.isDateTimeV2Type())) {
-            // datetime to datetime is always nullable
+        } else if (childDataType.isDateTimeV2Type() && 
targetType.isDateTimeV2Type()) {
+            // BE's generic datelike cast creates a nullable result for 
DATETIMEV2 scale changes:
+            // reducing scale can overflow while rounding at the maximum 
datetime boundary.
+            // Exact-type casts have already returned above.
+            return true;
+        } else if (childDataType.isTimeStampTzType() && 
targetType.isDateTimeV2Type()) {
+            // The BE TIMESTAMPTZ -> DATETIMEV2 kernel can fail while 
converting the instant in the
+            // session time zone, and its non-strict implementation returns a 
nullable column.
             return true;
         } else if ((childDataType.isDateTimeV2Type() || 
childDataType.isTimeStampNsType())
                 && targetType.isTimeStampTzType()) {
@@ -170,7 +174,7 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
                 return childDataType.isSmallIntType() && 
targetType.isTinyIntType();
             } else if (targetType.isDecimalLikeType()) {
                 // Integral to decimal
-                int range = targetType.isDecimalV2Type() ? ((DecimalV2Type) 
targetType).getRange()
+                int range = targetType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_RANGE
                         : ((DecimalV3Type) targetType).getRange();
                 if (childDataType.isTinyIntType() && range < 
TinyIntType.RANGE) {
                     return true;
@@ -196,7 +200,7 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
             if (targetType.isIntegralType()) {
                 int range = 0;
                 if (childDataType.isDecimalV2Type()) {
-                    range = ((DecimalV2Type) childDataType).getRange();
+                    range = DecimalV2Type.EXECUTION_RANGE;
                 } else {
                     range = ((DecimalV3Type) childDataType).getRange();
                 }
@@ -215,9 +219,23 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
                 return targetType.isBigIntType() && range >= BigIntType.RANGE;
             } else if (targetType.isDecimalLikeType()) {
                 // Decimal to decimal
-                int targetRange = targetType.isDecimalV2Type() ? 
((DecimalV2Type) targetType).getRange()
+                if (childDataType.isDecimalV2Type() && 
targetType.isDecimalV3Type()) {
+                    DecimalV2Type sourceDecimal = (DecimalV2Type) 
childDataType;
+                    DecimalV3Type targetDecimal = (DecimalV3Type) targetType;
+                    int sourceRange = sourceDecimal.getRange();
+                    int targetRange = targetDecimal.getRange();
+                    // DECIMALV2 values are evaluated as DECIMAL(27, 9), but 
BE's D2-to-D3
+                    // specialization deliberately uses the source type's 
original precision and
+                    // scale to decide whether its physical result is 
ColumnNullable. It applies
+                    // this wrapper in both strict and non-strict modes. 
Mirror that decision here;
+                    // otherwise VExpr rejects the nullable BE column against 
a non-nullable FE slot.
+                    return sourceRange > targetRange
+                            || (sourceRange == targetRange
+                                && sourceDecimal.getScale() > 
targetDecimal.getScale());
+                }
+                int targetRange = targetType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_RANGE
                         : ((DecimalV3Type) targetType).getRange();
-                int sourceRange = childDataType.isDecimalV2Type() ? 
((DecimalV2Type) childDataType).getRange()
+                int sourceRange = childDataType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_RANGE
                         : ((DecimalV3Type) childDataType).getRange();
                 if (sourceRange > targetRange) {
                     return true;
@@ -228,9 +246,9 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
                 // When source range == target range, if source precision is 
larger than target precision,
                 // it is possible to be null when fraction part overflow.
                 // e.g. decimal(3, 2) to decimal(2, 1), 9.99 to decimal(2, 1) 
overflow, result is null.
-                int targetPrecision = targetType.isDecimalV2Type() ? 
((DecimalV2Type) targetType).getPrecision()
+                int targetPrecision = targetType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_PRECISION
                         : ((DecimalV3Type) targetType).getPrecision();
-                int sourcePrecision = childDataType.isDecimalV2Type() ? 
((DecimalV2Type) childDataType).getPrecision()
+                int sourcePrecision = childDataType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_PRECISION
                         : ((DecimalV3Type) childDataType).getPrecision();
                 return sourcePrecision > targetPrecision;
             } else if (targetType.isTimeType() || targetType.isDateLikeType()) 
{
@@ -239,7 +257,7 @@ public class Cast extends Expression implements 
UnaryExpression, Monotonic {
             }
         } else if (childDataType.isBooleanType() && 
targetType.isDecimalLikeType()) {
             // Boolean to decimal
-            return (targetType.isDecimalV2Type() ? ((DecimalV2Type) 
targetType).getRange()
+            return (targetType.isDecimalV2Type() ? 
DecimalV2Type.EXECUTION_RANGE
                     : ((DecimalV3Type) targetType).getRange()) < 1;
         } else if (childDataType.isJsonType() && !targetType.isJsonType()) {
             // Json to other type is always nullable
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/ArrayType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/ArrayType.java
index 015690e6434..998a2c232af 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/ArrayType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/ArrayType.java
@@ -18,7 +18,6 @@
 package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Type;
-import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.ComplexDataType;
 
 import java.util.Objects;
@@ -63,7 +62,10 @@ public class ArrayType extends DataType implements 
ComplexDataType, NestedColumn
         if (target instanceof ArrayType) {
             return itemType.isInjectiveCastTo(((ArrayType) target).itemType);
         }
-        return target instanceof CharacterType;
+        // BE's DataTypeArraySerDe::to_string writes element text separated by 
", " without
+        // escaping delimiter-like content. Therefore different arrays can 
have the same text,
+        // for example ARRAY('a", "b') and ARRAY('a', 'b'), so ARRAY -> STRING 
is not injective.
+        return false;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/BooleanType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/BooleanType.java
index 708801a883f..8117e56191d 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/BooleanType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/BooleanType.java
@@ -33,8 +33,11 @@ public class BooleanType extends PrimitiveType {
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
+        if (target instanceof DecimalV2Type) {
+            // DECIMALV2 is deprecated, so every cast involving it is 
conservatively non-injective.
+            return false;
+        }
         return target instanceof BooleanType || target.isIntegralType() || 
target.isFloatLikeType()
-                || (target instanceof DecimalV2Type && ((DecimalV2Type) 
target).getRange() >= 1)
                 || (target instanceof DecimalV3Type && ((DecimalV3Type) 
target).getRange() >= 1)
                 || target.isStringLikeType();
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
index 8fd5b454b11..fb82bef3e02 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
@@ -833,6 +833,13 @@ public abstract class DataType {
 
     public abstract int width();
 
+    /**
+     * Return whether a legal cast to {@code target} preserves distinctness: 
two values that are
+     * distinct under this type's equality semantics must not become equal 
after the cast. Rewrite
+     * rules use this property when moving casts across DISTINCT or GROUP BY, 
so implementations
+     * must account for rounding, truncation, ambiguous text formatting, and 
special equality
+     * classes such as floating-point NaNs and signed zero.
+     */
     public boolean isInjectiveCastTo(DataType target) {
         return this.equals(target);
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeType.java
index a93bfda0364..03410490c24 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeType.java
@@ -48,10 +48,11 @@ public class DateTimeType extends DateLikeType {
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
-        if (target instanceof DateTimeType || target instanceof DateTimeV2Type 
|| target instanceof CharacterType) {
-            return true;
-        }
-        return false;
+        // BE converts DATETIME to YYYYMMDDHHMMSS for numeric targets. This 
14-digit integer fits
+        // in BIGINT and remains exact in DOUBLE because it is below 2^53.
+        return target instanceof DateTimeType || target instanceof 
DateTimeV2Type
+                || target instanceof BigIntType || target instanceof 
LargeIntType
+                || target instanceof DoubleType || target instanceof 
CharacterType;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeV2Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeV2Type.java
index 13097339554..0c600ff18a8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeV2Type.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateTimeV2Type.java
@@ -138,6 +138,12 @@ public class DateTimeV2Type extends DateLikeType 
implements ScaleTimeType {
         if (target instanceof DateTimeType) {
             return this.scale == 0;
         }
+        // Numeric datetime casts omit the fractional part. They are injective 
only when the source
+        // scale is zero; the resulting 14-digit integer fits in BIGINT and 
exactly in DOUBLE.
+        if (scale == 0 && (target instanceof BigIntType || target instanceof 
LargeIntType
+                || target instanceof DoubleType)) {
+            return true;
+        }
         return target instanceof CharacterType;
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateType.java
index c6ce702ebe7..7a293977a65 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateType.java
@@ -48,7 +48,13 @@ public class DateType extends DateLikeType {
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
-        return target instanceof DateType || target instanceof DateV2Type || 
target instanceof CharacterType;
+        // BE converts DATE to YYYYMMDD for numeric targets. The largest value 
fits in INT and is
+        // below 2^53, so INT and wider integers as well as DOUBLE preserve 
every DATE exactly.
+        return target instanceof DateType || target instanceof DateV2Type
+                || target instanceof DateTimeType || target instanceof 
DateTimeV2Type
+                || target instanceof IntegerType || target instanceof 
BigIntType
+                || target instanceof LargeIntType || target instanceof 
DoubleType
+                || target instanceof CharacterType;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateV2Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateV2Type.java
index 2acac334304..95f0e37bb28 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateV2Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DateV2Type.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.DateLikeType;
 
 import java.time.DateTimeException;
@@ -36,6 +37,17 @@ public class DateV2Type extends DateLikeType {
     private DateV2Type() {
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        // BE uses the same calendar domain for DATEV2 and DATE and converts 
DATEV2 to YYYYMMDD for
+        // numeric targets. That value fits in INT and is represented exactly 
by DOUBLE.
+        return target instanceof DateV2Type || target instanceof DateType
+                || target instanceof DateTimeType || target instanceof 
DateTimeV2Type
+                || target instanceof IntegerType || target instanceof 
BigIntType
+                || target instanceof LargeIntType || target instanceof 
DoubleType
+                || target instanceof CharacterType;
+    }
+
     @Override
     public Type toCatalogDataType() {
         return Type.DATEV2;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV2Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV2Type.java
index b055172f262..58cd89ff960 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV2Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV2Type.java
@@ -21,7 +21,6 @@ import org.apache.doris.catalog.PrimitiveType;
 import org.apache.doris.catalog.ScalarType;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.Config;
-import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.FractionalType;
 
 import com.google.common.base.Preconditions;
@@ -38,6 +37,11 @@ public class DecimalV2Type extends FractionalType {
 
     public static int MAX_PRECISION = 27;
     public static int MAX_SCALE = 9;
+    // BE stores every DECIMALV2 as DECIMAL(27, 9) while evaluating 
expressions. The declared
+    // precision and scale are retained only as original schema metadata, 
primarily for formatting.
+    public static final int EXECUTION_PRECISION = 27;
+    public static final int EXECUTION_SCALE = 9;
+    public static final int EXECUTION_RANGE = EXECUTION_PRECISION - 
EXECUTION_SCALE;
     public static final DecimalV2Type SYSTEM_DEFAULT = new 
DecimalV2Type(MAX_PRECISION, MAX_SCALE, true);
     public static final DecimalV2Type SYSTEM_DEFAULT_NOT_CONVERSION =
             new DecimalV2Type(MAX_PRECISION, MAX_SCALE, false);
@@ -162,15 +166,13 @@ public class DecimalV2Type extends FractionalType {
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
-        if (target instanceof DecimalV2Type) {
-            DecimalV2Type decimalV2Type = (DecimalV2Type) target;
-            return decimalV2Type.getRange() >= this.getRange() && 
decimalV2Type.getScale() >= this.getScale();
-        }
-        if (target instanceof DecimalV3Type) {
-            DecimalV3Type decimalV3Type = (DecimalV3Type) target;
-            return decimalV3Type.getRange() >= this.getRange() && 
decimalV3Type.getScale() >= this.getScale();
-        }
-        return target instanceof CharacterType;
+        // DECIMALV2 is a deprecated legacy type. Its declared precision and 
scale do not always
+        // match BE's execution domain, and casts between DECIMALV2 and newer 
decimal types are not
+        // supported consistently. Conservatively keep every cast with a 
DECIMALV2 source out of
+        // injective-only rewrites, including apparent identity or widening 
casts. This prevents an
+        // optimizer rewrite from removing or reordering a legacy cast whose 
execution can fail or
+        // merge values.
+        return false;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV3Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV3Type.java
index b366568cb35..2a04051ffe8 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV3Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DecimalV3Type.java
@@ -24,6 +24,7 @@ import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.exceptions.NotSupportedException;
 import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.FractionalType;
+import org.apache.doris.nereids.types.coercion.IntegralType;
 import org.apache.doris.qe.ConnectContext;
 import org.apache.doris.qe.SessionVariable;
 
@@ -218,13 +219,29 @@ public class DecimalV3Type extends FractionalType {
     @Override
     public boolean isInjectiveCastTo(DataType target) {
         if (target instanceof DecimalV2Type) {
-            DecimalV2Type decimalV2Type = (DecimalV2Type) target;
-            return decimalV2Type.getRange() >= this.getRange() && 
decimalV2Type.getScale() >= this.getScale();
+            // DECIMALV2 is deprecated, and BE does not support every 
DECIMALV3-to-DECIMALV2 cast
+            // accepted by FE. Keep all such casts non-injective so rewrites 
preserve the cast and
+            // its original execution behavior.
+            return false;
         }
         if (target instanceof DecimalV3Type) {
             DecimalV3Type decimalV3Type = (DecimalV3Type) target;
             return decimalV3Type.getRange() >= this.getRange() && 
decimalV3Type.getScale() >= this.getScale();
         }
+        // An integral target discards the fractional part, so it is injective 
only for scale zero.
+        // The strict range bound also leaves room for the asymmetric negative 
endpoint of a signed
+        // integer. Binary floating-point casts round, but adjacent values in 
a DECIMAL(P, S) domain
+        // remain distinguishable when P <= 7 for FLOAT or P <= 15 for DOUBLE. 
Those conservative
+        // decimal-digit limits hold for every scale, not just for integral 
decimals.
+        if (scale == 0 && target instanceof IntegralType) {
+            return getRange() < ((IntegralType) target).range();
+        }
+        if (target instanceof FloatType) {
+            return precision <= 7;
+        }
+        if (target instanceof DoubleType) {
+            return precision <= 15;
+        }
         return target instanceof CharacterType;
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/FloatType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/FloatType.java
index 394ba7be443..10d9b06ecf5 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/FloatType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/FloatType.java
@@ -31,6 +31,14 @@ public class FloatType extends FractionalType {
     private FloatType() {
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        // Every binary32 value is exactly representable as binary64. Doris 
normalizes signed zero
+        // and NaN payloads before DISTINCT and aggregation, and widening 
preserves those equivalence
+        // classes. Do not include character targets: their formatting can 
distinguish signed zero.
+        return target instanceof FloatType || target instanceof DoubleType;
+    }
+
     @Override
     public Type toCatalogDataType() {
         return Type.FLOAT;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv4Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv4Type.java
index c86377b1010..f1909378d16 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv4Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv4Type.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.PrimitiveType;
 
 /**
@@ -32,6 +33,12 @@ public class IPv4Type extends PrimitiveType {
     private IPv4Type() {
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        // BE maps all 32 IPv4 bits into IPv6 and prints a canonical address 
for string casts.
+        return target instanceof IPv4Type || target instanceof IPv6Type || 
target instanceof CharacterType;
+    }
+
     @Override
     public Type toCatalogDataType() {
         return Type.IPV4;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv6Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv6Type.java
index ece20835a5d..39115b5c6b1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv6Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/IPv6Type.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.PrimitiveType;
 
 /**
@@ -32,6 +33,12 @@ public class IPv6Type extends PrimitiveType {
     private IPv6Type() {
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        // IPv6Value::to_string emits a canonical textual representation of 
all 128 address bits.
+        return target instanceof IPv6Type || target instanceof CharacterType;
+    }
+
     @Override
     public Type toCatalogDataType() {
         return Type.IPV6;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/MapType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/MapType.java
index fc6e9ba2f94..8e00be04528 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/MapType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/MapType.java
@@ -19,7 +19,6 @@ package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.Type;
 import org.apache.doris.nereids.annotation.Developing;
-import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.ComplexDataType;
 
 import java.util.Objects;
@@ -66,11 +65,17 @@ public class MapType extends DataType implements 
ComplexDataType, NestedColumnPr
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
-        if (target instanceof MapType) {
+        // BE bypasses the MAP cast kernel for an exact type match. Every 
non-identity MAP cast,
+        // however, runs ColumnMap::deduplicate_keys() after converting its 
children. Two distinct
+        // source maps that differ only by duplicate entries can consequently 
produce the same
+        // result even when both child conversions are injective. Also check 
the children for an
+        // exact match so conservative leaf policies, such as deprecated 
DECIMALV2, are preserved.
+        if (equals(target)) {
             MapType mapType = (MapType) target;
-            return keyType.isInjectiveCastTo(mapType.keyType) && 
valueType.isInjectiveCastTo(mapType.valueType);
+            return keyType.isInjectiveCastTo(mapType.keyType)
+                    && valueType.isInjectiveCastTo(mapType.valueType);
         }
-        return target instanceof CharacterType;
+        return false;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/NullType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/NullType.java
index 7fdbc5a17fa..9369f99a674 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/NullType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/NullType.java
@@ -31,6 +31,17 @@ public class NullType extends PrimitiveType {
     private NullType() {
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        if (target instanceof DecimalV2Type) {
+            // DECIMALV2 is deprecated, so every cast involving it is 
conservatively non-injective.
+            return false;
+        }
+        // NULL is the only value in this domain. CheckCast permits it to cast 
to every target type,
+        // and nullable propagation keeps the result NULL, so no two distinct 
values can collapse.
+        return true;
+    }
+
     @Override
     public Type toCatalogDataType() {
         return Type.NULL;
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
index 8f2893ec55f..0b61bafda48 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
@@ -20,7 +20,6 @@ package org.apache.doris.nereids.types;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.nereids.annotation.Developing;
 import org.apache.doris.nereids.exceptions.AnalysisException;
-import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.ComplexDataType;
 
 import com.google.common.collect.ImmutableList;
@@ -117,7 +116,10 @@ public class StructType extends DataType implements 
ComplexDataType, NestedColum
             }
             return true;
         }
-        return target instanceof CharacterType;
+        // DataTypeStructSerDe::to_string concatenates field text with 
structural delimiters but
+        // does not escape delimiter-like field content. The resulting STRING 
is not a unique
+        // representation of the source STRUCT and must not be treated as an 
injective cast.
+        return false;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/TimeV2Type.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/TimeV2Type.java
index af758c02cb5..4707a024f1f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/TimeV2Type.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/TimeV2Type.java
@@ -55,6 +55,17 @@ public class TimeV2Type extends PrimitiveType implements 
RangeScalable, ScaleTim
             TimeV2Type timeV2Type = (TimeV2Type) target;
             return timeV2Type.scale >= scale;
         }
+        // Scale-zero values are one second (one million microseconds) apart. 
Even near the largest
+        // supported TIMEV2 value, a binary32 ULP is only 262144 microseconds, 
so FLOAT cannot merge
+        // adjacent values. At scale one or greater, the spacing can be 
smaller than that ULP.
+        if (target instanceof FloatType) {
+            return scale == 0;
+        }
+        // BE stores TIMEV2 as an integral microsecond count in a DOUBLE. Its 
bounded domain
+        // is well below 2^53, so BIGINT, LARGEINT, and DOUBLE preserve it 
exactly.
+        if (target instanceof BigIntType || target instanceof LargeIntType || 
target instanceof DoubleType) {
+            return true;
+        }
         return target instanceof CharacterType;
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/VarBinaryType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/VarBinaryType.java
index a06b7c20053..7d1d481c303 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/VarBinaryType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/VarBinaryType.java
@@ -19,6 +19,7 @@ package org.apache.doris.nereids.types;
 
 import org.apache.doris.catalog.ScalarType;
 import org.apache.doris.catalog.Type;
+import org.apache.doris.nereids.types.coercion.CharacterType;
 import org.apache.doris.nereids.types.coercion.PrimitiveType;
 
 import com.google.common.base.Preconditions;
@@ -54,6 +55,13 @@ public class VarBinaryType extends PrimitiveType {
         return new VarBinaryType(len);
     }
 
+    @Override
+    public boolean isInjectiveCastTo(DataType target) {
+        // VARBINARY length is schema metadata only: BE's DataTypeVarbinary 
equality ignores it and
+        // does not truncate values. Its string cast also copies the raw bytes 
without formatting.
+        return target instanceof VarBinaryType || target instanceof 
CharacterType;
+    }
+
     @Override
     public Type toCatalogDataType() {
         ScalarType catalogDataType = ScalarType.createVarbinaryType(len);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/CharacterType.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/CharacterType.java
index 3d8590534f5..c799766a510 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/CharacterType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/CharacterType.java
@@ -20,6 +20,7 @@ package org.apache.doris.nereids.types.coercion;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.VarBinaryType;
 
 /**
  * Abstract type for all characters type in Nereids.
@@ -44,7 +45,9 @@ public abstract class CharacterType extends PrimitiveType {
 
     @Override
     public boolean isInjectiveCastTo(DataType target) {
-        return target instanceof CharacterType;
+        // BE deserializes STRING into VARBINARY by copying the original 
bytes, including embedded
+        // zeroes; the declared VARBINARY length is not enforced by the 
execution data type.
+        return target instanceof CharacterType || target instanceof 
VarBinaryType;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/IntegralType.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/IntegralType.java
index fe625fa34bc..7b36fd79945 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/IntegralType.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/coercion/IntegralType.java
@@ -19,8 +19,12 @@ package org.apache.doris.nereids.types.coercion;
 
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.DecimalV2Type;
 import org.apache.doris.nereids.types.DecimalV3Type;
-import org.apache.doris.nereids.types.LargeIntType;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.FloatType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.SmallIntType;
 
 import org.apache.commons.lang3.NotImplementedException;
 
@@ -51,10 +55,23 @@ public class IntegralType extends NumericType {
         if (target instanceof IntegralType) {
             return this.equals(target) || ((IntegralType) 
target).widerThan(this);
         }
-        if (target instanceof DecimalV3Type && !(this instanceof 
LargeIntType)) {
-            DecimalV3Type other = (DecimalV3Type) target;
-            DecimalV3Type self = DecimalV3Type.forType(this);
-            return other.getRange() >= self.getRange();
+        // IEEE-754 FLOAT and DOUBLE have 24 and 53 bits of integer precision 
respectively.
+        // Consequently every TINYINT/SMALLINT value is exact in FLOAT, and 
every value through
+        // INT is exact in DOUBLE; wider integer domains contain values that 
would collide.
+        if (target instanceof FloatType) {
+            return range() <= SmallIntType.RANGE;
+        }
+        if (target instanceof DoubleType) {
+            return range() <= IntegerType.RANGE;
+        }
+        if (target instanceof DecimalV2Type) {
+            // DECIMALV2 is deprecated, so every cast involving it is 
conservatively non-injective.
+            return false;
+        }
+        // Decimal casts preserve an integer exactly when the target has 
enough integer digits.
+        // This also covers LARGEINT -> DECIMAL256 when a precision of at 
least 39 is available.
+        if (target instanceof DecimalV3Type) {
+            return ((DecimalV3Type) target).getRange() >= range();
         }
         return target instanceof CharacterType;
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushProjectThroughUnionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushProjectThroughUnionTest.java
index a877348491c..3f981f71e5f 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushProjectThroughUnionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/PushProjectThroughUnionTest.java
@@ -27,10 +27,12 @@ import org.apache.doris.nereids.trees.plans.Plan;
 import org.apache.doris.nereids.trees.plans.algebra.SetOperation.Qualifier;
 import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
 import org.apache.doris.nereids.trees.plans.logical.LogicalUnion;
+import org.apache.doris.nereids.types.ArrayType;
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.DateTimeType;
 import org.apache.doris.nereids.types.DateType;
 import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
 import org.apache.doris.nereids.util.MemoTestUtils;
 import org.apache.doris.nereids.util.PlanChecker;
 
@@ -138,6 +140,21 @@ public class PushProjectThroughUnionTest {
         LogicalUnion unionDistinctWithUnsafeCast = new 
LogicalUnion(Qualifier.DISTINCT,
                 ImmutableList.of(dateTimeOutput), ImmutableList.of(), 
ImmutableList.of(), false, ImmutableList.of());
         
Assertions.assertFalse(PushProjectThroughUnion.canPushProject(unsafeProjects, 
unionDistinctWithUnsafeCast));
+
+        SlotReference arrayOutput = new SlotReference(new ExprId(12), "a",
+                ArrayType.of(StringType.INSTANCE), true, ImmutableList.of());
+        Alias arrayToStringProject = new Alias(new ExprId(102),
+                new Cast(arrayOutput, StringType.INSTANCE), "s");
+        ImmutableList<NamedExpression> arrayProjects = 
ImmutableList.of(arrayToStringProject);
+        LogicalUnion arrayUnionAll = new LogicalUnion(Qualifier.ALL,
+                ImmutableList.of(arrayOutput), ImmutableList.of(), 
ImmutableList.of(), false, ImmutableList.of());
+        
Assertions.assertTrue(PushProjectThroughUnion.canPushProject(arrayProjects, 
arrayUnionAll));
+
+        // ARRAY text serialization is ambiguous: ARRAY('a", "b') and 
ARRAY('a', 'b') both
+        // produce ["a", "b"]. Pushing this cast below DISTINCT would collapse 
two input rows.
+        LogicalUnion arrayUnionDistinct = new LogicalUnion(Qualifier.DISTINCT,
+                ImmutableList.of(arrayOutput), ImmutableList.of(), 
ImmutableList.of(), false, ImmutableList.of());
+        
Assertions.assertFalse(PushProjectThroughUnion.canPushProject(arrayProjects, 
arrayUnionDistinct));
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/SimplifyAggGroupByTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/SimplifyAggGroupByTest.java
index f6a822d5b65..52fcff70e26 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/SimplifyAggGroupByTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/SimplifyAggGroupByTest.java
@@ -306,10 +306,12 @@ class SimplifyAggGroupByTest implements 
MemoPatternMatchSupported {
     }
 
     @Test
-    void testCrossFamilyRejected() {
+    void testExactCrossFamilyWidening() {
+        
Assertions.assertTrue(TinyIntType.INSTANCE.isInjectiveCastTo(FloatType.INSTANCE));
         
Assertions.assertFalse(IntegerType.INSTANCE.isInjectiveCastTo(FloatType.INSTANCE));
         
Assertions.assertFalse(FloatType.INSTANCE.isInjectiveCastTo(IntegerType.INSTANCE));
-        
Assertions.assertFalse(IntegerType.INSTANCE.isInjectiveCastTo(DoubleType.INSTANCE));
+        
Assertions.assertTrue(IntegerType.INSTANCE.isInjectiveCastTo(DoubleType.INSTANCE));
+        
Assertions.assertFalse(BigIntType.INSTANCE.isInjectiveCastTo(DoubleType.INSTANCE));
     }
 
     // ========== tests for canExtractSlot ==========
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 7fcd5477131..7d0ee0f1f26 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
@@ -67,13 +67,13 @@ public class CastTest {
             cast = new Cast(child, TinyIntType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // BE uses DECIMAL(27, 9) for every DECIMALV2 target, so BOOLEAN 
always fits.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             child = new SlotReference("slot", BooleanType.INSTANCE, false);
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(2, 1));
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(2, 2));
-            Assertions.assertTrue(cast.nullable());
+            Assertions.assertFalse(cast.nullable());
         }
     }
 
@@ -89,13 +89,13 @@ public class CastTest {
             cast = new Cast(child, BooleanType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // BE uses DECIMAL(27, 9) for every DECIMALV2 target, so TINYINT 
always fits.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             child = new SlotReference("slot", TinyIntType.INSTANCE, false);
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(4, 1));
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(4, 2));
-            Assertions.assertTrue(cast.nullable());
+            Assertions.assertFalse(cast.nullable());
 
             // To date is always nullable
             cast = new Cast(child, DateType.INSTANCE);
@@ -123,13 +123,13 @@ public class CastTest {
             cast = new Cast(child, BooleanType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // BE uses DECIMAL(27, 9) for every DECIMALV2 target, so SMALLINT 
always fits.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             child = new SlotReference("slot", SmallIntType.INSTANCE, false);
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(6, 1));
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(6, 2));
-            Assertions.assertTrue(cast.nullable());
+            Assertions.assertFalse(cast.nullable());
 
             // To date is always nullable
             cast = new Cast(child, DateType.INSTANCE);
@@ -161,13 +161,13 @@ public class CastTest {
             cast = new Cast(child, IntegerType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // BE uses DECIMAL(27, 9) for every DECIMALV2 target, so INT 
always fits.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             child = new SlotReference("slot", IntegerType.INSTANCE, false);
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(11, 1));
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(11, 2));
-            Assertions.assertTrue(cast.nullable());
+            Assertions.assertFalse(cast.nullable());
 
             // To date is always nullable
             cast = new Cast(child, DateType.INSTANCE);
@@ -201,11 +201,11 @@ public class CastTest {
             cast = new Cast(child, LargeIntType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // DECIMALV2 has 18 integer digits in BE, so it cannot hold the 
full BIGINT domain.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             child = new SlotReference("slot", BigIntType.INSTANCE, false);
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(20, 1));
-            Assertions.assertFalse(cast.nullable());
+            Assertions.assertTrue(cast.nullable());
             cast = new Cast(child, DecimalV2Type.createDecimalV2Type(20, 2));
             Assertions.assertTrue(cast.nullable());
 
@@ -244,7 +244,7 @@ public class CastTest {
             cast = new Cast(child, DoubleType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
 
-            // When strict mode is false, return nullable when decimal range < 
1
+            // When strict mode is false, return nullable when decimal range 
is too small.
             
mockedSessionVariable.when(SessionVariable::enableStrictCast).thenReturn(false);
             ConnectContext context = new ConnectContext();
             context.getSessionVariable().enableDecimal256 = true;
@@ -386,19 +386,19 @@ public class CastTest {
             // To integer
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(4, 2), false);
             cast = new Cast(child, TinyIntType.INSTANCE);
-            Assertions.assertFalse(cast.nullable());
+            Assertions.assertTrue(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(5, 2), false);
             cast = new Cast(child, TinyIntType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(6, 2), false);
             cast = new Cast(child, SmallIntType.INSTANCE);
-            Assertions.assertFalse(cast.nullable());
+            Assertions.assertTrue(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(7, 2), false);
             cast = new Cast(child, SmallIntType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(11, 2), false);
             cast = new Cast(child, IntegerType.INSTANCE);
-            Assertions.assertFalse(cast.nullable());
+            Assertions.assertTrue(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(12, 2), false);
             cast = new Cast(child, IntegerType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
@@ -407,7 +407,7 @@ public class CastTest {
             Assertions.assertFalse(cast.nullable());
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(21, 2), false);
             cast = new Cast(child, BigIntType.INSTANCE);
-            Assertions.assertTrue(cast.nullable());
+            Assertions.assertFalse(cast.nullable());
             child = new SlotReference("slot", 
DecimalV3Type.createDecimalV3Type(40, 2), false);
             cast = new Cast(child, LargeIntType.INSTANCE);
             Assertions.assertFalse(cast.nullable());
@@ -420,6 +420,22 @@ public class CastTest {
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DecimalV3Type.createDecimalV3Type(40, 2));
             Assertions.assertTrue(cast.nullable());
+            child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(7, 0), false);
+            cast = new Cast(child, DecimalV2Type.createDecimalV2Type(1, 1));
+            Assertions.assertFalse(cast.nullable());
+            cast = new Cast(child, DecimalV3Type.createDecimalV3Type(27, 9));
+            Assertions.assertFalse(cast.nullable());
+            cast = new Cast(child, DecimalV3Type.createDecimalV3Type(26, 9));
+            Assertions.assertFalse(cast.nullable());
+
+            // The D2-to-D3 BE kernel decides its physical nullable wrapper 
from DECIMALV2's
+            // original schema precision and scale, independently of 
strict-cast behavior.
+            child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(27, 0), false);
+            DecimalV3Type decimalV3Target = 
DecimalV3Type.createDecimalV3Type(19, 0);
+            cast = new Cast(child, decimalV3Target, false, false);
+            Assertions.assertTrue(cast.nullable());
+            cast = new Cast(child, decimalV3Target, false, true);
+            Assertions.assertTrue(cast.nullable());
             // To date is always nullable
             cast = new Cast(child, DateType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
@@ -444,11 +460,23 @@ public class CastTest {
             // An exact-type cast cannot fail even though conversions between 
datetime types can.
             Assertions.assertFalse(cast.nullable());
             cast = new Cast(child, DateTimeV2Type.SYSTEM_DEFAULT);
-            Assertions.assertTrue(cast.nullable());
+            // The BE DATETIME -> DATETIMEV2 branch directly converts valid 
calendar fields.
+            Assertions.assertFalse(cast.nullable());
             child = new SlotReference("slot", DateTimeV2Type.SYSTEM_DEFAULT, 
false);
             cast = new Cast(child, DateTimeV2Type.SYSTEM_DEFAULT);
             Assertions.assertFalse(cast.nullable());
+
+            child = new SlotReference("slot", DateTimeV2Type.of(3), false);
             cast = new Cast(child, DateTimeType.INSTANCE);
+            // BE handles DATETIMEV2 -> DATETIME directly even when it 
discards fractional seconds;
+            // unlike a DATETIMEV2 scale change, this path does not wrap its 
result in Nullable.
+            Assertions.assertFalse(cast.nullable());
+            cast = new Cast(child, DateTimeV2Type.MAX);
+            // BE wraps every non-identity DATETIMEV2 scale conversion in a 
nullable column.
+            Assertions.assertTrue(cast.nullable());
+
+            child = new SlotReference("slot", TimeStampTzType.MAX, false);
+            cast = new Cast(child, DateTimeV2Type.MAX);
             Assertions.assertTrue(cast.nullable());
         }
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/TryCastTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/TryCastTest.java
index 6333f4517f9..ec027cfd68a 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/TryCastTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/TryCastTest.java
@@ -109,7 +109,7 @@ public class TryCastTest {
             Assertions.assertFalse(cast.originCastNullable());
             cast = new TryCast(child, DecimalV2Type.createDecimalV2Type(6, 2));
             Assertions.assertTrue(cast.nullable());
-            Assertions.assertTrue(cast.originCastNullable());
+            Assertions.assertFalse(cast.originCastNullable());
         }
     }
 
@@ -135,7 +135,7 @@ public class TryCastTest {
             Assertions.assertFalse(cast.originCastNullable());
             cast = new TryCast(child, DecimalV2Type.createDecimalV2Type(11, 
2));
             Assertions.assertTrue(cast.nullable());
-            Assertions.assertTrue(cast.originCastNullable());
+            Assertions.assertFalse(cast.originCastNullable());
         }
     }
 
@@ -158,7 +158,7 @@ public class TryCastTest {
             child = new SlotReference("slot", BigIntType.INSTANCE, false);
             cast = new TryCast(child, DecimalV2Type.createDecimalV2Type(20, 
1));
             Assertions.assertTrue(cast.nullable());
-            Assertions.assertFalse(cast.originCastNullable());
+            Assertions.assertTrue(cast.originCastNullable());
             cast = new TryCast(child, DecimalV2Type.createDecimalV2Type(20, 
2));
             Assertions.assertTrue(cast.nullable());
             Assertions.assertTrue(cast.originCastNullable());
@@ -266,7 +266,14 @@ public class TryCastTest {
             child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(4, 2), false);
             cast = new TryCast(child, TinyIntType.INSTANCE);
             Assertions.assertTrue(cast.nullable());
-            Assertions.assertFalse(cast.originCastNullable());
+            Assertions.assertTrue(cast.originCastNullable());
+
+            // TRY_CAST itself is always nullable. Its underlying CAST 
nullability must additionally
+            // match the D2-to-D3 kernel's wrapper decision based on the 
original DECIMALV2 metadata.
+            child = new SlotReference("slot", 
DecimalV2Type.createDecimalV2Type(27, 0), false);
+            cast = new TryCast(child, DecimalV3Type.createDecimalV3Type(19, 
0));
+            Assertions.assertTrue(cast.nullable());
+            Assertions.assertTrue(cast.originCastNullable());
         }
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
index 6c3ccf45fb4..2195b5e4b49 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/DataTypeTest.java
@@ -218,12 +218,28 @@ public class DataTypeTest {
 
     @Test
     public void testIsInjectiveCastToForPrimitiveTypes() {
+        assertSafeCast(NullType.INSTANCE, IntegerType.INSTANCE);
         assertSafeCast(IntegerType.INSTANCE, IntegerType.INSTANCE);
         assertSafeCast(IntegerType.INSTANCE, BigIntType.INSTANCE);
         assertUnsafeCast(BigIntType.INSTANCE, IntegerType.INSTANCE);
+        assertSafeCast(TinyIntType.INSTANCE, FloatType.INSTANCE);
+        assertSafeCast(SmallIntType.INSTANCE, FloatType.INSTANCE);
+        assertUnsafeCast(IntegerType.INSTANCE, FloatType.INSTANCE);
+        assertSafeCast(FloatType.INSTANCE, DoubleType.INSTANCE);
+        assertUnsafeCast(FloatType.INSTANCE, StringType.INSTANCE);
+        assertSafeCast(IntegerType.INSTANCE, DoubleType.INSTANCE);
+        assertUnsafeCast(BigIntType.INSTANCE, DoubleType.INSTANCE);
+
+        // DECIMALV2 is deprecated. Treat every cast involving it as 
non-injective, even when the
+        // declared domains suggest that the cast is an identity or widening 
conversion.
+        assertUnsafeCast(NullType.INSTANCE, DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(BooleanType.INSTANCE, DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(IntegerType.INSTANCE, 
DecimalV2Type.createDecimalV2Type(9, 0));
+        assertUnsafeCast(BigIntType.INSTANCE, 
DecimalV2Type.createDecimalV2Type(27, 0));
         assertSafeCast(IntegerType.INSTANCE, 
DecimalV3Type.createDecimalV3Type(10, 0));
         assertUnsafeCast(IntegerType.INSTANCE, 
DecimalV3Type.createDecimalV3Type(9, 0));
         assertUnsafeCast(LargeIntType.INSTANCE, 
DecimalV3Type.createDecimalV3Type(38, 0));
+        assertSafeCast(LargeIntType.INSTANCE, 
DecimalV3Type.createDecimalV3TypeNotCheck256(39, 0));
 
         assertSafeCast(BooleanType.INSTANCE, 
DecimalV3Type.createDecimalV3Type(1, 0));
         assertUnsafeCast(BooleanType.INSTANCE, 
DecimalV3Type.createDecimalV3Type(1, 1));
@@ -231,21 +247,66 @@ public class DataTypeTest {
         assertSafeCast(DecimalV3Type.createDecimalV3Type(6, 2), 
DecimalV3Type.createDecimalV3Type(8, 3));
         assertUnsafeCast(DecimalV3Type.createDecimalV3Type(6, 2), 
DecimalV3Type.createDecimalV3Type(6, 1));
         assertUnsafeCast(DecimalV3Type.createDecimalV3Type(6, 2), 
DecimalV3Type.createDecimalV3Type(5, 2));
-
+        DecimalV2Type narrowDecimalV2 = DecimalV2Type.createDecimalV2Type(2, 
0);
+        assertUnsafeCast(narrowDecimalV2, DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(DecimalV2Type.SYSTEM_DEFAULT, narrowDecimalV2);
+        assertUnsafeCast(narrowDecimalV2, 
DecimalV3Type.createDecimalV3Type(27, 9));
+        assertUnsafeCast(narrowDecimalV2, 
DecimalV3Type.createDecimalV3Type(26, 9));
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(27, 9), 
narrowDecimalV2);
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(28, 9), 
DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(27, 10), 
DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(narrowDecimalV2, TinyIntType.INSTANCE);
+        assertUnsafeCast(narrowDecimalV2, FloatType.INSTANCE);
+        assertUnsafeCast(narrowDecimalV2, DoubleType.INSTANCE);
+        assertUnsafeCast(narrowDecimalV2, StringType.INSTANCE);
+        assertSafeCast(DecimalV3Type.createDecimalV3Type(15, 0), 
DoubleType.INSTANCE);
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(16, 0), 
DoubleType.INSTANCE);
+        assertSafeCast(DecimalV3Type.createDecimalV3Type(15, 6), 
DoubleType.INSTANCE);
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(16, 6), 
DoubleType.INSTANCE);
+        assertUnsafeCast(DecimalV3Type.createDecimalV3Type(6, 2), 
IntegerType.INSTANCE);
+
+        assertSafeCast(DateType.INSTANCE, DateTimeType.INSTANCE);
+        assertSafeCast(DateType.INSTANCE, DateTimeV2Type.of(0));
+        assertSafeCast(DateType.INSTANCE, IntegerType.INSTANCE);
+        assertSafeCast(DateType.INSTANCE, DoubleType.INSTANCE);
+        assertSafeCast(DateV2Type.INSTANCE, DateType.INSTANCE);
+        assertSafeCast(DateV2Type.INSTANCE, DateTimeType.INSTANCE);
+        assertSafeCast(DateV2Type.INSTANCE, IntegerType.INSTANCE);
+        assertSafeCast(DateV2Type.INSTANCE, StringType.INSTANCE);
         assertSafeCast(DateTimeType.INSTANCE, DateTimeV2Type.of(0));
+        assertSafeCast(DateTimeType.INSTANCE, BigIntType.INSTANCE);
+        assertSafeCast(DateTimeType.INSTANCE, DoubleType.INSTANCE);
         assertSafeCast(DateTimeV2Type.of(0), DateTimeType.INSTANCE);
+        assertSafeCast(DateTimeV2Type.of(0), BigIntType.INSTANCE);
+        assertSafeCast(DateTimeV2Type.of(0), DoubleType.INSTANCE);
         assertSafeCast(DateTimeV2Type.of(3), DateTimeV2Type.of(6));
         assertUnsafeCast(DateTimeV2Type.of(3), DateTimeType.INSTANCE);
+        assertUnsafeCast(DateTimeV2Type.of(3), BigIntType.INSTANCE);
+        assertUnsafeCast(DateTimeV2Type.of(3), DoubleType.INSTANCE);
         assertUnsafeCast(DateTimeType.INSTANCE, DateType.INSTANCE);
+        assertSafeCast(TimeV2Type.of(0), FloatType.INSTANCE);
+        assertUnsafeCast(TimeV2Type.of(1), FloatType.INSTANCE);
+        assertSafeCast(TimeV2Type.MAX, BigIntType.INSTANCE);
+        assertSafeCast(TimeV2Type.MAX, LargeIntType.INSTANCE);
+        assertSafeCast(TimeV2Type.MAX, DoubleType.INSTANCE);
+        assertUnsafeCast(TimeV2Type.MAX, IntegerType.INSTANCE);
         assertSafeCast(TimeStampNsType.INSTANCE, TimeStampNsType.INSTANCE);
         assertSafeCast(TimeStampNsType.INSTANCE, StringType.INSTANCE);
         assertUnsafeCast(TimeStampNsType.INSTANCE, DateTimeV2Type.MAX);
         assertUnsafeCast(DateTimeV2Type.MAX, TimeStampNsType.INSTANCE);
 
+        assertSafeCast(IPv4Type.INSTANCE, IPv6Type.INSTANCE);
+        assertSafeCast(IPv4Type.INSTANCE, StringType.INSTANCE);
+        assertSafeCast(IPv6Type.INSTANCE, StringType.INSTANCE);
+        assertUnsafeCast(IPv6Type.INSTANCE, IPv4Type.INSTANCE);
+
         assertSafeCast(VarcharType.createVarcharType(10), 
VarcharType.createVarcharType(20));
         assertSafeCast(VarcharType.createVarcharType(10), StringType.INSTANCE);
         assertSafeCast(VarcharType.createVarcharType(20), 
VarcharType.createVarcharType(10));
         assertSafeCast(StringType.INSTANCE, VarcharType.createVarcharType(10));
+        assertSafeCast(StringType.INSTANCE, 
VarBinaryType.createVarBinaryType(10));
+        assertSafeCast(VarBinaryType.createVarBinaryType(10), 
StringType.INSTANCE);
+        assertSafeCast(VarBinaryType.createVarBinaryType(10), 
VarBinaryType.createVarBinaryType(2));
 
         VariantType v1 = new VariantType(100);
         VariantType anotherV1 = new VariantType(200);
@@ -257,11 +318,18 @@ public class DataTypeTest {
     public void testIsInjectiveCastToForComplexTypes() {
         assertSafeCast(ArrayType.of(IntegerType.INSTANCE), 
ArrayType.of(BigIntType.INSTANCE));
         assertUnsafeCast(ArrayType.of(BigIntType.INSTANCE), 
ArrayType.of(IntegerType.INSTANCE));
-
-        assertSafeCast(MapType.of(IntegerType.INSTANCE, 
VarcharType.createVarcharType(10)),
-                MapType.of(BigIntType.INSTANCE, StringType.INSTANCE));
+        assertUnsafeCast(ArrayType.of(DecimalV2Type.SYSTEM_DEFAULT),
+                ArrayType.of(DecimalV2Type.SYSTEM_DEFAULT));
+
+        MapType intVarcharMap = MapType.of(IntegerType.INSTANCE, 
VarcharType.createVarcharType(10));
+        assertSafeCast(intVarcharMap, intVarcharMap);
+        MapType decimalV2Map = MapType.of(IntegerType.INSTANCE, 
DecimalV2Type.SYSTEM_DEFAULT);
+        assertUnsafeCast(decimalV2Map, decimalV2Map);
+        assertUnsafeCast(intVarcharMap, MapType.of(BigIntType.INSTANCE, 
StringType.INSTANCE));
         assertUnsafeCast(MapType.of(BigIntType.INSTANCE, 
VarcharType.createVarcharType(10)),
                 MapType.of(IntegerType.INSTANCE, StringType.INSTANCE));
+        assertUnsafeCast(ArrayType.of(intVarcharMap),
+                ArrayType.of(MapType.of(BigIntType.INSTANCE, 
StringType.INSTANCE)));
 
         StructType intStringStruct = new StructType(ImmutableList.of(
                 new StructField("a", IntegerType.INSTANCE, true, ""),
@@ -276,9 +344,9 @@ public class DataTypeTest {
         assertUnsafeCast(bigintStringStruct, intStringStruct);
         assertUnsafeCast(intOnlyStruct, intStringStruct);
 
-        assertSafeCast(ArrayType.of(IntegerType.INSTANCE), 
StringType.INSTANCE);
-        assertSafeCast(MapType.of(IntegerType.INSTANCE, StringType.INSTANCE), 
StringType.INSTANCE);
-        assertSafeCast(intStringStruct, StringType.INSTANCE);
+        assertUnsafeCast(ArrayType.of(IntegerType.INSTANCE), 
StringType.INSTANCE);
+        assertUnsafeCast(MapType.of(IntegerType.INSTANCE, 
StringType.INSTANCE), StringType.INSTANCE);
+        assertUnsafeCast(intStringStruct, StringType.INSTANCE);
     }
 
     @Test


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

Reply via email to