github-actions[bot] commented on code in PR #65693:
URL: https://github.com/apache/doris/pull/65693#discussion_r3593657031
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java:
##########
@@ -1322,8 +1347,9 @@ public static Expression
processComparisonPredicate(ComparisonPredicate comparis
throw new AnalysisException("data type " + commonType.get()
+ " could not used in ComparisonPredicate " +
comparisonPredicate.toSql());
}
- left = castIfNotSameType(left, commonType.get());
- right = castIfNotSameType(right, commonType.get());
+ boolean losslessDecimalCast = comparisonPredicate instanceof
EqualPredicate;
+ left = castComparisonOperand(left, commonType.get(),
losslessDecimalCast);
+ right = castComparisonOperand(right, commonType.get(),
losslessDecimalCast);
Review Comment:
**[correctness] Keep conversion rejection distinct from SQL NULL.**
`NullSafeEqual` is an `EqualPredicate`, so this analyzes `(decimal_col <=>
string_col)` as `NullSafeEqual(decimal_col, LosslessCast(string_col AS
DECIMAL(38,0)))`. For a row `(NULL, '100.4')`, the cast marks the non-null
string NULL because scale 0 cannot represent it, and `eq_for_null` then returns
true because both null maps are set. The same sentinel makes `CAST(100 AS
DECIMAL(38,0)) NOT IN ('100.4')` evaluate UNKNOWN instead of true. Please
preserve source NULL separately from an inexact non-match (or use
comparison-specific failure handling), and cover both `<=>` and typed `NOT IN`
regressions.
##########
be/src/exprs/function/cast/cast_to_decimal.h:
##########
@@ -704,6 +833,33 @@ class CastToImpl<Mode, DataTypeString, ToDataType> :
public CastToBase {
RETURN_IF_ERROR(
serde->from_string_strict_mode_batch(*col_from,
*column_to, {}, null_map));
block.get_by_position(result).column = std::move(column_to);
+ } else if constexpr (Mode == CastModeType::LosslessMode) {
+ auto nullable_col_to =
create_empty_nullable_column(nested_to_type);
+ auto& nullable_to = assert_cast<ColumnNullable&>(*nullable_col_to);
+ nullable_to.resize(col_from->size());
+ auto& column_to = assert_cast<typename ToDataType::ColumnType&>(
+ nullable_to.get_nested_column());
+ auto& values_to = column_to.get_data();
+ auto& null_map_to = nullable_to.get_null_map_data();
+ auto* decimal_to_type = assert_cast<const
ToDataType*>(nested_to_type.get());
+ const auto& chars = col_from->get_chars();
+ const auto& offsets = col_from->get_offsets();
+ size_t current_offset = 0;
+ UInt32 precision = decimal_to_type->get_precision();
+ UInt32 scale = decimal_to_type->get_scale();
+ CastParameters params;
+ params.is_strict = false;
+ for (size_t i = 0; i < col_from->size(); ++i) {
+ size_t next_offset = offsets[i];
+ size_t string_size = next_offset - current_offset;
+ bool source_is_null = null_map != nullptr && null_map[i];
+ null_map_to[i] = source_is_null
Review Comment:
**[correctness] Initialize the nested decimal slot on every NULL/failure
path.** `nullable_to.resize()` reaches `PODArray::resize()`, which only
advances the end pointer. Here `source_is_null || ...` skips parsing for source
NULLs, and `from_string_lossless()` returns before writing `values_to[i]` for
rejected strings. Existing `eq` and `eq_for_null` paths evaluate the nested
decimal vectors before applying their null maps, so these rows read
indeterminate storage (UB/MSAN failure) even when the final value is NULL.
Write a decimal default before the short circuit or on both failure paths, and
add a vectorized test with source NULL and an inexact string.
##########
be/src/exprs/vcast_expr.h:
##########
@@ -44,7 +44,8 @@ class VCastExpr : public VExpr {
#ifdef BE_TEST
VCastExpr() = default;
#endif
- VCastExpr(const TExprNode& node) : VExpr(node) {}
+ VCastExpr(const TExprNode& node)
+ : VExpr(node), _lossless_decimal_cast(node.lossless_decimal_cast)
{}
Review Comment:
**[correctness] Preserve the lossless mode in prefilter clones.**
`VCastExpr::clone_node()` rebuilds from `clone_texpr_node()`, which omits this
field, while the format-v2 override recreates a cast from only its target type.
For an external row `(d=0, s='0e2147483648')`, the original lossless cast
treats zero as exact and accepts the parser's overflow status as value 0, but
the localized ordinary cast returns NULL and filters the row before
`Scanner::_filter_output_block()` can run the original residual. Copy the flag
through both clone paths or refuse localization, and add this external-file
false-negative regression.
##########
be/src/exprs/vcast_expr.h:
##########
@@ -82,6 +83,7 @@ class VCastExpr : public VExpr {
std::string _target_data_type_name;
DataTypePtr _cast_param_data_type;
+ bool _lossless_decimal_cast = false;
Review Comment:
**[correctness] Include this mode in `VCastExpr::get_digest()`.** The
current digest hashes the child/base expression and target type only, so
ordinary and lossless casts collide despite different results. For a granule
containing `(d=100, s='100.4')`, the implicit lossless predicate can cache the
granule as all-false; a later explicit `d = CAST(s AS DECIMAL(38,0))` has the
same condition-cache key even though ordinary casting rounds and should match,
so the cached bitmap suppresses the row. Hash `_lossless_decimal_cast` (or
disable condition caching for this mode) and add a digest/cache distinction
test.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java:
##########
@@ -1092,6 +1104,19 @@ private static Optional<DataType>
getCommonDataTypeWithFixedNumericType(
return Optional.empty();
}
+ private static DecimalV3Type getNumericStringComparisonType(NumericType
numericType) {
+ DecimalV3Type decimalType = DecimalV3Type.forType(numericType);
Review Comment:
**[correctness] Preserve the full `LARGEINT` integer range here.**
`DecimalV3Type.forType(LargeIntType)` is `DECIMAL(38,0)`, but signed LARGEINT
extrema require 39 digits. This helper therefore chooses `DECIMAL(38,0)` with
decimal256 off, or e.g. `DECIMAL(44,6)` with it on—still only 38 integer
digits. The analyzed tree casts both the LARGEINT slot and string slot to that
type, so a maximum LARGEINT and its identical text overflow to NULL instead of
matching. Derive capacity from the actual 39-digit range (with a non-narrowing
fallback when decimal256 is unavailable) and test both extrema under both
settings.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java:
##########
@@ -1322,8 +1347,9 @@ public static Expression
processComparisonPredicate(ComparisonPredicate comparis
throw new AnalysisException("data type " + commonType.get()
+ " could not used in ComparisonPredicate " +
comparisonPredicate.toSql());
}
- left = castIfNotSameType(left, commonType.get());
- right = castIfNotSameType(right, commonType.get());
+ boolean losslessDecimalCast = comparisonPredicate instanceof
EqualPredicate;
Review Comment:
**[correctness] Do not use the scale-exhausted equality type for ordering.**
The new numeric/string common type also reaches `<`, `<=`, `>`, `>=`, and
BETWEEN, but this flag is enabled only for equality. With `DECIMAL(38,0)`, the
common type remains scale 0, so ordinary parsing rounds `'100.4'` to 100: `100
>= '100.4'` becomes true and `100 < '100.4'` becomes false. Decimal256 just
moves the same boundary to `DECIMAL(76,0)`. Preserve enough information to
order the fractional string without rounding, and add range/BETWEEN boundary
tests in both coercion modes.
##########
be/src/exprs/function/cast/cast_to_decimal.h:
##########
@@ -35,45 +37,172 @@ namespace doris {
value, from_type_name, precision, scale))
struct CastToDecimal {
+ static bool is_lossless_decimal_string(const char* data, size_t size,
UInt32 precision,
+ UInt32 target_scale) {
+ size_t begin = 0;
+ size_t end = size;
+ while (begin < end && std::isspace(static_cast<unsigned
char>(data[begin]))) {
+ ++begin;
+ }
+ while (begin < end && std::isspace(static_cast<unsigned char>(data[end
- 1]))) {
+ --end;
+ }
+ if (begin == end) {
+ return false;
+ }
+
+ size_t pos = begin;
+ if (data[pos] == '+' || data[pos] == '-') {
+ ++pos;
+ }
+
+ bool seen_dot = false;
+ bool seen_digit = false;
+ size_t digits_before_dot = 0;
+ size_t digits = 0;
+ size_t first_non_zero = 0;
+ size_t last_non_zero = 0;
+ while (pos < end && data[pos] != 'e' && data[pos] != 'E') {
+ unsigned char ch = static_cast<unsigned char>(data[pos]);
+ if (std::isdigit(ch)) {
+ seen_digit = true;
+ ++digits;
+ if (!seen_dot) {
+ ++digits_before_dot;
+ }
+ if (ch != '0') {
+ if (first_non_zero == 0) {
+ first_non_zero = digits;
+ }
+ last_non_zero = digits;
+ }
+ } else if (data[pos] == '.' && !seen_dot) {
+ seen_dot = true;
+ } else {
+ return false;
+ }
+ ++pos;
+ }
+ if (!seen_digit) {
+ return false;
+ }
+
+ int exponent = 0;
+ if (pos < end) {
+ ++pos;
+ bool negative_exponent = false;
+ if (pos < end && (data[pos] == '+' || data[pos] == '-')) {
+ negative_exponent = data[pos] == '-';
+ ++pos;
+ }
+ if (pos == end) {
+ return false;
+ }
+
+ constexpr int max_relevant_exponent = 1024;
+ int exponent_magnitude = 0;
+ while (pos < end) {
+ unsigned char ch = static_cast<unsigned char>(data[pos]);
+ if (!std::isdigit(ch)) {
+ return false;
+ }
+ if (exponent_magnitude <= max_relevant_exponent) {
+ exponent_magnitude = exponent_magnitude * 10 + (ch - '0');
+ if (exponent_magnitude > max_relevant_exponent) {
+ exponent_magnitude = max_relevant_exponent + 1;
+ }
+ }
+ ++pos;
+ }
+ if (exponent_magnitude > max_relevant_exponent) {
Review Comment:
**[correctness] Combine the exponent and significand offset before applying
a cap.** For example, `"0." + 1024 zeroes + "1e1025"` is only 1032 bytes and
equals exactly 1, which the existing `StringParser` accepts. This branch
rejects every nonzero exponent above 1024 before adding the opposite
`base_decimal_point`, so the new lossless equality turns that exact value into
NULL and misses a match. Use overflow-safe signed arithmetic for the combined
decimal-point position, and test large positive and negative offsets that
cancel.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/TypeCoercionUtils.java:
##########
@@ -474,6 +474,16 @@ public static Expression castIfNotSameType(Expression
input, DataType targetType
}
}
+ private static Expression castComparisonOperand(
+ Expression input, DataType targetType, boolean
losslessDecimalCast) {
+ if (losslessDecimalCast && input.getDataType().isStringLikeType()
Review Comment:
**[correctness] Apply lossless coercion recursively for complex comparison
types.** Common-type derivation recurses, so `ARRAY<DECIMAL(38,0)> =
ARRAY<STRING>` becomes an outer cast to `ARRAY<DECIMAL(38,0)>`; this
scalar-only check sees neither an outer string nor an outer decimal and creates
an ordinary cast. BE then rounds the nested `'100.4'` to 100, making `[100] =
['100.4']` true. Struct-valued IN has the same gap. Detect nested
numeric/string coercion and mark the containing cast (the BE wrapper propagates
its context), or reject these shapes until supported; add array equality and
struct-IN controls.
##########
fe/fe-core/src/main/java/org/apache/doris/nereids/glue/translator/ExpressionTranslator.java:
##########
@@ -449,7 +449,7 @@ public Expr visitCaseWhen(CaseWhen caseWhen,
PlanTranslatorContext context) {
public Expr visitCast(Cast cast, PlanTranslatorContext context) {
// left child of cast is expression, right child of cast is target type
CastExpr castExpr = new
CastExpr(cast.getDataType().toCatalogDataType(),
- cast.child().accept(this, context), cast.nullable());
+ cast.child().accept(this, context), cast.nullable(),
cast.isLosslessDecimalCast());
Review Comment:
**[correctness] Update the COPY INTO `visitCast` override as well.**
`CopyIntoInfo.ExpressionToExpr` analyzes file filters/column mappings with this
coercion, but then reconstructs every cast with the three-argument `CastExpr`
constructor, which defaults `losslessDecimalCast` to false. A numeric/string
file filter therefore reaches BE as ordinary CAST and can round `'100.4'` to
scale-zero 100. Forward both `isLosslessDecimalCast()` and the
implicit/explicit bit there (or delegate to this implementation), and add a
COPY INTO translation/end-to-end regression.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]