Skip to content

Commit 5741cea

Browse files
committed
Refactor encoding BigDecimal on PG
I had originally worked on this as part of 0.15.2, but have been sitting on it until a new version was released so we could use `to_bigint_and_exponent`. The new code takes a different approach to the encoding. I've specifically broken the code into a `From` impl to make it easier for us to test the output. There was an impedence mismatch in the old code. Ultimately the way `BigDecimal` is represented is as a `BigInteger` with a scale. The way we're transmitting it is as a `BigInteger` with a weight and scale. Splitting it into separate integral and fractional parts, and applying different logic was making the code difficult to understand, and introducing unneccessary complications. There are two differences to how `BigUint` is represented vs how PG wants it. The first is that PG is base 10000, while `BigUint` is base 2^16. The second is that in `BigUint`, `scale` represents both the number of significant digits, *and* the position of the decimal point. In PG, `scale` only represents the number of significant digits, and there is a separate `weight` to represent the position of the decimal point. This means that `BigUint` will store trailing zeroes that we need to strip, but PG will need to ensure that the decimal falls on a digit boundary in base 10k. The majority of the logic here is to handle that portion. I've tried to make it as clear as possible what's going on through proper naming. Unfortunately the fairly simple code of iterating over the digits in base 10k gets drowned out by the cleanup. The code could potentially be simplified if we could easily iterate over the digits in big-endian order, but I could find an easy way to do that mathematically.
1 parent 37abbb2 commit 5741cea

5 files changed

Lines changed: 144 additions & 77 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
4949
`grandchild.join(child.join(parent))`. Previously only
5050
`parent.join(child.join(grandchild))` would compile.
5151

52+
* When encoding a `BigDecimal` on PG, `1.0` is no longer encoded as if it were
53+
`1`.
54+
5255
[bigdecimal-0.16.0]: https://crates.io/crates/bigdecimal
5356
[range-0.16.0]: http://docs.diesel.rs/diesel/pg/types/sql_types/struct.Range.html
5457

diesel/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ ipnetwork = { version = "0.12.2", optional = true }
2828
num-bigint = { version = "0.1.37", optional = true }
2929
num-traits = { version = "0.1.35", optional = true }
3030
num-integer = { version = "0.1.32", optional = true }
31-
bigdecimal = { version = "0.0.7", optional = true }
31+
bigdecimal = { version = "0.0.10", optional = true }
3232
bitflags = { version = "0.9", optional = true }
3333

3434
[dev-dependencies]

diesel/src/pg/types/numeric.rs

Lines changed: 137 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,91 +5,62 @@ mod bigdecimal {
55
extern crate num_integer;
66
extern crate bigdecimal;
77

8+
use self::bigdecimal::BigDecimal;
9+
use self::num_bigint::{Sign, BigInt, BigUint};
10+
use self::num_integer::Integer;
11+
use self::num_traits::{Signed, Zero, ToPrimitive};
812
use std::error::Error;
913
use std::io::prelude::*;
1014

1115
use pg::Pg;
12-
13-
use self::num_traits::{Signed, Zero, ToPrimitive};
14-
use self::num_bigint::{Sign, BigInt, BigUint, ToBigInt};
15-
use self::num_integer::Integer;
16-
use self::bigdecimal::BigDecimal;
17-
1816
use pg::data_types::PgNumeric;
1917
use types::{self, FromSql, ToSql, ToSqlOutput, IsNull};
2018

21-
type Digits = Vec<i16>;
22-
23-
fn bigdec_add_integer_part(digits: &mut Digits, absolute: &BigDecimal) -> i16 {
24-
let mut weight = 0;
25-
let ten_k = BigInt::from(10000);
26-
27-
let mut integer_part = absolute.to_bigint().expect("Can always take integer part of BigDecimal");
28-
29-
while ten_k <= integer_part {
30-
weight += 1;
31-
// digit is integer_part REM 10_000
32-
let (div, digit) = integer_part.div_rem(&ten_k);
33-
digits.push(digit.to_u16().expect("digit < 10000, but cannot fit in i16") as i16);
34-
integer_part = div;
35-
}
36-
digits.push(integer_part.to_string().parse::<i16>().expect("digit < 10000, but cannot fit in i16"));
37-
38-
digits.reverse();
39-
40-
weight
41-
}
42-
43-
fn bigdec_add_decimal_part(digits: &mut Digits, absolute: &BigDecimal) -> u16 {
44-
use std::str::FromStr;
45-
46-
let ten_k = BigDecimal::from_str("10000").expect("Could not parse into BigDecimal");
47-
48-
let decimal_part = absolute;
49-
let mut decimal_part = decimal_part - absolute.with_scale(0);
50-
// scale is the amount of digits to print. to_string() includes a "0.",
51-
// that's why the -2 is there.
52-
let scale = if decimal_part == Zero::zero() {
53-
0
54-
} else {
55-
decimal_part.to_string().len() as u16 - 2
56-
};
57-
58-
while decimal_part != BigDecimal::zero() {
59-
decimal_part *= &ten_k;
60-
let digit = decimal_part.to_bigint().expect("Can always take integer part of BigDecimal");
61-
62-
// This can be simplified when github.com/akubera/bigdecimal-rs/issues/13 gets
63-
// solved; decimal_part -= &digit; should suffice by then.
64-
decimal_part -= BigDecimal::new(digit.clone(), 0);
65-
digits.push(digit.to_u16().expect("digit < 10000, but cannot fit in i16") as i16);
19+
/// Iterator over the digits of a big uint in base 10k.
20+
/// The digits will be returned in little endian order.
21+
struct ToBase10000(Option<BigUint>);
22+
23+
impl Iterator for ToBase10000 {
24+
type Item = i16;
25+
26+
fn next(&mut self) -> Option<Self::Item> {
27+
self.0.take().map(|v| {
28+
let (div, rem) = v.div_rem(&BigUint::from(10000u16));
29+
if !div.is_zero() {
30+
self.0 = Some(div);
31+
}
32+
rem.to_i16().expect("10000 always fits in an i16")
33+
})
6634
}
67-
68-
scale
6935
}
7036

71-
impl ToSql<types::Numeric, Pg> for BigDecimal {
72-
fn to_sql<W: Write>(&self, out: &mut ToSqlOutput<W, Pg>) -> Result<IsNull, Box<Error + Send + Sync>> {
73-
// The encoding of the BigDecimal type for PostgreSQL is a bit complicated:
74-
// PostgreSQL expects the data in base-10000 (so two bytes per 10k),
75-
// and the decimal point should lie on a boundary (as per definition of "base-10000").
76-
77-
// BigDecimal, internally, holds an int vector (base-256, one byte per byte),
78-
// and a base (u64, base-10) shift.
79-
80-
// Therefore, we split up the encoding in three parts:
81-
// the sign, the (integer) part before the decimal, and the part after the decimal.
82-
83-
let absolute = self.abs();
84-
let mut digits = vec![];
85-
86-
// Encode the integer part
87-
let weight = bigdec_add_integer_part(&mut digits, &absolute);
37+
impl<'a> From<&'a BigDecimal> for PgNumeric {
38+
fn from(decimal: &'a BigDecimal) -> Self {
39+
let (mut integer, scale) = decimal.as_bigint_and_exponent();
40+
let scale = scale as u16;
41+
integer = integer.abs();
8842

89-
// Encode the decimal part
90-
let scale = bigdec_add_decimal_part(&mut digits, &absolute);
91-
92-
let numeric = match self.sign() {
43+
// Ensure that the decimal will always lie on a digit boundary
44+
for _ in 0..(4 - scale % 4) {
45+
integer = integer * 10;
46+
}
47+
let integer = integer.to_biguint().expect("integer is always positive");
48+
49+
let mut digits = ToBase10000(Some(integer)).collect::<Vec<_>>();
50+
digits.reverse();
51+
let digits_after_decimal = scale as u16 / 4 + 1;
52+
let weight = digits.len() as i16 - digits_after_decimal as i16 - 1;
53+
let index_of_decimal = (weight + 1) as usize;
54+
55+
let unneccessary_zeroes = digits[index_of_decimal..]
56+
.iter()
57+
.rev()
58+
.take_while(|i| i.is_zero())
59+
.count();
60+
let relevant_digits = digits.len() - unneccessary_zeroes;
61+
digits.truncate(relevent_digits);
62+
63+
match decimal.sign() {
9364
Sign::Plus => PgNumeric::Positive {
9465
digits, scale, weight
9566
},
@@ -101,7 +72,19 @@ mod bigdecimal {
10172
scale: 0,
10273
weight: 0,
10374
},
104-
};
75+
}
76+
}
77+
}
78+
79+
impl From<BigDecimal> for PgNumeric {
80+
fn from(bigdecimal: BigDecimal) -> Self {
81+
(&bigdecimal).into()
82+
}
83+
}
84+
85+
impl ToSql<types::Numeric, Pg> for BigDecimal {
86+
fn to_sql<W: Write>(&self, out: &mut ToSqlOutput<W, Pg>) -> Result<IsNull, Box<Error + Send + Sync>> {
87+
let numeric = PgNumeric::from(self);
10588
ToSql::<types::Numeric, Pg>::to_sql(&numeric, out)
10689
}
10790
}
@@ -127,4 +110,83 @@ mod bigdecimal {
127110
Ok(result)
128111
}
129112
}
113+
114+
#[cfg(test)]
115+
mod tests {
116+
use super::*;
117+
use std::str::FromStr;
118+
119+
#[test]
120+
fn bigdecimal_to_pgnumeric_converts_digits_to_base_10000() {
121+
let decimal = BigDecimal::from_str("1").unwrap();
122+
let expected = PgNumeric::Positive { weight: 0, scale: 0, digits: vec![1] };
123+
assert_eq!(expected, decimal.into());
124+
125+
let decimal = BigDecimal::from_str("10").unwrap();
126+
let expected = PgNumeric::Positive { weight: 0, scale: 0, digits: vec![10] };
127+
assert_eq!(expected, decimal.into());
128+
129+
let decimal = BigDecimal::from_str("10000").unwrap();
130+
let expected = PgNumeric::Positive { weight: 1, scale: 0, digits: vec![1, 0] };
131+
assert_eq!(expected, decimal.into());
132+
133+
let decimal = BigDecimal::from_str("10001").unwrap();
134+
let expected = PgNumeric::Positive { weight: 1, scale: 0, digits: vec![1, 1] };
135+
assert_eq!(expected, decimal.into());
136+
137+
let decimal = BigDecimal::from_str("100000000").unwrap();
138+
let expected = PgNumeric::Positive { weight: 2, scale: 0, digits: vec![1, 0, 0] };
139+
assert_eq!(expected, decimal.into());
140+
}
141+
142+
#[test]
143+
fn bigdecimal_to_pg_numeric_properly_adjusts_scale() {
144+
let decimal = BigDecimal::from_str("1").unwrap();
145+
let expected = PgNumeric::Positive { weight: 0, scale: 0, digits: vec![1] };
146+
assert_eq!(expected, decimal.into());
147+
148+
let decimal = BigDecimal::from_str("1.0").unwrap();
149+
let expected = PgNumeric::Positive { weight: 0, scale: 1, digits: vec![1] };
150+
assert_eq!(expected, decimal.into());
151+
152+
let decimal = BigDecimal::from_str("1.1").unwrap();
153+
let expected = PgNumeric::Positive { weight: 0, scale: 1, digits: vec![1, 1000] };
154+
assert_eq!(expected, decimal.into());
155+
156+
let decimal = BigDecimal::from_str("1.10").unwrap();
157+
let expected = PgNumeric::Positive { weight: 0, scale: 2, digits: vec![1, 1000] };
158+
assert_eq!(expected, decimal.into());
159+
160+
let decimal = BigDecimal::from_str("100000000.0001").unwrap();
161+
let expected = PgNumeric::Positive {
162+
weight: 2,
163+
scale: 4,
164+
digits: vec![1, 0, 0, 1],
165+
};
166+
assert_eq!(expected, decimal.into());
167+
168+
let decimal = BigDecimal::from_str("0.1").unwrap();
169+
let expected = PgNumeric::Positive { weight: -1, scale: 1, digits: vec![1000] };
170+
assert_eq!(expected, decimal.into());
171+
}
172+
173+
#[test]
174+
fn bigdecimal_to_pg_numeric_retains_sign() {
175+
let decimal = BigDecimal::from_str("123.456").unwrap();
176+
let expected = PgNumeric::Positive {
177+
weight: 0,
178+
scale: 3,
179+
digits: vec![123, 4560],
180+
};
181+
assert_eq!(expected, decimal.into());
182+
183+
let decimal = BigDecimal::from_str("-123.456").unwrap();
184+
let expected = PgNumeric::Negative {
185+
weight: 0,
186+
scale: 3,
187+
digits: vec![123, 4560],
188+
};
189+
assert_eq!(expected, decimal.into());
190+
}
191+
}
130192
}

diesel_tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ quickcheck = { version = "0.3.1", features = ["unstable"] }
1919
uuid = { version = ">=0.2.0, <0.6.0" }
2020
serde_json = { version=">=0.9, <2.0" }
2121
ipnetwork = "0.12.2"
22-
bigdecimal = "0.0.7"
22+
bigdecimal = "0.0.10"
2323

2424
[features]
2525
default = []

diesel_tests/tests/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ fn pg_numeric_bigdecimal_to_sql() {
443443
quickcheck(correct_rep as fn(u64, u64) -> bool);
444444

445445
let test_values = vec![
446+
"0.1",
446447
"1.0",
447448
"141.0",
448449
"-1.0",
@@ -498,6 +499,7 @@ fn pg_numeric_bigdecimal_from_sql() {
498499
use self::bigdecimal::BigDecimal;
499500

500501
let values = vec![
502+
"0.1",
501503
"1.0",
502504
"141.0",
503505
"-1.0",

0 commit comments

Comments
 (0)