Skip to content

Commit a0007b4

Browse files
Julius de Bruijnabonander
Julius de Bruijn
authored andcommitted
Fixing BigDecimal conversion for PostgreSQL
Now working properly with numbers, such as `0.01` and `0.012`.
1 parent 25e7292 commit a0007b4

File tree

2 files changed

+45
-39
lines changed

2 files changed

+45
-39
lines changed

sqlx-core/src/postgres/types/bigdecimal.rs

+38-39
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::cmp;
22
use std::convert::{TryFrom, TryInto};
33

4-
use bigdecimal::BigDecimal;
4+
use bigdecimal::{BigDecimal, ToPrimitive, Zero};
55
use num_bigint::{BigInt, Sign};
66

77
use crate::decode::Decode;
@@ -77,65 +77,64 @@ impl TryFrom<&'_ BigDecimal> for PgNumeric {
7777
type Error = BoxDynError;
7878

7979
fn try_from(decimal: &BigDecimal) -> Result<Self, BoxDynError> {
80-
let base_10_to_10000 = |chunk: &[u8]| chunk.iter().fold(0i16, |a, &d| a * 10 + d as i16);
80+
if decimal.is_zero() {
81+
return Ok(PgNumeric::Number {
82+
sign: PgNumericSign::Positive,
83+
scale: 0,
84+
weight: 0,
85+
digits: vec![],
86+
});
87+
}
8188

8289
// NOTE: this unfortunately copies the BigInt internally
8390
let (integer, exp) = decimal.as_bigint_and_exponent();
8491

85-
// this routine is specifically optimized for base-10
86-
// FIXME: is there a way to iterate over the digits to avoid the Vec allocation
87-
let (sign, base_10) = integer.to_radix_be(10);
88-
89-
// weight is positive power of 10000
90-
// exp is the negative power of 10
91-
let weight_10 = base_10.len() as i64 - exp;
92-
9392
// scale is only nonzero when we have fractional digits
9493
// since `exp` is the _negative_ decimal exponent, it tells us
9594
// exactly what our scale should be
9695
let scale: i16 = cmp::max(0, exp).try_into()?;
9796

98-
// there's an implicit +1 offset in the interpretation
99-
let weight: i16 = if weight_10 <= 0 {
100-
weight_10 / 4 - 1
101-
} else {
102-
// the `-1` is a fix for an off by 1 error (4 digits should still be 0 weight)
103-
(weight_10 - 1) / 4
104-
}
105-
.try_into()?;
97+
let (sign, uint) = integer.into_parts();
98+
let mut mantissa = uint.to_u128().unwrap();
10699

107-
let digits_len = if base_10.len() % 4 != 0 {
108-
base_10.len() / 4 + 1
109-
} else {
110-
base_10.len() / 4
111-
};
100+
// If our scale is not a multiple of 4, we need to go to the next
101+
// multiple.
102+
let groups_diff = scale % 4;
103+
if groups_diff > 0 {
104+
let remainder = 4 - groups_diff as u32;
105+
let power = 10u32.pow(remainder as u32) as u128;
112106

113-
let offset = weight_10.rem_euclid(4) as usize;
107+
mantissa = mantissa * power;
108+
}
114109

115-
let mut digits = Vec::with_capacity(digits_len);
110+
// Array to store max mantissa of Decimal in Postgres decimal format.
111+
let mut digits = Vec::with_capacity(8);
116112

117-
if let Some(first) = base_10.get(..offset) {
118-
if offset != 0 {
119-
digits.push(base_10_to_10000(first));
120-
}
113+
// Convert to base-10000.
114+
while mantissa != 0 {
115+
digits.push((mantissa % 10_000) as i16);
116+
mantissa /= 10_000;
121117
}
122118

123-
if let Some(rest) = base_10.get(offset..) {
124-
digits.extend(
125-
rest.chunks(4)
126-
.map(|chunk| base_10_to_10000(chunk) * 10i16.pow(4 - chunk.len() as u32)),
127-
);
128-
}
119+
// Change the endianness.
120+
digits.reverse();
121+
122+
// Weight is number of digits on the left side of the decimal.
123+
let digits_after_decimal = (scale + 3) as u16 / 4;
124+
let weight = digits.len() as i16 - digits_after_decimal as i16 - 1;
129125

126+
// Remove non-significant zeroes.
130127
while let Some(&0) = digits.last() {
131128
digits.pop();
132129
}
133130

131+
let sign = match sign {
132+
Sign::Plus | Sign::NoSign => PgNumericSign::Positive,
133+
Sign::Minus => PgNumericSign::Negative,
134+
};
135+
134136
Ok(PgNumeric::Number {
135-
sign: match sign {
136-
Sign::Plus | Sign::NoSign => PgNumericSign::Positive,
137-
Sign::Minus => PgNumericSign::Negative,
138-
},
137+
sign,
139138
scale,
140139
weight,
141140
digits,

tests/postgres/types.rs

+7
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,14 @@ test_type!(bigdecimal<sqlx::types::BigDecimal>(Postgres,
396396
"10000::numeric" == "10000".parse::<sqlx::types::BigDecimal>().unwrap(),
397397
"0.1::numeric" == "0.1".parse::<sqlx::types::BigDecimal>().unwrap(),
398398
"0.01::numeric" == "0.01".parse::<sqlx::types::BigDecimal>().unwrap(),
399+
"0.012::numeric" == "0.012".parse::<sqlx::types::BigDecimal>().unwrap(),
400+
"0.0123::numeric" == "0.0123".parse::<sqlx::types::BigDecimal>().unwrap(),
399401
"0.01234::numeric" == "0.01234".parse::<sqlx::types::BigDecimal>().unwrap(),
402+
"0.012345::numeric" == "0.012345".parse::<sqlx::types::BigDecimal>().unwrap(),
403+
"0.0123456::numeric" == "0.0123456".parse::<sqlx::types::BigDecimal>().unwrap(),
404+
"0.01234567::numeric" == "0.01234567".parse::<sqlx::types::BigDecimal>().unwrap(),
405+
"0.012345678::numeric" == "0.012345678".parse::<sqlx::types::BigDecimal>().unwrap(),
406+
"0.0123456789::numeric" == "0.0123456789".parse::<sqlx::types::BigDecimal>().unwrap(),
400407
"12.34::numeric" == "12.34".parse::<sqlx::types::BigDecimal>().unwrap(),
401408
"12345.6789::numeric" == "12345.6789".parse::<sqlx::types::BigDecimal>().unwrap(),
402409
));

0 commit comments

Comments
 (0)