Skip to content

Commit 827e968

Browse files
committed
parse real coefficient directly to avoid int128 overflow on large-magnitude decimal casts
1 parent 9f2ecdb commit 827e968

2 files changed

Lines changed: 78 additions & 6 deletions

File tree

‎src/iceberg/expression/literal.cc‎

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,18 @@ Result<Decimal> RescaleHalfUp(const Decimal& unscaled, int32_t from_scale,
130130
return unscaled;
131131
}
132132
if (delta > 0) {
133-
// Growing the scale multiplies by 10^delta and is exact; Rescale rejects overflow.
133+
// Growing the scale multiplies the coefficient by 10^delta. Report an overflow as an
134+
// invalid argument (consistent with the other rejections here) rather than leaking
135+
// Rescale's "data loss" error to callers.
134136
if (delta > Decimal::kMaxScale) {
135137
return InvalidArgument("scale change {} exceeds the maximum {}", delta,
136138
Decimal::kMaxScale);
137139
}
138-
return unscaled.Rescale(from_scale, to_scale);
140+
auto rescaled = unscaled.Rescale(from_scale, to_scale);
141+
if (!rescaled.has_value()) {
142+
return InvalidArgument("value does not fit the target decimal scale");
143+
}
144+
return rescaled.value();
139145
}
140146
// Shrinking the scale drops `drop` digits with HALF_UP rounding. A drop larger than the
141147
// digits any decimal can hold rounds everything away, so the result is zero (e.g.
@@ -157,6 +163,55 @@ Result<Decimal> RescaleHalfUp(const Decimal& unscaled, int32_t from_scale,
157163
return quotient;
158164
}
159165

166+
// Parse a `std::to_chars` double rendering (`[-]digits[.digits][e[+-]exp]`) into an
167+
// integer coefficient and the scale at which it is interpreted (value == coefficient *
168+
// 10^-scale). The coefficient is just the significant digits, so it never overflows
169+
// int128; the exponent is folded into the returned scale rather than expanded, leaving
170+
// RescaleHalfUp to combine it with the target scale.
171+
Result<Decimal> ParseRealCoefficient(std::string_view str, int32_t* scale) {
172+
bool negative = !str.empty() && str.front() == '-';
173+
if (negative) {
174+
str.remove_prefix(1);
175+
}
176+
177+
int32_t exponent = 0;
178+
if (auto e = str.find_first_of("eE"); e != std::string_view::npos) {
179+
auto exp_str = str.substr(e + 1);
180+
// std::to_chars writes a leading '+' for positive exponents, which from_chars
181+
// rejects.
182+
if (!exp_str.empty() && exp_str.front() == '+') {
183+
exp_str.remove_prefix(1);
184+
}
185+
if (std::from_chars(exp_str.data(), exp_str.data() + exp_str.size(), exponent).ec !=
186+
std::errc{}) {
187+
return InvalidArgument("Invalid real value '{}'", str);
188+
}
189+
str = str.substr(0, e);
190+
}
191+
192+
int32_t frac_digits = 0;
193+
std::string digits;
194+
digits.reserve(str.size());
195+
if (auto dot = str.find('.'); dot != std::string_view::npos) {
196+
frac_digits = static_cast<int32_t>(str.size() - dot - 1);
197+
digits.append(str.substr(0, dot));
198+
digits.append(str.substr(dot + 1));
199+
} else {
200+
digits.append(str);
201+
}
202+
if (digits.empty() || digits.find_first_not_of("0123456789") != std::string::npos) {
203+
return InvalidArgument("Invalid real value '{}'", str);
204+
}
205+
206+
// Coefficient has scale `frac_digits`; a positive base-10 exponent lowers that scale.
207+
*scale = frac_digits - exponent;
208+
ICEBERG_ASSIGN_OR_RAISE(auto coefficient, Decimal::FromString(digits));
209+
if (negative) {
210+
coefficient.Negate();
211+
}
212+
return coefficient;
213+
}
214+
160215
} // namespace
161216

162217
Result<Literal> LiteralCaster::CastIntegerToDecimal(
@@ -193,12 +248,18 @@ Result<Literal> LiteralCaster::CastRealToDecimal(
193248
return InvalidArgument("Cannot cast {} as a {} value", value,
194249
target_type->ToString());
195250
}
196-
int32_t parsed_scale = 0;
251+
252+
// Parse the coefficient and its scale directly instead of via Decimal::FromString,
253+
// which normalizes a negative scale by multiplying the coefficient by 10^-scale and can
254+
// overflow int128 for large magnitudes (e.g. 4e38). The coefficient itself always fits,
255+
// and RescaleHalfUp handles the exponent against the target scale (and rejects
256+
// overflow).
257+
int32_t coeff_scale = 0;
197258
ICEBERG_ASSIGN_OR_RAISE(
198-
auto parsed,
199-
Decimal::FromString(std::string_view(buf.data(), ptr), nullptr, &parsed_scale));
259+
auto coefficient,
260+
ParseRealCoefficient(std::string_view(buf.data(), ptr), &coeff_scale));
200261

201-
ICEBERG_ASSIGN_OR_RAISE(auto unscaled, RescaleHalfUp(parsed, parsed_scale,
262+
ICEBERG_ASSIGN_OR_RAISE(auto unscaled, RescaleHalfUp(coefficient, coeff_scale,
202263
decimal_type.scale(), value < 0));
203264
if (!unscaled.FitsInPrecision(decimal_type.precision())) {
204265
return InvalidArgument("Cannot cast {} as a {} value", value,

‎src/iceberg/test/literal_test.cc‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,17 @@ TEST(LiteralTest, RealCastToDecimal) {
235235
auto neg_scale = Literal::Double(149.0).CastTo(decimal(9, -2));
236236
ASSERT_THAT(neg_scale, IsOk());
237237
EXPECT_EQ(*neg_scale, Literal::Decimal(1, 9, -2));
238+
239+
// A large magnitude must be rejected for exceeding precision, not wrap the int128
240+
// coefficient: 4e38 needs 39 digits, over decimal(38, 0)'s precision.
241+
EXPECT_THAT(Literal::Double(4e38).CastTo(decimal(38, 0)),
242+
IsError(ErrorKind::kInvalidArgument));
243+
244+
// A large magnitude that IS representable at a negative scale is accepted: 1e39 as
245+
// decimal(2, -38) is the unscaled value 10 (10 * 10^38).
246+
auto big_neg_scale = Literal::Double(1e39).CastTo(decimal(2, -38));
247+
ASSERT_THAT(big_neg_scale, IsOk());
248+
EXPECT_EQ(*big_neg_scale, Literal::Decimal(10, 2, -38));
238249
}
239250

240251
// Error cases for casts

0 commit comments

Comments
 (0)