diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc index ba2a8c4b563..2d44e6d8a37 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc @@ -490,6 +490,36 @@ struct DecimalConversions { static Decimal32 ConvertOutput(Decimal32&& val) { return val; } }; +// Safe casts to Decimal32 need to rescale the full input coefficient before +// narrowing it. Otherwise, high bits can be lost before overflow and precision +// checks run. +template +struct SafeDecimalConversions : DecimalConversions {}; + +template <> +struct SafeDecimalConversions { + static Decimal64 ConvertInput(Decimal64&& val) { return val; } + static Decimal32 ConvertOutput(Decimal64&& val) { + return Decimal32(static_cast(val.low_bits())); + } +}; + +template <> +struct SafeDecimalConversions { + static Decimal128 ConvertInput(Decimal128&& val) { return val; } + static Decimal32 ConvertOutput(Decimal128&& val) { + return Decimal32(static_cast(val.low_bits())); + } +}; + +template <> +struct SafeDecimalConversions { + static Decimal256 ConvertInput(Decimal256&& val) { return val; } + static Decimal32 ConvertOutput(Decimal256&& val) { + return Decimal32(static_cast(val.low_bits())); + } +}; + template <> struct DecimalConversions { // Convert then scale @@ -573,7 +603,7 @@ struct UnsafeDownscaleDecimal { struct SafeRescaleDecimal { template OutValue Call(KernelContext*, Arg0Value val, Status* st) const { - using Conv = DecimalConversions; + using Conv = SafeDecimalConversions; auto maybe_rescaled = Conv::ConvertInput(std::move(val)).Rescale(in_scale_, out_scale_); if (ARROW_PREDICT_FALSE(!maybe_rescaled.ok())) { diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 864ec4afce0..6aa1f5c0fa1 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -1646,6 +1646,24 @@ TEST(Cast, Decimal64ToDecimal256) { TEST(Cast, Decimal128ToDecimal32) { CastOptions options; + // Safe downcasts must check the value after rescaling the full input + // coefficient, before narrowing it to Decimal32. + auto d128_12_0_overflow = ArrayFromJSON(decimal128(12, 0), R"(["5000000000"])"); + options.to_type = decimal32(9, 0); + CheckCastFails(d128_12_0_overflow, options); + + auto d128_12_0_rescaled = ArrayFromJSON(decimal128(12, 0), R"(["536870912000"])"); + Decimal32Builder d32_9_neg3_builder(decimal32(9, -3)); + ASSERT_OK(d32_9_neg3_builder.Append(Decimal32(536870912))); + ASSERT_OK_AND_ASSIGN(auto d32_9_neg3, d32_9_neg3_builder.Finish()); + options.to_type = d32_9_neg3->type(); + CheckCast(d128_12_0_rescaled, d32_9_neg3, options); + + auto d128_10_2_rescaled = ArrayFromJSON(decimal128(10, 2), R"(["30000000.00"])"); + auto d32_9_0 = ArrayFromJSON(decimal32(9, 0), R"(["30000000"])"); + options.to_type = d32_9_0->type(); + CheckCast(d128_10_2_rescaled, d32_9_0, options); + for (bool allow_decimal_truncate : {false, true}) { options.allow_decimal_truncate = allow_decimal_truncate;