This is an automated email from the ASF dual-hosted git repository.
jacktengg 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 9b699317c29 [fix](be) Return null for TRY_CAST arithmetic overflow
(#67697)
9b699317c29 is described below
commit 9b699317c29e21ba893fc2c1b3cbd03af76f4470
Author: TengJianPing <[email protected]>
AuthorDate: Mon Sep 14 10:07:46 2026 +0800
[fix](be) Return null for TRY_CAST arithmetic overflow (#67697)
Problem Summary:
With enable_strict_cast=true, TRY_CAST of a DECIMAL(6,3) column
containing 123.456 to DECIMAL(4,2) fails the entire query with E-255.
TRY_CAST only classified INVALID_ARGUMENT as a recoverable conversion
failure, while decimal casts return ARITHMETIC_OVERFLOW_ERRROR.
Recognize arithmetic overflow in the shared batch and per-row error
classification so overflowing values become NULL and valid rows survive.
Child expression errors and ordinary CAST still propagate failures. Add
unit coverage for single-row and batch overflow with nullable and
non-nullable original cast results, plus child overflow propagation.
### What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
### Release note
None
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [ ] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
- Behavior changed:
- [ ] No.
- [ ] Yes. <!-- Explain the behavior change -->
- Does this need documentation?
- [ ] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->
---
.../core/data_type_serde/data_type_map_serde.cpp | 2 +-
.../data_type_serde/data_type_struct_serde.cpp | 2 +-
be/src/exprs/function/cast/cast_to_date.h | 2 +-
be/src/exprs/function/cast/cast_to_timestamptz.h | 4 +-
.../cast/variant_v2/cast_array_to_variant.cpp | 22 +-
.../cast/variant_v2/cast_variant_to_array.cpp | 2 +-
.../cast/variant_v2/cast_variant_to_jsonb.cpp | 8 +-
.../cast/variant_v2/cast_variant_to_scalar.cpp | 20 +-
.../cast/variant_v2/cast_variant_to_string.cpp | 6 +-
.../function/cast/variant_v2/cast_variant_v2.cpp | 13 +-
be/src/exprs/vcast_expr.cpp | 52 ++--
be/src/exprs/vcast_expr.h | 1 -
.../function/cast/cast_variant_v2_from_test.cpp | 2 +-
.../function/cast/cast_variant_v2_to_test.cpp | 2 +-
be/test/exprs/try_cast_expr_test.cpp | 282 +++++++++++++++++++++
.../cast/test_try_cast_decimal_overflow.out | 22 ++
.../cast/test_try_cast_complex_overflow.groovy | 75 ++++++
.../cast/test_try_cast_conversion_errors.groovy | 73 ++++++
.../cast/test_try_cast_decimal_overflow.groovy | 110 ++++++++
19 files changed, 621 insertions(+), 79 deletions(-)
diff --git a/be/src/core/data_type_serde/data_type_map_serde.cpp
b/be/src/core/data_type_serde/data_type_map_serde.cpp
index 2702e14373d..581bd791d47 100644
--- a/be/src/core/data_type_serde/data_type_map_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_map_serde.cpp
@@ -690,7 +690,7 @@ Status DataTypeMapSerDe::serialize_column_to_jsonb(const
IColumn& from_column, i
auto key_str = key_string_column->get_data_at(i);
// check key size
if (key_str.size > std::numeric_limits<uint8_t>::max()) {
- return Status::InternalError("key size exceeds max limit {} ",
key_str.to_string());
+ return Status::InvalidArgument("key size exceeds max limit {} ",
key_str.to_string());
}
// write key
if (!writer.writeKey(key_str.data, (uint8_t)key_str.size)) {
diff --git a/be/src/core/data_type_serde/data_type_struct_serde.cpp
b/be/src/core/data_type_serde/data_type_struct_serde.cpp
index cf13c11dcaf..97554c3b852 100644
--- a/be/src/core/data_type_serde/data_type_struct_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_struct_serde.cpp
@@ -394,7 +394,7 @@ Status DataTypeStructSerDe::serialize_column_to_jsonb(const
IColumn& from_column
for (size_t i = 0; i < elem_serdes_ptrs.size(); ++i) {
// check key
if (elem_names[i].size() > std::numeric_limits<uint8_t>::max()) {
- return Status::InternalError("key size exceeds max limit {} ",
elem_names[i]);
+ return Status::InvalidArgument("key size exceeds max limit {} ",
elem_names[i]);
}
// write key
if (!writer.writeKey(elem_names[i].data(),
(uint8_t)elem_names[i].size())) {
diff --git a/be/src/exprs/function/cast/cast_to_date.h
b/be/src/exprs/function/cast/cast_to_date.h
index 903ef17083a..eecfcb6d552 100644
--- a/be/src/exprs/function/cast/cast_to_date.h
+++ b/be/src/exprs/function/cast/cast_to_date.h
@@ -504,7 +504,7 @@ public:
TimestampTzValue from_tz {col_from[i]};
DateV2Value<DateTimeV2ValueType> dt;
if (!from_tz.to_datetime(dt, local_time_zone, dt_scale, tz_scale))
{
- return Status::InternalError(
+ return Status::InvalidArgument(
"can not cast from timestamptz : {} to datetime in
timezone : {}",
from_tz.to_string(local_time_zone),
context->state()->timezone());
}
diff --git a/be/src/exprs/function/cast/cast_to_timestamptz.h
b/be/src/exprs/function/cast/cast_to_timestamptz.h
index 95849184a49..1e31cf24be5 100644
--- a/be/src/exprs/function/cast/cast_to_timestamptz.h
+++ b/be/src/exprs/function/cast/cast_to_timestamptz.h
@@ -91,7 +91,7 @@ public:
TimestampTzValue tz_value;
if (!tz_value.from_datetime(from_dt, local_time_zone, dt_scale,
tz_scale)) {
- return Status::InternalError(
+ return Status::InvalidArgument(
"can not cast from datetime : {} to timestamptz in
timezone : {}",
from_dt.to_string(), context->state()->timezone());
}
@@ -214,7 +214,7 @@ public:
auto& to_tz = col_to_data[i];
if (!transform_date_scale(to_scale, from_scale, to_tz, from_tz)) {
- return Status::InternalError(
+ return Status::InvalidArgument(
"can not cast from timestamptz : {} to timestamptz in
timezone : {}",
TimestampTzValue {from_tz}.to_string(local_time_zone,
from_scale),
context->state()->timezone());
diff --git a/be/src/exprs/function/cast/variant_v2/cast_array_to_variant.cpp
b/be/src/exprs/function/cast/variant_v2/cast_array_to_variant.cpp
index e35c9b1983b..b30ad8ef673 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_array_to_variant.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_array_to_variant.cpp
@@ -55,8 +55,8 @@ struct ArrayEncodePlan {
Status validate_local_nulls(const IColumn& source, const NullMap* nulls) {
if (nulls != nullptr && nulls->size() != source.size()) {
- return Status::InvalidArgument("Array element null map has {} rows,
expected {}",
- nulls->size(), source.size());
+ return Status::InternalError("Array element null map has {} rows,
expected {}",
+ nulls->size(), source.size());
}
return Status::OK();
}
@@ -90,8 +90,8 @@ Status populate_effective_nulls(const IColumn& source, const
NullMap* local_null
ForcedNulls inherited_nulls, ArrayEncodePlan*
plan) {
RETURN_IF_ERROR(validate_local_nulls(source, local_nulls));
if (!inherited_nulls.empty() && inherited_nulls.size() != source.size()) {
- return Status::InvalidArgument("Array ancestor null map has {} rows,
expected {}",
- inherited_nulls.size(), source.size());
+ return Status::InternalError("Array ancestor null map has {} rows,
expected {}",
+ inherited_nulls.size(), source.size());
}
const ForcedNulls local = local_nulls == nullptr
? ForcedNulls {}
@@ -124,8 +124,8 @@ Status build_array_node_plan(const ColumnPtr& source, const
DataTypePtr& source_
ArrayEncodePlan* plan) {
plan->array = check_and_get_column<ColumnArray>(source.get());
if (plan->array == nullptr) {
- return Status::InvalidArgument("Array Variant V2 CAST expected
ColumnArray, got {}",
- source->get_name());
+ return Status::InternalError("Array Variant V2 CAST expected
ColumnArray, got {}",
+ source->get_name());
}
const auto& elements = assert_cast<const
ColumnNullable&>(plan->array->get_data());
const auto& array_type = assert_cast<const DataTypeArray&>(*source_type);
@@ -142,7 +142,7 @@ Status build_array_leaf_plan(const ColumnPtr& source,
PrimitiveType primitive,
} else if (primitive == TYPE_VARIANT) {
const auto* variant =
check_and_get_column<ColumnVariantV2>(source.get());
if (variant == nullptr) {
- return Status::InvalidArgument("Array Variant V2 CAST received a
legacy Variant leaf");
+ return Status::InternalError("Array Variant V2 CAST received a
legacy Variant leaf");
}
if (variant->is_typed()) {
const auto& typed = assert_cast<const
ColumnNullable&>(variant->typed_column());
@@ -154,8 +154,8 @@ Status build_array_leaf_plan(const ColumnPtr& source,
PrimitiveType primitive,
} else if (primitive == TYPE_JSONB) {
plan->jsonb_leaf = check_and_get_column<ColumnString>(source.get());
if (plan->jsonb_leaf == nullptr) {
- return Status::InvalidArgument("Array JSONB leaf expected
ColumnString, got {}",
- source->get_name());
+ return Status::InternalError("Array JSONB leaf expected
ColumnString, got {}",
+ source->get_name());
}
} else if (is_supported_scalar_source(plan->type)) {
configure_scalar_leaf(*source, plan->type, plan);
@@ -170,7 +170,7 @@ Status build_array_encode_plan(const ColumnPtr& source,
const DataTypePtr& sourc
const NullMap* local_nulls, ForcedNulls
inherited_nulls,
ArrayEncodePlan* plan) {
if (!source || !source_type) {
- return Status::InvalidArgument("Array Variant V2 CAST received an
empty source");
+ return Status::InternalError("Array Variant V2 CAST received an empty
source");
}
plan->type = remove_nullable(source_type);
RETURN_IF_ERROR(populate_effective_nulls(*source, local_nulls,
inherited_nulls, plan));
@@ -215,7 +215,7 @@ Status cast_array_to_variant(const ColumnPtr& source, const
DataTypePtr& source_
ForcedNulls forced_nulls, ColumnPtr* output) {
if (!source || source->size() != rows ||
(!forced_nulls.empty() && forced_nulls.size() != rows)) {
- return Status::InvalidArgument("Invalid ARRAY input shape for Variant
V2 CAST");
+ return Status::InternalError("Invalid ARRAY input shape for Variant V2
CAST");
}
ArrayEncodePlan plan;
RETURN_IF_ERROR(build_array_encode_plan(source, source_type, nullptr,
forced_nulls, &plan));
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_to_array.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_to_array.cpp
index 95519a51f85..595f60c80f7 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_to_array.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_to_array.cpp
@@ -157,7 +157,7 @@ Status cast_variant_to_array(FunctionContext* context,
const ColumnVariantV2& so
ColumnPtr* output) {
if (source.size() != rows || target_type->get_primitive_type() !=
TYPE_ARRAY ||
(!forced_nulls.empty() && forced_nulls.size() != rows)) {
- return Status::InvalidArgument("Invalid Variant V2 input shape for
ARRAY CAST");
+ return Status::InternalError("Invalid Variant V2 input shape for ARRAY
CAST");
}
if (source.is_typed()) {
if (context == nullptr) {
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_to_jsonb.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_to_jsonb.cpp
index 93d5daf055f..e6b5e4b6a13 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_to_jsonb.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_to_jsonb.cpp
@@ -40,7 +40,7 @@ Status cast_jsonb_to_variant(const ColumnPtr& source, size_t
rows, ForcedNulls f
const auto* strings = check_and_get_column<ColumnString>(source.get());
if (strings == nullptr || strings->size() != rows ||
(!forced_nulls.empty() && forced_nulls.size() != rows)) {
- return Status::InvalidArgument("Invalid JSONB input shape for Variant
V2 CAST");
+ return Status::InternalError("Invalid JSONB input shape for Variant V2
CAST");
}
JsonbToVariantEncoder encoder(VariantBatchBuilder::ReserveHint {.rows =
rows});
for (size_t row = 0; row < rows; ++row) {
@@ -60,7 +60,7 @@ Status cast_jsonb_to_variant(const ColumnPtr& source, size_t
rows, ForcedNulls f
Status cast_variant_to_jsonb(FunctionContext* context, const ColumnVariantV2&
source, size_t rows,
ForcedNulls forced_nulls, ColumnPtr* output) {
if (source.size() != rows || (!forced_nulls.empty() && forced_nulls.size()
!= rows)) {
- return Status::InvalidArgument("Invalid Variant V2 input shape for
JSONB CAST");
+ return Status::InternalError("Invalid Variant V2 input shape for JSONB
CAST");
}
auto strings = ColumnString::create();
auto nulls = ColumnUInt8::create(rows, 0);
@@ -83,8 +83,8 @@ Status cast_variant_to_jsonb(FunctionContext* context, const
ColumnVariantV2& so
Status cast_variant_refs_to_jsonb(FunctionContext* context, std::span<const
VariantRef> values,
ForcedNulls forced_nulls, ColumnPtr* output)
{
if (!forced_nulls.empty() && forced_nulls.size() != values.size()) {
- return Status::InvalidArgument("Variant V2 JSONB CAST null map has {}
rows, expected {}",
- forced_nulls.size(), values.size());
+ return Status::InternalError("Variant V2 JSONB CAST null map has {}
rows, expected {}",
+ forced_nulls.size(), values.size());
}
auto strings = ColumnString::create();
auto nulls = ColumnUInt8::create(values.size(), 0);
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp
index a0fb4c52a26..20ea41b40d7 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_to_scalar.cpp
@@ -349,7 +349,7 @@ Status execute_non_strict_scalar_cast(FunctionContext*
context, const ColumnPtr&
const DataTypePtr& target_type, const
char* source_name,
size_t rows, ColumnPtr* output) {
if (context == nullptr) {
- return Status::InvalidArgument("Variant V2 scalar CAST requires a
FunctionContext");
+ return Status::InternalError("Variant V2 scalar CAST requires a
FunctionContext");
}
auto cast_context = context->clone();
cast_context->set_enable_strict_mode(false);
@@ -483,7 +483,7 @@ Status cast_scalar_to_variant(const ColumnPtr& source,
const DataTypePtr& source
ForcedNulls forced_nulls, ColumnPtr* output) {
if (!source || source->size() != rows ||
(!forced_nulls.empty() && forced_nulls.size() != rows)) {
- return Status::InvalidArgument("Invalid scalar input shape for Variant
V2 CAST");
+ return Status::InternalError("Invalid scalar input shape for Variant
V2 CAST");
}
auto nulls = ColumnUInt8::create(rows, 0);
if (!forced_nulls.empty()) {
@@ -499,7 +499,7 @@ Status cast_typed_variant_to_scalar(FunctionContext*
context, const ColumnVarian
const DataTypePtr& target_type, size_t
rows,
ForcedNulls forced_nulls, ColumnPtr*
output) {
if (!source.is_typed() || source.size() != rows) {
- return Status::InvalidArgument("Expected a typed Variant V2 source
with {} rows", rows);
+ return Status::InternalError("Expected a typed Variant V2 source with
{} rows", rows);
}
ColumnPtr converted;
RETURN_IF_ERROR(execute_typed_cast(context,
source.typed_column().get_ptr(),
@@ -511,8 +511,8 @@ Status cast_variant_refs_to_scalar(FunctionContext*
context, std::span<const Var
const DataTypePtr& target_type, ForcedNulls
forced_nulls,
ColumnPtr* output) {
if (!forced_nulls.empty() && forced_nulls.size() != values.size()) {
- return Status::InvalidArgument("Variant V2 CAST null map has {} rows,
expected {}",
- forced_nulls.size(), values.size());
+ return Status::InternalError("Variant V2 CAST null map has {} rows,
expected {}",
+ forced_nulls.size(), values.size());
}
ScalarGroups groups;
for (size_t row = 0; row < values.size(); ++row) {
@@ -527,7 +527,7 @@ Status cast_encoded_variant_to_scalar(FunctionContext*
context, const ColumnVari
ForcedNulls forced_nulls, ColumnPtr*
output) {
if (source.is_typed() || source.size() != rows ||
(!forced_nulls.empty() && forced_nulls.size() != rows)) {
- return Status::InvalidArgument("Invalid encoded Variant V2 input for
scalar CAST");
+ return Status::InternalError("Invalid encoded Variant V2 input for
scalar CAST");
}
ScalarGroups groups;
for (size_t row = 0; row < rows; ++row) {
@@ -542,7 +542,7 @@ Status cast_variant_values_to_scalar(FunctionContext*
context, const ColumnVaria
const DataTypePtr& target_type, size_t
rows,
ForcedNulls forced_nulls, ColumnPtr*
output) {
if (source.size() != rows || (!forced_nulls.empty() && forced_nulls.size()
!= rows)) {
- return Status::InvalidArgument("Invalid Variant V2 input for canonical
scalar CAST");
+ return Status::InternalError("Invalid Variant V2 input for canonical
scalar CAST");
}
ScalarGroups groups;
visit_variant_v2_values(
@@ -563,15 +563,15 @@ ColumnPtr make_all_null_column(const DataTypePtr&
nested_type, size_t rows) {
Status apply_forced_nulls(ColumnPtr column, ForcedNulls forced_nulls,
ColumnPtr* output) {
if (!column) {
- return Status::InvalidArgument("Cannot apply a null map to an empty
Variant V2 result");
+ return Status::InternalError("Cannot apply a null map to an empty
Variant V2 result");
}
if (forced_nulls.empty()) {
*output = std::move(column);
return Status::OK();
}
if (forced_nulls.size() != column->size()) {
- return Status::InvalidArgument("Variant V2 CAST null map has {} rows,
expected {}",
- forced_nulls.size(), column->size());
+ return Status::InternalError("Variant V2 CAST null map has {} rows,
expected {}",
+ forced_nulls.size(), column->size());
}
auto nulls = ColumnUInt8::create(column->size(), 0);
if (const auto* nullable =
check_and_get_column<ColumnNullable>(column.get())) {
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
index 73ba82537ae..069e58fc2c1 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_to_string.cpp
@@ -222,8 +222,8 @@ Status cast_typed_variant_to_string(FunctionContext*
context, const ColumnVarian
Status cast_variant_refs_to_string(FunctionContext* context, std::span<const
VariantRef> values,
ForcedNulls forced_nulls, ColumnPtr*
output) {
if (!forced_nulls.empty() && forced_nulls.size() != values.size()) {
- return Status::InvalidArgument("Variant V2 STRING CAST null map has {}
rows, expected {}",
- forced_nulls.size(), values.size());
+ return Status::InternalError("Variant V2 STRING CAST null map has {}
rows, expected {}",
+ forced_nulls.size(), values.size());
}
return cast_values_to_string(
context, values.size(), forced_nulls, [&](size_t row) { return
values[row]; },
@@ -236,7 +236,7 @@ Status cast_variant_refs_to_string(FunctionContext*
context, std::span<const Var
Status cast_variant_to_string(FunctionContext* context, const ColumnVariantV2&
source, size_t rows,
ForcedNulls forced_nulls, ColumnPtr* output) {
if (source.size() != rows || (!forced_nulls.empty() && forced_nulls.size()
!= rows)) {
- return Status::InvalidArgument("Invalid Variant V2 input shape for
STRING CAST");
+ return Status::InternalError("Invalid Variant V2 input shape for
STRING CAST");
}
if (source.is_typed()) {
return cast_typed_variant_to_string(context, source, rows,
forced_nulls, output);
diff --git a/be/src/exprs/function/cast/variant_v2/cast_variant_v2.cpp
b/be/src/exprs/function/cast/variant_v2/cast_variant_v2.cpp
index 78a261f2e71..93c1c680624 100644
--- a/be/src/exprs/function/cast/variant_v2/cast_variant_v2.cpp
+++ b/be/src/exprs/function/cast/variant_v2/cast_variant_v2.cpp
@@ -55,18 +55,18 @@ ForcedNulls forced_nulls(const NullMap::value_type*
null_map, size_t rows) {
Status require_materialized_source(const Block& block, const ColumnNumbers&
arguments, size_t rows,
const IColumn** source) {
if (arguments.size() != 1 || arguments[0] >= block.columns()) {
- return Status::InvalidArgument("Variant V2 CAST requires exactly one
valid argument");
+ return Status::InternalError("Variant V2 CAST requires exactly one
valid argument");
}
const ColumnPtr& column = block.get_by_position(arguments[0]).column;
if (!column) {
- return Status::InvalidArgument("Variant V2 CAST source column is
null");
+ return Status::InternalError("Variant V2 CAST source column is null");
}
if (column->size() != rows) {
return Status::InternalError("Variant V2 CAST source has {} rows,
expected {}",
column->size(), rows);
}
if (is_column_const(*column)) {
- return Status::InvalidArgument(
+ return Status::InternalError(
"Variant V2 CAST kernel requires a materialized source
column");
}
*source = column.get();
@@ -75,8 +75,7 @@ Status require_materialized_source(const Block& block, const
ColumnNumbers& argu
Status commit_result(Block& block, uint32_t result, size_t rows, ColumnPtr
output) {
if (result >= block.columns()) {
- return Status::InvalidArgument("Variant V2 CAST result position {} is
out of range",
- result);
+ return Status::InternalError("Variant V2 CAST result position {} is
out of range", result);
}
if (!output || output->size() != rows) {
return Status::InternalError("Variant V2 CAST produced {} rows,
expected {}",
@@ -111,7 +110,7 @@ Status execute_to_variant(const DataTypePtr&
captured_from_type, FunctionContext
output = std::move(nulls);
} else if (primitive == TYPE_VARIANT) {
if (check_and_get_column<ColumnVariantV2>(source) == nullptr) {
- return Status::InvalidArgument(
+ return Status::InternalError(
"ColumnVariantV2 CAST received a legacy Variant "
"column in compute-only mode");
}
@@ -142,7 +141,7 @@ Status execute_from_variant(const DataTypePtr&
captured_to_type, FunctionContext
RETURN_IF_ERROR(require_materialized_source(block, arguments, rows,
&source_column));
const auto* source = check_and_get_column<ColumnVariantV2>(source_column);
if (source == nullptr) {
- return Status::InvalidArgument(
+ return Status::InternalError(
"ColumnVariantV2 CAST received a legacy Variant column in
compute-only mode");
}
diff --git a/be/src/exprs/vcast_expr.cpp b/be/src/exprs/vcast_expr.cpp
index ca41d96ba90..09681a46dd3 100644
--- a/be/src/exprs/vcast_expr.cpp
+++ b/be/src/exprs/vcast_expr.cpp
@@ -130,13 +130,14 @@ Status VCastExpr::execute_column_impl(VExprContext*
context, const Block* block,
return Status::OK();
}
-bool cast_error_code(Status& st) {
- //There may be more error codes that need to be captured by try cast in
the future.
- if (st.is<ErrorCode::INVALID_ARGUMENT>()) {
- return true;
- } else {
- return false;
- }
+bool cast_error_code(const Status& st) {
+ // Value conversion failures use INVALID_ARGUMENT (parsing, numeric/date
range checks,
+ // JSONB and complex elements) or ARITHMETIC_OVERFLOW_ERRROR (decimal
conversions).
+ // This predicate relies on producers classifying errors correctly:
execution-contract
+ // violations (including invalid column shapes) must use INTERNAL_ERROR,
not these codes.
+ // Likewise, value failures reported as INTERNAL_ERROR must be corrected
at their producer;
+ // accepting all INTERNAL_ERROR/RUNTIME_ERROR statuses would hide
execution defects.
+ return st.is<ErrorCode::INVALID_ARGUMENT>() ||
st.is<ErrorCode::ARITHMETIC_OVERFLOW_ERRROR>();
}
DataTypePtr TryCastExpr::original_cast_return_type() const {
@@ -189,20 +190,12 @@ Status TryCastExpr::execute_column_impl(VExprContext*
context, const Block* bloc
// If there is an error that can be handled by try cast,
// it will be converted into line execution.
ColumnWithTypeAndName input_info {from_column, from_type,
_children[0]->expr_name()};
- // distinguish whether the return value of the original cast is nullable
- if (_original_cast_return_is_nullable) {
- RETURN_IF_ERROR(single_row_execute<true>(context, input_info,
result_column));
- } else {
- RETURN_IF_ERROR(single_row_execute<false>(context, input_info,
result_column));
- }
- // wrap nullable
- result_column = make_nullable(result_column);
+ RETURN_IF_ERROR(single_row_execute(context, input_info, result_column));
DCHECK_EQ(result_column->size(), count);
return Status::OK();
}
-template <bool original_cast_reutrn_is_nullable>
Status TryCastExpr::single_row_execute(VExprContext* context,
const ColumnWithTypeAndName& input_info,
ColumnPtr& return_column) const {
@@ -211,23 +204,7 @@ Status TryCastExpr::single_row_execute(VExprContext*
context,
const auto& input_name = input_info.name;
auto result_column = _data_type->create_column();
- ColumnNullable& result_null_column =
assert_cast<ColumnNullable&>(*result_column);
-
- IColumn& result_nested_column = result_null_column.get_nested_column();
- auto& result_null_map_data = result_null_column.get_null_map_data();
-
- auto insert_from_single_row = [&](const IColumn& single_exec_column,
size_t row) {
- DCHECK_EQ(single_exec_column.size(), 1);
- if constexpr (original_cast_reutrn_is_nullable) {
- result_null_column.insert_from(single_exec_column, 0);
- } else {
- DCHECK(!single_exec_column.is_nullable());
- result_nested_column.insert_from(single_exec_column, 0);
- result_null_map_data.push_back(0);
- }
- };
-
- auto insert_null = [&](size_t row) { result_null_column.insert_default();
};
+ auto& result_null_column = assert_cast<ColumnNullable&>(*result_column);
const auto size = input_column->size();
for (size_t row = 0; row < size; ++row) {
@@ -238,12 +215,17 @@ Status TryCastExpr::single_row_execute(VExprContext*
context,
auto single_exec_status =
_function->execute(context->fn_context(_fn_context_index),
single_row_block, {0}, 1,
1);
if (single_exec_status.ok()) {
-
insert_from_single_row(*single_row_block.get_by_position(1).column, row);
+ // FunctionCast uses TRY_CAST's nullable return type even when the
original CAST
+ // is non-nullable. Normalize the actual result before copying the
successful row.
+ auto single_exec_column = make_nullable(
+
single_row_block.get_by_position(1).column->convert_to_full_column_if_const());
+ DCHECK_EQ(single_exec_column->size(), 1);
+ result_null_column.insert_from(*single_exec_column, 0);
} else {
if (!cast_error_code(single_exec_status)) {
return single_exec_status;
}
- insert_null(row);
+ result_null_column.insert_default();
}
}
return_column = std::move(result_column);
diff --git a/be/src/exprs/vcast_expr.h b/be/src/exprs/vcast_expr.h
index 5fc51029aeb..f049811634b 100644
--- a/be/src/exprs/vcast_expr.h
+++ b/be/src/exprs/vcast_expr.h
@@ -117,7 +117,6 @@ public:
private:
DataTypePtr original_cast_return_type() const;
- template <bool original_cast_reutrn_is_nullable>
Status single_row_execute(VExprContext* context, const
ColumnWithTypeAndName& input_info,
ColumnPtr& return_column) const;
diff --git a/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
b/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
index 01339f7fa90..20669dcaf2c 100644
--- a/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
+++ b/be/test/exprs/function/cast/cast_variant_v2_from_test.cpp
@@ -788,7 +788,7 @@ TEST(CastVariantV2FromTest,
OuterNullMapMasksValueAndConstContractIsExplicit) {
ColumnPtr one = source->clone_resized(1);
ColumnPtr constant = ColumnConst::create(IColumn::mutate(one), 3);
CastResult const_result = execute_from_variant(constant,
std::make_shared<DataTypeInt32>());
- EXPECT_TRUE(const_result.status.is<ErrorCode::INVALID_ARGUMENT>());
+ EXPECT_TRUE(const_result.status.is<ErrorCode::INTERNAL_ERROR>());
EXPECT_EQ(const_result.column.get(), const_result.initial_result.get());
}
diff --git a/be/test/exprs/function/cast/cast_variant_v2_to_test.cpp
b/be/test/exprs/function/cast/cast_variant_v2_to_test.cpp
index 682498192de..54bedaa5c52 100644
--- a/be/test/exprs/function/cast/cast_variant_v2_to_test.cpp
+++ b/be/test/exprs/function/cast/cast_variant_v2_to_test.cpp
@@ -317,7 +317,7 @@ TEST(CastVariantV2ToTest,
UnsupportedMapAndConstInputLeaveResultUntouched) {
value->insert_value(1);
ColumnPtr constant = ColumnConst::create(std::move(value), 3);
CastResult const_result = execute_to_variant(constant,
std::make_shared<DataTypeInt32>());
- EXPECT_TRUE(const_result.status.is<ErrorCode::INVALID_ARGUMENT>());
+ EXPECT_TRUE(const_result.status.is<ErrorCode::INTERNAL_ERROR>());
EXPECT_EQ(const_result.column.get(), const_result.initial_result.get());
}
diff --git a/be/test/exprs/try_cast_expr_test.cpp
b/be/test/exprs/try_cast_expr_test.cpp
index d176c6cd6c4..030a0b48a22 100644
--- a/be/test/exprs/try_cast_expr_test.cpp
+++ b/be/test/exprs/try_cast_expr_test.cpp
@@ -18,15 +18,30 @@
#include <gtest/gtest.h>
#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+#include "common/exception.h"
#include "core/column/column_nothing.h"
#include "core/column/column_nullable.h"
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_decimal.h"
+#include "core/data_type/data_type_jsonb.h"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_struct.h"
+#include "core/data_type/data_type_timestamptz.h"
#include "core/data_type/primitive_type.h"
+#include "core/data_type_serde/data_type_serde.h"
#include "core/field.h"
#include "core/types.h"
+#include "exprs/function/simple_function_factory.h"
#include "exprs/function_context.h"
#include "exprs/vcast_expr.h"
#include "exprs/vexpr_context.h"
+#include "runtime/runtime_state.h"
namespace doris {
@@ -124,6 +139,49 @@ struct TryCastTestRowExecReturnErrorImpl {
}
};
+template <int error_code, bool retry, bool throw_error>
+struct TryCastTestExecutionErrorImpl {
+ static Status execute_impl(FunctionContext* context, Block& block,
+ const ColumnNumbers& arguments, uint32_t result,
+ size_t input_rows_count) {
+ if constexpr (retry) {
+ if (input_rows_count > 1) {
+ return Status::InvalidArgument("retry the failed conversion
batch");
+ }
+ }
+ auto status = Status::Error<error_code, false>("cast execution
failed");
+ if constexpr (throw_error) {
+ throw Exception(status);
+ } else {
+ return status;
+ }
+ }
+};
+
+template <bool nullable>
+struct TryCastTestOverflowImpl {
+ static Status execute_impl(FunctionContext* context, Block& block,
+ const ColumnNumbers& arguments, uint32_t result,
+ size_t input_rows_count) {
+ const auto& column = block.get_by_position(arguments[0]).column;
+ auto ret_col = ColumnInt32::create();
+ for (size_t row = 0; row < input_rows_count; ++row) {
+ auto value = column->get_int(row);
+ if (value == 0) {
+ return {ErrorCode::ARITHMETIC_OVERFLOW_ERRROR, "cast
overflow"};
+ }
+ ret_col->insert_value(value);
+ }
+ if constexpr (nullable) {
+ block.get_by_position(result).column = ColumnNullable::create(
+ std::move(ret_col), ColumnUInt8::create(input_rows_count,
0));
+ } else {
+ block.get_by_position(result).column = std::move(ret_col);
+ }
+ return Status::OK();
+ }
+};
+
class MockVExprForTryCast : public VExpr {
public:
MockVExprForTryCast() = default;
@@ -161,6 +219,19 @@ public:
std::string _expr_name;
};
+class MockBlockInputForTryCast : public MockVExprForTryCast {
+public:
+ Status execute_column_impl(VExprContext* context, const Block* block,
const Selector* selector,
+ size_t count, ColumnPtr& result_column) const
override {
+ result_column = block->get_by_position(0).column;
+ return Status::OK();
+ }
+
+ DataTypePtr execute_type(const Block* block) const override {
+ return block->get_by_position(0).type;
+ }
+};
+
struct TryCastExprTest : public ::testing::Test {
void SetUp() override {
try_cast_expr._data_type =
@@ -174,6 +245,63 @@ struct TryCastExprTest : public ::testing::Test {
context->_fn_contexts.push_back(nullptr);
}
+ void check_real_cast(const DataTypePtr& input_type, const DataTypePtr&
nested_result_type,
+ const std::vector<std::optional<std::string>>&
input_values,
+ const std::vector<std::optional<std::string>>&
expected_values,
+ bool strict, RuntimeState* state = nullptr) {
+ auto utc = cctz::utc_time_zone();
+ DataTypeSerDe::FormatOptions options;
+ options.timezone = state ? &state->timezone_obj() : &utc;
+ auto result_type = make_nullable(nested_result_type);
+ auto input_column = input_type->create_column();
+ auto expected_column = result_type->create_column();
+ auto fill_column = [&options](const DataTypePtr& type, IColumn& column,
+ const
std::vector<std::optional<std::string>>& values) {
+ auto serde = type->get_serde();
+ for (const auto& value : values) {
+ if (value.has_value()) {
+ StringRef text {*value};
+ auto status = serde->from_string_strict_mode(text, column,
options);
+ ASSERT_TRUE(status.ok()) << status;
+ } else {
+ ASSERT_TRUE(type->is_nullable());
+ column.insert_default();
+ }
+ }
+ };
+ ASSERT_NO_FATAL_FAILURE(fill_column(input_type, *input_column,
input_values));
+ ASSERT_NO_FATAL_FAILURE(fill_column(result_type, *expected_column,
expected_values));
+ if (input_type->is_nullable()) {
+ // Also exercise an outer NULL, independently of NULL elements in
a complex value.
+ input_column->insert_default();
+ expected_column->insert_default();
+ }
+
+ try_cast_expr._data_type = result_type;
+ try_cast_expr._original_cast_return_is_nullable =
input_type->is_nullable();
+ try_cast_expr._children[0] =
std::make_shared<MockBlockInputForTryCast>();
+ context->_fn_contexts[0] =
+ FunctionContext::create_context(state, result_type,
{input_type, result_type});
+ context->fn_context(0)->set_enable_strict_mode(strict);
+ ColumnsWithTypeAndName arguments {{input_column->get_ptr(),
input_type, "input"},
+ {nullptr, result_type, "target"}};
+ try_cast_expr._function =
+ SimpleFunctionFactory::instance().get_function("CAST",
arguments, result_type);
+ ASSERT_NE(try_cast_expr._function, nullptr);
+
+ Block block {arguments[0]};
+ ColumnPtr result;
+ auto status = try_cast_expr.execute_column(context.get(), &block,
nullptr,
+ input_column->size(),
result);
+ ASSERT_TRUE(status.ok()) << status;
+ ASSERT_EQ(result->size(), expected_column->size());
+ for (size_t row = 0; row < result->size(); ++row) {
+ EXPECT_EQ(result->compare_at(row, row, *expected_column, 1), 0)
+ << "row " << row << ", actual " <<
result_type->to_string(*result, row)
+ << ", expected " <<
result_type->to_string(*expected_column, row);
+ }
+ }
+
TryCastExpr try_cast_expr;
std::unique_ptr<VExprContext> context;
@@ -277,6 +405,160 @@ TEST_F(TryCastExprTest, row_exec3) {
EXPECT_FALSE(st.ok()) << st.msg();
}
+TEST_F(TryCastExprTest, arithmetic_overflow) {
+ auto check_overflow = [&]<bool nullable>() {
+ try_cast_expr._function = std::make_shared<DefaultFunction>(
+
try_cast_test_function<TryCastTestOverflowImpl<nullable>>::create(),
+ DataTypes {std::make_shared<DataTypeInt32>()},
std::make_shared<DataTypeInt32>());
+ try_cast_expr._original_cast_return_is_nullable = nullable;
+ for (size_t rows : {1, 3}) {
+ ColumnPtr result;
+ auto status = try_cast_expr.execute_column_impl(context.get(),
nullptr, nullptr, rows,
+ result);
+ ASSERT_TRUE(status.ok()) << status;
+ const auto& nullable_result = assert_cast<const
ColumnNullable&>(*result);
+ ASSERT_EQ(nullable_result.size(), rows);
+ EXPECT_TRUE(nullable_result.is_null_at(0));
+ for (size_t row = 1; row < rows; ++row) {
+ EXPECT_FALSE(nullable_result.is_null_at(row));
+ EXPECT_EQ(nullable_result.get_nested_column().get_int(row),
row);
+ }
+ }
+ };
+ check_overflow.template operator()<false>();
+ check_overflow.template operator()<true>();
+}
+
+TEST_F(TryCastExprTest, child_arithmetic_overflow) {
+ class OverflowChild : public MockVExprForTryCast {
+ Status execute_column_impl(VExprContext* context, const Block* block,
+ const Selector* selector, size_t count,
+ ColumnPtr& result_column) const override {
+ return {ErrorCode::ARITHMETIC_OVERFLOW_ERRROR, "child overflow"};
+ }
+ };
+ try_cast_expr._children[0] = std::make_shared<OverflowChild>();
+ ColumnPtr result;
+ auto status = try_cast_expr.execute_column_impl(context.get(), nullptr,
nullptr, 3, result);
+ EXPECT_TRUE(status.is<ErrorCode::ARITHMETIC_OVERFLOW_ERRROR>()) << status;
+}
+
+TEST_F(TryCastExprTest, execution_errors_are_not_conversion_failures) {
+ try_cast_expr._original_cast_return_is_nullable = false;
+ auto check_error = [&]<int error_code, bool retry, bool throw_error>() {
+ try_cast_expr._function = std::make_shared<DefaultFunction>(
+ try_cast_test_function<
+ TryCastTestExecutionErrorImpl<error_code, retry,
throw_error>>::create(),
+ DataTypes {std::make_shared<DataTypeInt32>()},
std::make_shared<DataTypeInt32>());
+ ColumnPtr result;
+ auto status = try_cast_expr.execute_column_impl(context.get(),
nullptr, nullptr, 3, result);
+ EXPECT_EQ(status.code(), error_code) << status;
+ EXPECT_EQ(status.msg(), "cast execution failed");
+ };
+ auto check_paths = [&]<int error_code>() {
+ // Exercise both direct Status returns and the function exception
boundary,
+ // before retry and during single-row retry, without exhausting real
resources.
+ check_error.template operator()<error_code, false, false>();
+ check_error.template operator()<error_code, false, true>();
+ check_error.template operator()<error_code, true, false>();
+ check_error.template operator()<error_code, true, true>();
+ };
+ check_paths.template operator()<ErrorCode::MEM_ALLOC_FAILED>();
+ check_paths.template operator()<ErrorCode::BUFFER_ALLOCATION_FAILED>();
+ check_paths.template operator()<ErrorCode::MEM_LIMIT_EXCEEDED>();
+ check_paths.template operator()<ErrorCode::QUERY_MEMORY_EXCEEDED>();
+ check_paths.template
operator()<ErrorCode::WORKLOAD_GROUP_MEMORY_EXCEEDED>();
+ check_paths.template operator()<ErrorCode::PROCESS_MEMORY_EXCEEDED>();
+ check_paths.template operator()<ErrorCode::INTERNAL_ERROR>();
+ check_paths.template operator()<ErrorCode::RUNTIME_ERROR>();
+ check_paths.template operator()<ErrorCode::CORRUPTION>();
+ check_paths.template operator()<ErrorCode::NOT_IMPLEMENTED_ERROR>();
+ check_paths.template operator()<ErrorCode::OUT_OF_BOUND>();
+ check_paths.template
operator()<ErrorCode::STRING_OVERFLOW_IN_VEC_ENGINE>();
+ check_paths.template operator()<ErrorCode::CANCELLED>();
+ check_paths.template operator()<ErrorCode::TIMEOUT>();
+}
+
+TEST_F(TryCastExprTest, real_cast_timestamptz_range_errors) {
+ RuntimeState state;
+ state._timezone = "UTC";
+ state._timezone_obj = cctz::utc_time_zone();
+ auto datetime6 = make_nullable(std::make_shared<DataTypeDateTimeV2>(6));
+ auto timestamptz6 =
make_nullable(std::make_shared<DataTypeTimeStampTz>(6));
+ auto datetime0 = std::make_shared<DataTypeDateTimeV2>(0);
+ auto timestamptz0 = std::make_shared<DataTypeTimeStampTz>(0);
+ for (bool strict : {false, true}) {
+ check_real_cast(datetime6, timestamptz0,
+ {"2024-01-01 00:00:00.123456", "9999-12-31
23:59:59.999999"},
+ {"2024-01-01 00:00:00+00:00", std::nullopt}, strict,
&state);
+ check_real_cast(timestamptz6, timestamptz0,
+ {"2024-01-01 00:00:00.123456+00:00", "9999-12-31
23:59:59.999999+00:00"},
+ {"2024-01-01 00:00:00+00:00", std::nullopt}, strict,
&state);
+ check_real_cast(timestamptz6, datetime0,
+ {"2024-01-01 00:00:00.123456+00:00", "9999-12-31
23:59:59.999999+00:00"},
+ {"2024-01-01 00:00:00", std::nullopt}, strict, &state);
+ }
+}
+
+TEST_F(TryCastExprTest, real_cast_jsonb_key_length_errors) {
+ auto string_type = make_nullable(std::make_shared<DataTypeString>());
+ auto map_type = make_nullable(std::make_shared<DataTypeMap>(string_type,
string_type));
+ auto jsonb_type = std::make_shared<DataTypeJsonb>();
+ const std::string valid_key(255, 'k');
+ const std::string invalid_key(256, 'k');
+ const auto valid_map = "{\"" + valid_key + "\":\"value\"}";
+ const auto invalid_map = "{\"" + invalid_key + "\":\"value\"}";
+ auto struct_type = make_nullable(
+ std::make_shared<DataTypeStruct>(DataTypes {string_type}, Strings
{invalid_key}));
+ for (bool strict : {false, true}) {
+ check_real_cast(map_type, jsonb_type, {valid_map, invalid_map, "{}"},
+ {valid_map, std::nullopt, "{}"}, strict);
+ check_real_cast(struct_type, jsonb_type, {invalid_map},
{std::nullopt}, strict);
+ }
+}
+
+TEST_F(TryCastExprTest, real_cast_array_decimal_overflow) {
+ auto input_type =
std::make_shared<DataTypeArray>(std::make_shared<DataTypeDecimal32>(6, 3));
+ auto result_type =
std::make_shared<DataTypeArray>(std::make_shared<DataTypeDecimal32>(4, 2));
+ for (bool nullable : {false, true}) {
+ DataTypePtr from_type = nullable ? make_nullable(input_type) :
input_type;
+ check_real_cast(from_type, result_type, {"[12.340]", "[123.456]",
"[null]", "[]"},
+ {"[12.34]", std::nullopt, "[null]", "[]"}, true);
+ check_real_cast(from_type, result_type, {"[12.340]", "[123.456]",
"[null]", "[]"},
+ {"[12.34]", "[null]", "[null]", "[]"}, false);
+ }
+}
+
+TEST_F(TryCastExprTest, real_cast_map_integer_overflow) {
+ auto input_type =
+
std::make_shared<DataTypeMap>(make_nullable(std::make_shared<DataTypeInt32>()),
+
make_nullable(std::make_shared<DataTypeInt32>()));
+ auto result_type =
+
std::make_shared<DataTypeMap>(make_nullable(std::make_shared<DataTypeInt32>()),
+
make_nullable(std::make_shared<DataTypeInt8>()));
+ for (bool nullable : {false, true}) {
+ DataTypePtr from_type = nullable ? make_nullable(input_type) :
input_type;
+ check_real_cast(from_type, result_type, {"{1:12}", "{1:128}",
"{1:null}", "{}"},
+ {"{1:12}", std::nullopt, "{1:null}", "{}"}, true);
+ check_real_cast(from_type, result_type, {"{1:12}", "{1:128}",
"{1:null}", "{}"},
+ {"{1:12}", "{1:null}", "{1:null}", "{}"}, false);
+ }
+}
+
+TEST_F(TryCastExprTest, real_cast_struct_integer_overflow) {
+ auto input_type = std::make_shared<DataTypeStruct>(
+ DataTypes {make_nullable(std::make_shared<DataTypeInt32>())},
Strings {"v"});
+ auto result_type = std::make_shared<DataTypeStruct>(
+ DataTypes {make_nullable(std::make_shared<DataTypeInt8>())},
Strings {"v"});
+ for (bool nullable : {false, true}) {
+ DataTypePtr from_type = nullable ? make_nullable(input_type) :
input_type;
+ check_real_cast(from_type, result_type, {"{v:12}", "{v:128}",
"{v:null}"},
+ {"{v:12}", std::nullopt, "{v:null}"}, true);
+ check_real_cast(from_type, result_type, {"{v:12}", "{v:128}",
"{v:null}"},
+ {"{v:12}", "{v:null}", "{v:null}"}, false);
+ }
+}
+
TEST_F(TryCastExprTest, selected_row_safety) {
VCastExpr cast_expr;
cast_expr.add_child(std::make_shared<MockVExprForTryCast>());
diff --git
a/regression-test/data/function_p0/cast/test_try_cast_decimal_overflow.out
b/regression-test/data/function_p0/cast/test_try_cast_decimal_overflow.out
new file mode 100644
index 00000000000..b96fc85f311
--- /dev/null
+++ b/regression-test/data/function_p0/cast/test_try_cast_decimal_overflow.out
@@ -0,0 +1,22 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !valid --
+1 12.340 12.34
+
+-- !overflow --
+2 123.456 \N
+
+-- !batch --
+1 12.340 12.34 12.34 12.34 12.34 \N 12.3400
+2 123.456 \N \N \N \N \N \N
+3 -123.456 \N \N \N \N \N \N
+4 99.994 99.99 99.99 99.99 99.99 \N 99.9940
+5 99.995 \N \N \N \N \N 99.9950
+6 -99.995 \N \N \N \N \N -99.9950
+7 0.000 0.00 \N 0.00 0.00 0.000 0.0000
+
+-- !const --
+12.34 \N
+
+-- !valid --
+1 12.340 12.34
+
diff --git
a/regression-test/suites/function_p0/cast/test_try_cast_complex_overflow.groovy
b/regression-test/suites/function_p0/cast/test_try_cast_complex_overflow.groovy
new file mode 100644
index 00000000000..2e09a0aaf3c
--- /dev/null
+++
b/regression-test/suites/function_p0/cast/test_try_cast_complex_overflow.groovy
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_try_cast_complex_overflow") {
+ sql "DROP TABLE IF EXISTS test_try_cast_complex_overflow"
+ sql """
+ CREATE TABLE test_try_cast_complex_overflow (
+ id INT NOT NULL,
+ a ARRAY<DECIMAL(6,3)> NOT NULL,
+ m MAP<INT,INT> NOT NULL,
+ s STRUCT<v:INT> NOT NULL
+ ) DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO test_try_cast_complex_overflow VALUES
+ (1, [12.340], MAP(1, 12), NAMED_STRUCT('v', 12)),
+ (2, [123.456], MAP(1, 128), NAMED_STRUCT('v', 128)),
+ (3, [NULL], MAP(1, NULL), NAMED_STRUCT('v', NULL)),
+ (4, [], MAP(), NAMED_STRUCT('v', 0)),
+ (5, [-12.340], MAP(1, -12), NAMED_STRUCT('v', -12)),
+ (6, [-123.456], MAP(1, -129), NAMED_STRUCT('v', -129))
+ """
+ sql "SET enable_sql_cache=false"
+
+ def targetTypes = [a: "ARRAY<DECIMAL(4,2)>", m: "MAP<INT,TINYINT>", s:
"STRUCT<v:TINYINT>"]
+ targetTypes.each { columnName, targetType ->
+ sql "SET enable_strict_cast=true"
+ // A failed nested conversion makes the whole row NULL in strict
TRY_CAST.
+ // Build the reference with ordinary CAST applied only to valid rows.
+ check_sqls_result_equal("""
+ SELECT id, TRY_CAST(${columnName} AS ${targetType}) AS converted
+ FROM test_try_cast_complex_overflow ORDER BY id
+ """, """
+ SELECT id, CAST(${columnName} AS ${targetType}) AS converted
+ FROM test_try_cast_complex_overflow WHERE id NOT IN (2, 6)
+ UNION ALL
+ SELECT id, CAST(NULL AS ${targetType}) AS converted
+ FROM test_try_cast_complex_overflow WHERE id IN (2, 6)
+ ORDER BY id
+ """)
+ test {
+ sql """
+ SELECT CAST(${columnName} AS ${targetType})
+ FROM test_try_cast_complex_overflow WHERE id = 2
+ """
+ exception(columnName == "a" ? "Arithmetic overflow" : "Value 128
out of range")
+ }
+
+ sql "SET enable_strict_cast=false"
+ // Non-strict conversion preserves the container and nulls only the
invalid element.
+ check_sqls_result_equal("""
+ SELECT id, TRY_CAST(${columnName} AS ${targetType}) AS converted
+ FROM test_try_cast_complex_overflow ORDER BY id
+ """, """
+ SELECT id, CAST(${columnName} AS ${targetType}) AS converted
+ FROM test_try_cast_complex_overflow ORDER BY id
+ """)
+ }
+}
diff --git
a/regression-test/suites/function_p0/cast/test_try_cast_conversion_errors.groovy
b/regression-test/suites/function_p0/cast/test_try_cast_conversion_errors.groovy
new file mode 100644
index 00000000000..3baf89b6f3e
--- /dev/null
+++
b/regression-test/suites/function_p0/cast/test_try_cast_conversion_errors.groovy
@@ -0,0 +1,73 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_try_cast_conversion_errors") {
+ sql "SET time_zone = '+00:00'"
+ sql "SET enable_sql_cache = false"
+ sql "DROP TABLE IF EXISTS test_try_cast_conversion_errors"
+ sql """
+ CREATE TABLE test_try_cast_conversion_errors (
+ id INT NOT NULL,
+ dt DATETIME(6),
+ tz TIMESTAMPTZ(6)
+ ) DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO test_try_cast_conversion_errors VALUES
+ (1, '2024-01-01 00:00:00.123456', '2024-01-01
00:00:00.123456+00:00'),
+ (2, '9999-12-31 23:59:59.999999', '9999-12-31
23:59:59.999999+00:00'),
+ (3, NULL, NULL),
+ (4, '2024-06-01 12:34:56.999999', '2024-06-01
12:34:56.999999+00:00')
+ """
+
+ def conversions = [
+ [source: "dt", target: "TIMESTAMPTZ(0)", zone: "+00:00", error: "can
not cast"],
+ [source: "tz", target: "TIMESTAMPTZ(0)", zone: "+00:00", error: "can
not cast"],
+ [source: "tz", target: "DATETIME(0)", zone: "+00:00", error: "can not
cast"],
+ // The precision is unchanged here; conversion overflows because of
the time zone.
+ [source: "dt", target: "TIMESTAMPTZ(6)", zone: "-01:00", error: "can
not cast"],
+ [source: "tz", target: "DATETIME(6)", zone: "+01:00", error: "can not
cast"]
+ ]
+ conversions.each { conversion ->
+ sql "SET time_zone = '${conversion.zone}'"
+ [true, false].each { strict ->
+ sql "SET enable_strict_cast = ${strict}"
+ // Only the failed conversion becomes NULL; preserve valid rows
and source NULLs.
+ check_sqls_result_equal("""
+ SELECT id, TRY_CAST(${conversion.source} AS
${conversion.target}) AS converted
+ FROM test_try_cast_conversion_errors ORDER BY id
+ """, """
+ SELECT id, CAST(${conversion.source} AS ${conversion.target})
AS converted
+ FROM test_try_cast_conversion_errors WHERE id != 2
+ UNION ALL
+ SELECT id, CAST(NULL AS ${conversion.target}) AS converted
+ FROM test_try_cast_conversion_errors WHERE id = 2
+ ORDER BY id
+ """)
+ }
+ sql "SET enable_strict_cast = true"
+ test {
+ sql """
+ SELECT CAST(${conversion.source} AS ${conversion.target})
+ FROM test_try_cast_conversion_errors WHERE id = 2
+ """
+ exception conversion.error
+ }
+ }
+}
diff --git
a/regression-test/suites/function_p0/cast/test_try_cast_decimal_overflow.groovy
b/regression-test/suites/function_p0/cast/test_try_cast_decimal_overflow.groovy
new file mode 100644
index 00000000000..fd8446887a5
--- /dev/null
+++
b/regression-test/suites/function_p0/cast/test_try_cast_decimal_overflow.groovy
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_try_cast_decimal_overflow") {
+ sql """DROP TABLE IF EXISTS test_try_cast_decimal_overflow"""
+ sql """
+ CREATE TABLE test_try_cast_decimal_overflow (
+ id INT NOT NULL,
+ d DECIMAL(6,3) NOT NULL,
+ nullable_d DECIMAL(6,3),
+ d64 DECIMAL(12,3) NOT NULL,
+ d128 DECIMAL(30,3) NOT NULL
+ ) DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_num" = "1")
+ """
+ sql """
+ INSERT INTO test_try_cast_decimal_overflow VALUES
+ (1, 12.340, 12.340, 12.340, 12.340),
+ (2, 123.456, 123.456, 123.456, 123.456),
+ (3, -123.456, -123.456, -123.456, -123.456),
+ (4, 99.994, 99.994, 99.994, 99.994),
+ (5, 99.995, 99.995, 99.995, 99.995),
+ (6, -99.995, -99.995, -99.995, -99.995),
+ (7, 0, NULL, 0, 0)
+ """
+
+ sql "SET enable_sql_cache=false"
+ sql "SET debug_skip_fold_constant=true"
+
+ def queries = [
+ valid: """
+ SELECT id, d, TRY_CAST(d AS DECIMAL(4,2))
+ FROM test_try_cast_decimal_overflow WHERE id = 1 ORDER BY id
+ """,
+ overflow: """
+ SELECT id, d, TRY_CAST(d AS DECIMAL(4,2))
+ FROM test_try_cast_decimal_overflow WHERE id = 2 ORDER BY id
+ """,
+ batch: """
+ SELECT id, d, TRY_CAST(d AS DECIMAL(4,2)),
+ TRY_CAST(nullable_d AS DECIMAL(4,2)),
+ TRY_CAST(d64 AS DECIMAL(4,2)), TRY_CAST(d128 AS
DECIMAL(4,2)),
+ TRY_CAST(d AS DECIMAL(4,3)), TRY_CAST(d AS DECIMAL(6,4))
+ FROM test_try_cast_decimal_overflow ORDER BY id
+ """,
+ "const": """
+ SELECT TRY_CAST(CAST(12.340 AS DECIMAL(6,3)) AS DECIMAL(4,2)),
+ TRY_CAST(CAST(123.456 AS DECIMAL(6,3)) AS DECIMAL(4,2))
+ """
+ ]
+
+ // Check the non-strict baseline against the recorded results first.
+ sql "SET enable_strict_cast=false"
+ queries.each { tag, query -> quickTest(tag, query) }
+
+ sql "SET enable_strict_cast=true"
+ qt_valid queries.valid
+ explain {
+ verbose true
+ sql queries.overflow
+ contains "TRY_CAST"
+ }
+ // TRY_CAST must preserve the non-strict results even when strict CAST
would fail.
+ queries.values().each { query ->
+ check_sqls_result_equal(query,
+ query.replaceFirst("SELECT", "SELECT /*+
SET_VAR(enable_strict_cast=false) */"))
+ }
+ check_sqls_result_equal("""
+ SELECT id, d, TRY_CAST(d AS DECIMAL(4,2))
+ FROM test_try_cast_decimal_overflow WHERE id <= 2 ORDER BY id
+ """, """
+ SELECT /*+ SET_VAR(enable_strict_cast=false) */ id, d, TRY_CAST(d AS
DECIMAL(4,2))
+ FROM test_try_cast_decimal_overflow WHERE id <= 2 ORDER BY id
+ """)
+
+ test {
+ sql """
+ SELECT CAST(d AS DECIMAL(4,2))
+ FROM test_try_cast_decimal_overflow WHERE id = 2
+ """
+ exception "Arithmetic overflow when converting value 123.456"
+ }
+ // TRY_CAST must not suppress errors raised while evaluating its child.
+ test {
+ sql """
+ SELECT TRY_CAST(CAST(d AS DECIMAL(4,2)) AS DECIMAL(6,3))
+ FROM test_try_cast_decimal_overflow WHERE id = 2
+ """
+ exception "Arithmetic overflow when converting value 123.456"
+ }
+
+ sql "SET enable_strict_cast=false"
+ check_sqls_result_equal(queries.batch,
+ queries.batch.replaceFirst("SELECT", "SELECT /*+
SET_VAR(enable_strict_cast=true) */"))
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]