Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions cpp/src/arrow/compute/kernels/scalar_cast_numeric.cc
Original file line number Diff line number Diff line change
Expand Up @@ -486,8 +486,12 @@ struct DecimalConversions<Decimal256, InDecimal> {

template <typename InDecimal>
struct DecimalConversions<Decimal32, InDecimal> {
static Decimal32 ConvertInput(InDecimal&& val) { return Decimal32(val.low_bits()); }
static Decimal32 ConvertOutput(Decimal32&& val) { return val; }
// Scale then truncate. Narrowing in ConvertInput would drop coefficient bits
// before Rescale and FitsInPrecision could see them.
static InDecimal ConvertInput(InDecimal&& val) { return val; }
static Decimal32 ConvertOutput(InDecimal&& val) {
return Decimal32(static_cast<int32_t>(val.low_bits()));
}
};

template <>
Expand Down
39 changes: 39 additions & 0 deletions cpp/src/arrow/compute/kernels/scalar_cast_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1726,6 +1726,45 @@ TEST(Cast, Decimal128ToDecimal32) {
}
}

// GH-53288: narrowing to Decimal32 must rescale and range-check the full-width
// coefficient. Truncating to 32 bits first silently wrapped the value. One
// specialization serves every source width, so all of them were affected.
TEST(Cast, DecimalToDecimal32NarrowingChecksFullCoefficient) {
CastOptions options;
options.allow_decimal_truncate = false;

// Ten-digit coefficient does not fit decimal32(9, 0). Truncating first yielded
// 5000000000 - 2^32 = 705032704, which does fit, so the cast wrongly succeeded.
auto too_wide = ArrayFromJSON(decimal128(12, 0), R"(["5000000000."])");
options.to_type = decimal32(9, 0);
CheckCastFails(too_wide, options);

// 536870912000 is exactly 125 * 2^32, so the low 32 bits are zero. Rescaling by
// -3 first gives 536870912, which fits decimal32(9, -3) and must be preserved.
Decimal32Builder builder(decimal32(9, -3));
ASSERT_OK(builder.Append(Decimal32(536870912))); // 536870912 * 10^3
ASSERT_OK_AND_ASSIGN(auto expected_negative_scale, builder.Finish());

auto negative_scale_input = ArrayFromJSON(decimal128(12, 0), R"(["536870912000."])");
CheckCast(negative_scale_input, expected_negative_scale, options);

// 30000000.00 has an unscaled value of 3000000000, which wraps to a negative
// int32. The wrapped value is no longer divisible by 100, so the exact rescale
// was rejected as a data loss.
auto exact_rescale = ArrayFromJSON(decimal128(12, 2), R"(["30000000.00"])");
CheckCast(exact_rescale, ArrayFromJSON(decimal32(9, 0), R"(["30000000."])"), options);

// The same specialization serves every source width, so check the other two.
options.to_type = decimal32(9, 0);
CheckCastFails(ArrayFromJSON(decimal64(18, 0), R"(["5000000000."])"), options);
CheckCastFails(ArrayFromJSON(decimal256(40, 0), R"(["5000000000."])"), options);

// With truncation allowed the rescale now also happens at the source width, so
// the downscale sees 536870912000 rather than its low 32 bits.
options.allow_decimal_truncate = true;
CheckCast(negative_scale_input, expected_negative_scale, options);
}

TEST(Cast, Decimal128ToDecimal64) {
CastOptions options;

Expand Down
Loading