Skip to main content

diesel/pg/types/
numeric.rs

1#[cfg(feature = "numeric")]
2mod bigdecimal {
3    extern crate bigdecimal;
4    extern crate num_bigint;
5    extern crate num_integer;
6    extern crate num_traits;
7
8    use self::bigdecimal::BigDecimal;
9    use self::num_bigint::{BigInt, BigUint, Sign};
10    use self::num_integer::Integer;
11    use self::num_traits::{Signed, ToPrimitive, Zero};
12
13    use crate::deserialize::{self, Defaultable, FromSql};
14    use crate::pg::data_types::PgNumeric;
15    use crate::pg::{Pg, PgValue};
16    use crate::serialize::{self, Output, ToSql};
17    use crate::sql_types::Numeric;
18
19    use std::error::Error;
20
21    /// Iterator over the digits of a big uint in base 10k.
22    /// The digits will be returned in little endian order.
23    struct ToBase10000(Option<BigUint>);
24
25    impl Iterator for ToBase10000 {
26        type Item = i16;
27
28        fn next(&mut self) -> Option<Self::Item> {
29            self.0.take().map(|v| {
30                let (div, rem) = v.div_rem(&BigUint::from(10_000u16));
31                if !div.is_zero() {
32                    self.0 = Some(div);
33                }
34                rem.to_i16().expect("10000 always fits in an i16")
35            })
36        }
37    }
38
39    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
40    impl<'a> TryFrom<&'a PgNumeric> for BigDecimal {
41        type Error = Box<dyn Error + Send + Sync>;
42
43        fn try_from(numeric: &'a PgNumeric) -> deserialize::Result<Self> {
44            let (sign, weight, scale, digits) = match *numeric {
45                PgNumeric::Positive {
46                    weight,
47                    scale,
48                    ref digits,
49                } => (Sign::Plus, weight, scale, digits),
50                PgNumeric::Negative {
51                    weight,
52                    scale,
53                    ref digits,
54                } => (Sign::Minus, weight, scale, digits),
55                PgNumeric::NaN => {
56                    return Err(Box::from("NaN is not (yet) supported in BigDecimal"))
57                }
58            };
59
60            let mut result = BigUint::default();
61            let count = i64::try_from(digits.len())?;
62            for digit in digits {
63                result *= BigUint::from(10_000u64);
64                result += BigUint::from(u64::try_from(*digit)?);
65            }
66            // First digit got factor 10_000^(digits.len() - 1), but should get 10_000^weight
67            let correction_exp = 4 * (i64::from(weight) - count + 1);
68            let result = BigDecimal::new(BigInt::from_biguint(sign, result), -correction_exp)
69                .with_scale(i64::from(scale));
70            Ok(result)
71        }
72    }
73
74    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
75    impl TryFrom<PgNumeric> for BigDecimal {
76        type Error = Box<dyn Error + Send + Sync>;
77
78        fn try_from(numeric: PgNumeric) -> deserialize::Result<Self> {
79            (&numeric).try_into()
80        }
81    }
82
83    // that should likely be a `TryFrom` impl
84    // TODO: diesel 3.0
85    // This now mostly exists for backward compatibility
86    // Our own `ToSql` impls don't call it anymore in favour of calling
87    // the failable inner function instead
88    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
89    impl<'a> From<&'a BigDecimal> for PgNumeric {
90        fn from(decimal: &'a BigDecimal) -> Self {
91            try_convert_decimal_to_pg_numeric(decimal)
92                .expect("Failed to convert BigDecimal to PgNumeric")
93        }
94    }
95
96    // NOTE(clippy): No `std::ops::MulAssign` impl for `BigInt`
97    // NOTE(clippy): Clippy suggests to replace the `.take_while(|i| i.is_zero())`
98    // with `.take_while(Zero::is_zero)`, but that's a false positive.
99    // The closure gets an `&&i16` due to autoderef `<i16 as Zero>::is_zero(&self) -> bool`
100    // is called. There is no impl for `&i16` that would work with this closure.
101    #[allow(clippy::assign_op_pattern, clippy::redundant_closure)]
102    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
103    fn try_convert_decimal_to_pg_numeric(
104        decimal: &BigDecimal,
105    ) -> Result<PgNumeric, Box<dyn core::error::Error + Send + Sync>> {
106        let (mut integer, scale) = decimal.as_bigint_and_exponent();
107
108        // Handling of negative scale
109        let scale = if scale < -131064 {
110            // that's a guard avoiding the potential expensive calculation below
111            // by returning the error early on that beginning with this scale would
112            // be emitted anyway
113            return Err("Max digit number is expect to fit into 16 bit".into());
114        } else if scale < 0 {
115            integer = BigDecimal::new(integer, scale)
116                .with_scale(0)
117                .as_bigint_and_exponent()
118                .0;
119            0
120        } else {
121            scale
122                .try_into()
123                .map_err(|_| "Scale is expected to be 16bit large")?
124        };
125
126        integer = integer.abs();
127
128        // Ensure that the decimal will always lie on a digit boundary
129        for _ in 0..(4 - scale % 4) {
130            integer = integer * 10;
131        }
132        let integer = integer.to_biguint().ok_or("integer is always positive")?;
133
134        let mut digits = ToBase10000(Some(integer)).collect::<Vec<_>>();
135        digits.reverse();
136        let digits_after_decimal = scale / 4 + 1;
137        let weight = i16::try_from(digits.len())
138            .map_err(|_| "Max digit number is expected to fit into 16 bit")?
139            - i16::try_from(digits_after_decimal)
140                .map_err(|_| "Max digit number is expected to fit into 16 bit")?
141            - 1;
142
143        let unnecessary_zeroes = digits.iter().rev().take_while(|i| i.is_zero()).count();
144
145        let relevant_digits = digits.len() - unnecessary_zeroes;
146        digits.truncate(relevant_digits);
147
148        let result = match decimal.sign() {
149            Sign::Plus => PgNumeric::Positive {
150                digits,
151                scale,
152                weight,
153            },
154            Sign::Minus => PgNumeric::Negative {
155                digits,
156                scale,
157                weight,
158            },
159            Sign::NoSign => PgNumeric::Positive {
160                digits: ::alloc::boxed::box_assume_init_into_vec_unsafe(::alloc::intrinsics::write_box_via_move(::alloc::boxed::Box::new_uninit(),
        [0]))vec![0],
161                scale: 0,
162                weight: 0,
163            },
164        };
165
166        Ok(result)
167    }
168
169    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
170    impl From<BigDecimal> for PgNumeric {
171        fn from(bigdecimal: BigDecimal) -> Self {
172            (&bigdecimal).into()
173        }
174    }
175
176    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
177    impl ToSql<Numeric, Pg> for BigDecimal {
178        fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
179            let numeric = try_convert_decimal_to_pg_numeric(self)?;
180            ToSql::<Numeric, Pg>::to_sql(&numeric, &mut out.reborrow())
181        }
182    }
183
184    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
185    impl FromSql<Numeric, Pg> for BigDecimal {
186        fn from_sql(numeric: PgValue<'_>) -> deserialize::Result<Self> {
187            PgNumeric::from_sql(numeric)?.try_into()
188        }
189    }
190
191    #[cfg(all(feature = "postgres_backend", feature = "numeric"))]
192    impl Defaultable for BigDecimal {
193        fn default_value() -> Self {
194            Self::default()
195        }
196    }
197
198    #[cfg(test)]
199    mod tests {
200        use crate::query_builder::ByteWrapper;
201
202        use super::*;
203        use std::str::FromStr;
204
205        #[diesel_test_helper::test]
206        fn bigdecimal_to_pgnumeric_converts_digits_to_base_10000() {
207            let decimal = BigDecimal::from_str("1").unwrap();
208            let expected = PgNumeric::Positive {
209                weight: 0,
210                scale: 0,
211                digits: vec![1],
212            };
213            assert_eq!(expected, decimal.into());
214
215            let decimal = BigDecimal::from_str("10").unwrap();
216            let expected = PgNumeric::Positive {
217                weight: 0,
218                scale: 0,
219                digits: vec![10],
220            };
221            assert_eq!(expected, decimal.into());
222
223            let decimal = BigDecimal::from_str("10000").unwrap();
224            let expected = PgNumeric::Positive {
225                weight: 1,
226                scale: 0,
227                digits: vec![1],
228            };
229            assert_eq!(expected, decimal.into());
230
231            let decimal = BigDecimal::from_str("10001").unwrap();
232            let expected = PgNumeric::Positive {
233                weight: 1,
234                scale: 0,
235                digits: vec![1, 1],
236            };
237            assert_eq!(expected, decimal.into());
238
239            let decimal = BigDecimal::from_str("100000000").unwrap();
240            let expected = PgNumeric::Positive {
241                weight: 2,
242                scale: 0,
243                digits: vec![1],
244            };
245            assert_eq!(expected, decimal.into());
246        }
247
248        #[diesel_test_helper::test]
249        fn bigdecimal_to_pg_numeric_properly_adjusts_scale() {
250            let decimal = BigDecimal::from_str("1").unwrap();
251            let expected = PgNumeric::Positive {
252                weight: 0,
253                scale: 0,
254                digits: vec![1],
255            };
256            assert_eq!(expected, decimal.into());
257
258            let decimal = BigDecimal::from_str("1.0").unwrap();
259            let expected = PgNumeric::Positive {
260                weight: 0,
261                scale: 1,
262                digits: vec![1],
263            };
264            assert_eq!(expected, decimal.into());
265
266            let decimal = BigDecimal::from_str("1.1").unwrap();
267            let expected = PgNumeric::Positive {
268                weight: 0,
269                scale: 1,
270                digits: vec![1, 1000],
271            };
272            assert_eq!(expected, decimal.into());
273
274            let decimal = BigDecimal::from_str("1.10").unwrap();
275            let expected = PgNumeric::Positive {
276                weight: 0,
277                scale: 2,
278                digits: vec![1, 1000],
279            };
280            assert_eq!(expected, decimal.into());
281
282            let decimal = BigDecimal::from_str("100000000.0001").unwrap();
283            let expected = PgNumeric::Positive {
284                weight: 2,
285                scale: 4,
286                digits: vec![1, 0, 0, 1],
287            };
288            assert_eq!(expected, decimal.into());
289
290            let decimal = BigDecimal::from_str("0.1").unwrap();
291            let expected = PgNumeric::Positive {
292                weight: -1,
293                scale: 1,
294                digits: vec![1000],
295            };
296            assert_eq!(expected, decimal.into());
297        }
298
299        #[diesel_test_helper::test]
300        fn bigdecimal_to_pg_numeric_retains_sign() {
301            let decimal = BigDecimal::from_str("123.456").unwrap();
302            let expected = PgNumeric::Positive {
303                weight: 0,
304                scale: 3,
305                digits: vec![123, 4560],
306            };
307            assert_eq!(expected, decimal.into());
308
309            let decimal = BigDecimal::from_str("-123.456").unwrap();
310            let expected = PgNumeric::Negative {
311                weight: 0,
312                scale: 3,
313                digits: vec![123, 4560],
314            };
315            assert_eq!(expected, decimal.into());
316        }
317
318        #[diesel_test_helper::test]
319        fn bigdecimal_with_negative_scale_to_pg_numeric_works() {
320            let decimal = BigDecimal::new(50.into(), -2);
321            let expected = PgNumeric::Positive {
322                weight: 0,
323                scale: 0,
324                digits: vec![5000],
325            };
326            assert_eq!(expected, decimal.into());
327
328            let decimal = BigDecimal::new(1.into(), -4);
329            let expected = PgNumeric::Positive {
330                weight: 1,
331                scale: 0,
332                digits: vec![1],
333            };
334            assert_eq!(expected, decimal.into());
335        }
336
337        #[diesel_test_helper::test]
338        fn bigdecimal_with_negative_weight_to_pg_numeric_works() {
339            let decimal = BigDecimal::from_str("0.1000000000000000").unwrap();
340            let expected = PgNumeric::Positive {
341                weight: -1,
342                scale: 16,
343                digits: vec![1000],
344            };
345            assert_eq!(expected, decimal.into());
346
347            let decimal = BigDecimal::from_str("0.00315937").unwrap();
348            let expected = PgNumeric::Positive {
349                weight: -1,
350                scale: 8,
351                digits: vec![31, 5937],
352            };
353            assert_eq!(expected, decimal.into());
354
355            let decimal = BigDecimal::from_str("0.003159370000000000").unwrap();
356            let expected = PgNumeric::Positive {
357                weight: -1,
358                scale: 18,
359                digits: vec![31, 5937],
360            };
361            assert_eq!(expected, decimal.into());
362        }
363
364        #[diesel_test_helper::test]
365        fn pg_numeric_to_bigdecimal_works() {
366            let expected = BigDecimal::from_str("123.456").unwrap();
367            let pg_numeric = PgNumeric::Positive {
368                weight: 0,
369                scale: 3,
370                digits: vec![123, 4560],
371            };
372            let res: BigDecimal = pg_numeric.try_into().unwrap();
373            assert_eq!(res, expected);
374
375            let expected = BigDecimal::from_str("-56.78").unwrap();
376            let pg_numeric = PgNumeric::Negative {
377                weight: 0,
378                scale: 2,
379                digits: vec![56, 7800],
380            };
381            let res: BigDecimal = pg_numeric.try_into().unwrap();
382            assert_eq!(res, expected);
383        }
384
385        #[diesel_test_helper::test]
386        fn bigdecimal_with_huge_scale_errors() {
387            let huge_scale = "0.".to_string() + &"0".repeat(70_000) + "1";
388            let decimal = BigDecimal::from_str(&huge_scale).unwrap();
389            let mut v = Vec::new();
390            let output = ByteWrapper(&mut v);
391
392            let mut output = Output::test(output);
393            let r = <BigDecimal as ToSql<Numeric, Pg>>::to_sql(&decimal, &mut output);
394            assert!(r.is_err());
395        }
396
397        #[diesel_test_helper::test]
398        fn bigdecimal_negative_scale_doesnt_burn_cpu_time() {
399            let d = BigDecimal::from_str("1E+130000").expect("parse bigdecimal");
400            let n: PgNumeric = d.into();
401            assert_eq!(
402                n,
403                PgNumeric::Positive {
404                    weight: 32500,
405                    scale: 0,
406                    digits: vec![1]
407                }
408            );
409        }
410
411        #[diesel_test_helper::test]
412        #[should_panic]
413        fn bigdecimal_negative_scale_doesnt_burn_cpu_time_fast_error() {
414            let d = BigDecimal::from_str("1E+131064").expect("parse bigdecimal");
415            let _n: PgNumeric = d.into();
416        }
417    }
418}