diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc index ba2a8c4b5638..1a1816db55ed 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc @@ -675,9 +675,13 @@ struct StringToDecimal { if (ARROW_PREDICT_TRUE(r_parse.ok())) { if (allow_truncate_) { - return (parsed_scale < out_scale_) - ? parsed_out.IncreaseScaleBy(out_scale_ - parsed_scale) - : parsed_out.ReduceScaleBy(parsed_scale - out_scale_, false); + if (parsed_scale < out_scale_) { + return parsed_out.IncreaseScaleBy(out_scale_ - parsed_scale); + } + if (parsed_scale - out_scale_ > OutValue::kMaxScale) { + return {}; // Zero: every digit is truncated + } + return parsed_out.ReduceScaleBy(parsed_scale - out_scale_, false); } auto maybe_rescaled = parsed_out.Rescale(parsed_scale, out_scale_); diff --git a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc index 864ec4afce0c..847e1b19831d 100644 --- a/cpp/src/arrow/compute/kernels/scalar_cast_test.cc +++ b/cpp/src/arrow/compute/kernels/scalar_cast_test.cc @@ -3123,6 +3123,17 @@ TEST(Cast, StringToDecimal) { CheckCastFails(ArrayFromJSON(string_type, "[\"" + not_decimal + "\"]"), options); } + // Parsed scale far beyond the type's maximum scale + auto tiny = ArrayFromJSON(string_type, R"(["1E-100000", "-1E-100000"])"); + CheckCastFails(tiny, CastOptions::Safe(decimal_type)); + CheckCast(ArrayFromJSON(string_type, R"(["0E-100000"])"), + ArrayFromJSON(decimal_type, R"(["0.00"])")); + auto truncate_options = CastOptions::Safe(decimal_type); + truncate_options.allow_decimal_truncate = true; + ASSERT_OK_AND_ASSIGN(auto truncated, Cast(tiny, truncate_options)); + AssertArraysEqual(*ArrayFromJSON(decimal_type, R"(["0.00", "0.00"])"), + *truncated.make_array(), /*verbose=*/true); + #if !defined(_WIN32) || defined(NDEBUG) // Test that casting is locale-independent // French locale uses the comma as decimal point diff --git a/cpp/src/arrow/csv/converter_test.cc b/cpp/src/arrow/csv/converter_test.cc index 5dc078e7fd80..aba24d27b49d 100644 --- a/cpp/src/arrow/csv/converter_test.cc +++ b/cpp/src/arrow/csv/converter_test.cc @@ -734,6 +734,12 @@ TEST(DecimalConversion, Basics) { AssertConversion( decimal128(23, 2), {"12,34.5\n", "36.37,-1e5\n"}, {{Dec128("12.00"), Dec128("36.37")}, {Dec128("34.50"), Dec128("-100000.00")}}); + + // Scale far beyond the maximum decimal128 scale + AssertConversion(decimal128(10, 2), {"0E-100000\n"}, + {{Dec128("0.00")}}); + AssertConversionError(decimal128(10, 2), {"1E-41\n"}, {0}); + AssertConversionError(decimal128(10, 2), {"1E-100000\n"}, {0}); } TEST(DecimalConversion, Nulls) { diff --git a/cpp/src/arrow/util/basic_decimal.cc b/cpp/src/arrow/util/basic_decimal.cc index 9e3de37f3f9f..c929beebb4c8 100644 --- a/cpp/src/arrow/util/basic_decimal.cc +++ b/cpp/src/arrow/util/basic_decimal.cc @@ -1063,6 +1063,16 @@ DecimalStatus DecimalRescale(const DecimalClass& value, int32_t original_scale, const int32_t delta_scale = new_scale - original_scale; const int32_t abs_delta_scale = std::abs(delta_scale); + // No multiplier this large fits in the type (nor in the powers-of-ten table), + // so only zero can be rescaled without data loss. + if (ARROW_PREDICT_FALSE(abs_delta_scale > DecimalClass::kMaxScale)) { + if (value == 0) { + *out = value; + return DecimalStatus::kSuccess; + } + return DecimalStatus::kRescaleDataLoss; + } + DecimalClass multiplier = DecimalClass::GetScaleMultiplier(abs_delta_scale); const bool rescale_would_cause_data_loss = diff --git a/cpp/src/arrow/util/decimal_test.cc b/cpp/src/arrow/util/decimal_test.cc index 7022c8117802..6592cb2204ca 100644 --- a/cpp/src/arrow/util/decimal_test.cc +++ b/cpp/src/arrow/util/decimal_test.cc @@ -1568,6 +1568,13 @@ TYPED_TEST(TestBasicDecimalFunctionality, Rescale) { ASSERT_OK_AND_EQ(TypeParam(500000), TypeParam(5).Rescale(1, 6)); ASSERT_RAISES(Invalid, TypeParam(555555).Rescale(6, 1)); + // Scale deltas larger than kMaxScale + constexpr int32_t kMaxScale = TypeParam::kMaxScale; + ASSERT_RAISES(Invalid, TypeParam(1).Rescale(kMaxScale + 1, 0)); + ASSERT_RAISES(Invalid, TypeParam(1).Rescale(0, kMaxScale + 1)); + ASSERT_RAISES(Invalid, TypeParam(-1).Rescale(100000, 2)); + ASSERT_OK_AND_EQ(TypeParam(0), TypeParam(0).Rescale(100000, 2)); + using OrigScaleType = std::conditional_t, Int8Type, Int16Type>; using ValueType = @@ -2369,6 +2376,12 @@ TEST(Decimal256Test, Rescale) { ASSERT_OK_AND_EQ(Decimal256(500000), Decimal256(5).Rescale(1, 6)); ASSERT_RAISES(Invalid, Decimal256(555555).Rescale(6, 1)); + // Scale deltas larger than kMaxScale + ASSERT_RAISES(Invalid, Decimal256(1).Rescale(Decimal256::kMaxScale + 1, 0)); + ASSERT_RAISES(Invalid, Decimal256(1).Rescale(0, Decimal256::kMaxScale + 1)); + ASSERT_RAISES(Invalid, Decimal256(-1).Rescale(100000, 2)); + ASSERT_OK_AND_EQ(Decimal256(0), Decimal256(0).Rescale(100000, 2)); + // Test some random numbers. for (auto original_scale : GetRandomNumbers(16)) { for (auto value : GetRandomNumbers(16)) {