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]