sqlx_postgres/types/
money.rs

1use crate::{
2    decode::Decode,
3    encode::{Encode, IsNull},
4    error::BoxDynError,
5    types::Type,
6    {PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres},
7};
8use byteorder::{BigEndian, ByteOrder};
9use std::{
10    io,
11    ops::{Add, AddAssign, Sub, SubAssign},
12};
13
14/// The PostgreSQL [`MONEY`] type stores a currency amount with a fixed fractional
15/// precision. The fractional precision is determined by the database's
16/// `lc_monetary` setting.
17///
18/// Data is read and written as 64-bit signed integers, and conversion into a
19/// decimal should be done using the right precision.
20///
21/// Reading `MONEY` value in text format is not supported and will cause an error.
22///
23/// ### `locale_frac_digits`
24/// This parameter corresponds to the number of digits after the decimal separator.
25///
26/// This value must match what Postgres is expecting for the locale set in the database
27/// or else the decimal value you see on the client side will not match the `money` value
28/// on the server side.
29///
30/// **For _most_ locales, this value is `2`.**
31///
32/// If you're not sure what locale your database is set to or how many decimal digits it specifies,
33/// you can execute `SHOW lc_monetary;` to get the locale name, and then look it up in this list
34/// (you can ignore the `.utf8` prefix):
35/// <https://lh.2xlibre.net/values/frac_digits/>
36///
37/// If that link is dead and you're on a POSIX-compliant system (Unix, FreeBSD) you can also execute:
38///
39/// ```sh
40/// $ LC_MONETARY=<value returned by `SHOW lc_monetary`> locale -k frac_digits
41/// ```
42///
43/// And the value you want is `N` in `frac_digits=N`. If you have shell access to the database
44/// server you should execute it there as available locales may differ between machines.
45///
46/// Note that if `frac_digits` for the locale is outside the range `[0, 10]`, Postgres assumes
47/// it's a sentinel value and defaults to 2:
48/// <https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/cash.c#L114-L123>
49///
50/// [`MONEY`]: https://www.postgresql.org/docs/current/datatype-money.html
51#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
52pub struct PgMoney(
53    /// The raw integer value sent over the wire; for locales with `frac_digits=2` (i.e. most
54    /// of them), this will be the value in whole cents.
55    ///
56    /// E.g. for `select '$123.45'::money` with a locale of `en_US` (`frac_digits=2`),
57    /// this will be `12345`.
58    ///
59    /// If the currency of your locale does not have fractional units, e.g. Yen, then this will
60    /// just be the units of the currency.
61    ///
62    /// See the type-level docs for an explanation of `locale_frac_units`.
63    pub i64,
64);
65
66impl PgMoney {
67    /// Convert the money value into a [`BigDecimal`] using `locale_frac_digits`.
68    ///
69    /// See the type-level docs for an explanation of `locale_frac_digits`.
70    ///
71    /// [`BigDecimal`]: bigdecimal::BigDecimal
72    #[cfg(feature = "bigdecimal")]
73    pub fn to_bigdecimal(self, locale_frac_digits: i64) -> bigdecimal::BigDecimal {
74        let digits = num_bigint::BigInt::from(self.0);
75
76        bigdecimal::BigDecimal::new(digits, locale_frac_digits)
77    }
78
79    /// Convert the money value into a [`Decimal`] using `locale_frac_digits`.
80    ///
81    /// See the type-level docs for an explanation of `locale_frac_digits`.
82    ///
83    /// [`Decimal`]: rust_decimal::Decimal
84    #[cfg(feature = "rust_decimal")]
85    pub fn to_decimal(self, locale_frac_digits: u32) -> rust_decimal::Decimal {
86        rust_decimal::Decimal::new(self.0, locale_frac_digits)
87    }
88
89    /// Convert a [`Decimal`] value into money using `locale_frac_digits`.
90    ///
91    /// See the type-level docs for an explanation of `locale_frac_digits`.
92    ///
93    /// Note that `Decimal` has 96 bits of precision, but `PgMoney` only has 63 plus the sign bit.
94    /// If the value is larger than 63 bits it will be truncated.
95    ///
96    /// [`Decimal`]: rust_decimal::Decimal
97    #[cfg(feature = "rust_decimal")]
98    pub fn from_decimal(mut decimal: rust_decimal::Decimal, locale_frac_digits: u32) -> Self {
99        // this is all we need to convert to our expected locale's `frac_digits`
100        decimal.rescale(locale_frac_digits);
101
102        /// a mask to bitwise-AND with an `i64` to zero the sign bit
103        const SIGN_MASK: i64 = i64::MAX;
104
105        let is_negative = decimal.is_sign_negative();
106        let serialized = decimal.serialize();
107
108        // interpret bytes `4..12` as an i64, ignoring the sign bit
109        // this is where truncation occurs
110        let value = i64::from_le_bytes(
111            *<&[u8; 8]>::try_from(&serialized[4..12])
112                .expect("BUG: slice of serialized should be 8 bytes"),
113        ) & SIGN_MASK; // zero out the sign bit
114
115        // negate if necessary
116        Self(if is_negative { -value } else { value })
117    }
118
119    /// Convert a [`BigDecimal`](bigdecimal::BigDecimal) value into money using the correct precision
120    /// defined in the PostgreSQL settings. The default precision is two.
121    #[cfg(feature = "bigdecimal")]
122    pub fn from_bigdecimal(
123        decimal: bigdecimal::BigDecimal,
124        locale_frac_digits: u32,
125    ) -> Result<Self, BoxDynError> {
126        use bigdecimal::ToPrimitive;
127
128        let multiplier = bigdecimal::BigDecimal::new(
129            num_bigint::BigInt::from(10i128.pow(locale_frac_digits)),
130            0,
131        );
132
133        let cents = decimal * multiplier;
134
135        let money = cents.to_i64().ok_or_else(|| {
136            io::Error::new(
137                io::ErrorKind::InvalidData,
138                "Provided BigDecimal could not convert to i64: overflow.",
139            )
140        })?;
141
142        Ok(Self(money))
143    }
144}
145
146impl Type<Postgres> for PgMoney {
147    fn type_info() -> PgTypeInfo {
148        PgTypeInfo::MONEY
149    }
150}
151
152impl PgHasArrayType for PgMoney {
153    fn array_type_info() -> PgTypeInfo {
154        PgTypeInfo::MONEY_ARRAY
155    }
156}
157
158impl<T> From<T> for PgMoney
159where
160    T: Into<i64>,
161{
162    fn from(num: T) -> Self {
163        Self(num.into())
164    }
165}
166
167impl Encode<'_, Postgres> for PgMoney {
168    fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result<IsNull, BoxDynError> {
169        buf.extend(&self.0.to_be_bytes());
170
171        Ok(IsNull::No)
172    }
173}
174
175impl Decode<'_, Postgres> for PgMoney {
176    fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
177        match value.format() {
178            PgValueFormat::Binary => {
179                let cents = BigEndian::read_i64(value.as_bytes()?);
180
181                Ok(PgMoney(cents))
182            }
183            PgValueFormat::Text => {
184                let error = io::Error::new(
185                    io::ErrorKind::InvalidData,
186                    "Reading a `MONEY` value in text format is not supported.",
187                );
188
189                Err(Box::new(error))
190            }
191        }
192    }
193}
194
195impl Add<PgMoney> for PgMoney {
196    type Output = PgMoney;
197
198    /// Adds two monetary values.
199    ///
200    /// # Panics
201    /// Panics if overflowing the `i64::MAX`.
202    fn add(self, rhs: PgMoney) -> Self::Output {
203        self.0
204            .checked_add(rhs.0)
205            .map(PgMoney)
206            .expect("overflow adding money amounts")
207    }
208}
209
210impl AddAssign<PgMoney> for PgMoney {
211    /// An assigning add for two monetary values.
212    ///
213    /// # Panics
214    /// Panics if overflowing the `i64::MAX`.
215    fn add_assign(&mut self, rhs: PgMoney) {
216        self.0 = self
217            .0
218            .checked_add(rhs.0)
219            .expect("overflow adding money amounts")
220    }
221}
222
223impl Sub<PgMoney> for PgMoney {
224    type Output = PgMoney;
225
226    /// Subtracts two monetary values.
227    ///
228    /// # Panics
229    /// Panics if underflowing the `i64::MIN`.
230    fn sub(self, rhs: PgMoney) -> Self::Output {
231        self.0
232            .checked_sub(rhs.0)
233            .map(PgMoney)
234            .expect("overflow subtracting money amounts")
235    }
236}
237
238impl SubAssign<PgMoney> for PgMoney {
239    /// An assigning subtract for two monetary values.
240    ///
241    /// # Panics
242    /// Panics if underflowing the `i64::MIN`.
243    fn sub_assign(&mut self, rhs: PgMoney) {
244        self.0 = self
245            .0
246            .checked_sub(rhs.0)
247            .expect("overflow subtracting money amounts")
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::PgMoney;
254
255    #[test]
256    fn adding_works() {
257        assert_eq!(PgMoney(3), PgMoney(1) + PgMoney(2))
258    }
259
260    #[test]
261    fn add_assign_works() {
262        let mut money = PgMoney(1);
263        money += PgMoney(2);
264
265        assert_eq!(PgMoney(3), money);
266    }
267
268    #[test]
269    fn subtracting_works() {
270        assert_eq!(PgMoney(4), PgMoney(5) - PgMoney(1))
271    }
272
273    #[test]
274    fn sub_assign_works() {
275        let mut money = PgMoney(1);
276        money -= PgMoney(2);
277
278        assert_eq!(PgMoney(-1), money);
279    }
280
281    #[test]
282    fn default_value() {
283        let money = PgMoney::default();
284
285        assert_eq!(money, PgMoney(0));
286    }
287
288    #[test]
289    #[should_panic]
290    fn add_overflow_panics() {
291        let _ = PgMoney(i64::MAX) + PgMoney(1);
292    }
293
294    #[test]
295    #[should_panic]
296    fn add_assign_overflow_panics() {
297        let mut money = PgMoney(i64::MAX);
298        money += PgMoney(1);
299    }
300
301    #[test]
302    #[should_panic]
303    fn sub_overflow_panics() {
304        let _ = PgMoney(i64::MIN) - PgMoney(1);
305    }
306
307    #[test]
308    #[should_panic]
309    fn sub_assign_overflow_panics() {
310        let mut money = PgMoney(i64::MIN);
311        money -= PgMoney(1);
312    }
313
314    #[test]
315    #[cfg(feature = "bigdecimal")]
316    fn conversion_to_bigdecimal_works() {
317        let money = PgMoney(12345);
318
319        assert_eq!(
320            bigdecimal::BigDecimal::new(num_bigint::BigInt::from(12345), 2),
321            money.to_bigdecimal(2)
322        );
323    }
324
325    #[test]
326    #[cfg(feature = "rust_decimal")]
327    fn conversion_to_decimal_works() {
328        assert_eq!(
329            rust_decimal::Decimal::new(12345, 2),
330            PgMoney(12345).to_decimal(2)
331        );
332    }
333
334    #[test]
335    #[cfg(feature = "rust_decimal")]
336    fn conversion_from_decimal_works() {
337        assert_eq!(
338            PgMoney(12345),
339            PgMoney::from_decimal(rust_decimal::Decimal::new(12345, 2), 2)
340        );
341
342        assert_eq!(
343            PgMoney(12345),
344            PgMoney::from_decimal(rust_decimal::Decimal::new(123450, 3), 2)
345        );
346
347        assert_eq!(
348            PgMoney(-12345),
349            PgMoney::from_decimal(rust_decimal::Decimal::new(-123450, 3), 2)
350        );
351
352        assert_eq!(
353            PgMoney(-12300),
354            PgMoney::from_decimal(rust_decimal::Decimal::new(-123, 0), 2)
355        );
356    }
357
358    #[test]
359    #[cfg(feature = "bigdecimal")]
360    fn conversion_from_bigdecimal_works() {
361        let dec = bigdecimal::BigDecimal::new(num_bigint::BigInt::from(12345), 2);
362
363        assert_eq!(PgMoney(12345), PgMoney::from_bigdecimal(dec, 2).unwrap());
364    }
365}