fix: correct decoding of rust_decimal::Decimal for high-precision values (#2820)

also fixes handling of feature flags
This commit is contained in:
Austin Bonander
2023-10-16 14:03:28 -07:00
committed by GitHub
parent 540baf7df5
commit 58cb18a47a
5 changed files with 34 additions and 38 deletions

View File

@@ -16,7 +16,7 @@ migrate = ["sqlx-core/migrate"]
offline = ["sqlx-core/offline"]
# Type integration features which require additional dependencies
rust_decimal = ["dep:rust_decimal", "dep:num-bigint"]
rust_decimal = ["dep:rust_decimal", "rust_decimal/maths"]
bigdecimal = ["dep:bigdecimal", "dep:num-bigint"]
[dependencies]

View File

@@ -1,8 +1,4 @@
use num_bigint::{BigInt, Sign};
use rust_decimal::{
prelude::{ToPrimitive, Zero},
Decimal,
};
use rust_decimal::{prelude::Zero, Decimal};
use crate::decode::Decode;
use crate::encode::{Encode, IsNull};
@@ -11,6 +7,8 @@ use crate::types::numeric::{PgNumeric, PgNumericSign};
use crate::types::Type;
use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres};
use rust_decimal::MathematicalOps;
impl Type<Postgres> for Decimal {
fn type_info() -> PgTypeInfo {
PgTypeInfo::NUMERIC
@@ -27,7 +25,7 @@ impl TryFrom<PgNumeric> for Decimal {
type Error = BoxDynError;
fn try_from(numeric: PgNumeric) -> Result<Self, BoxDynError> {
let (digits, sign, weight) = match numeric {
let (digits, sign, mut weight) = match numeric {
PgNumeric::Number {
digits,
sign,
@@ -41,39 +39,33 @@ impl TryFrom<PgNumeric> for Decimal {
};
if digits.is_empty() {
// Postgres returns an empty digit array for 0 but BigInt expects at least one zero
// Postgres returns an empty digit array for 0
return Ok(0u64.into());
}
let sign = match sign {
PgNumericSign::Positive => Sign::Plus,
PgNumericSign::Negative => Sign::Minus,
};
let mut value = Decimal::ZERO;
// weight is 0 if the decimal point falls after the first base-10000 digit
let scale = (digits.len() as i64 - weight as i64 - 1) * 4;
// Sum over `digits`, multiply each by its weight and add it to `value`.
for digit in digits {
let mul = Decimal::from(10_000i16)
.checked_powi(weight as i64)
.ok_or("value not representable as rust_decimal::Decimal")?;
// no optimized algorithm for base-10 so use base-100 for faster processing
let mut cents = Vec::with_capacity(digits.len() * 2);
for digit in &digits {
cents.push((digit / 100) as u8);
cents.push((digit % 100) as u8);
let part = Decimal::from(digit) * mul;
value = value
.checked_add(part)
.ok_or("value not representable as rust_decimal::Decimal")?;
weight = weight.checked_sub(1).ok_or("weight underflowed")?;
}
let bigint = BigInt::from_radix_be(sign, &cents, 100)
.ok_or("PgNumeric contained an out-of-range digit")?;
match (bigint.to_i128(), scale) {
// A negative scale, meaning we have nothing on the right and must
// add zeroes to the left.
(Some(num), scale) if scale < 0 => Ok(Decimal::from_i128_with_scale(
num * 10i128.pow(scale.abs() as u32),
0,
)),
// A positive scale, so we have decimals on the right.
(Some(num), _) => Ok(Decimal::from_i128_with_scale(num, scale as u32)),
(None, _) => Err("Decimal's integer part out of range.".into()),
match sign {
PgNumericSign::Positive => value.set_sign_positive(true),
PgNumericSign::Negative => value.set_sign_negative(true),
}
Ok(value)
}
}
@@ -403,4 +395,7 @@ mod decimal_to_pgnumeric {
}
);
}
#[test]
fn issue_666_trailing_zeroes_at_max_precision() {}
}