Implement a proper type for money

This commit is contained in:
Julius de Bruijn 2020-07-03 11:58:37 +02:00 committed by Ryan Leckey
parent 0c01ca966c
commit b3db51cdfc
3 changed files with 228 additions and 1 deletions

View file

@ -14,9 +14,11 @@
//! | `&[u8]`, `Vec<u8>` | BYTEA |
//! | [`PgInterval`] | INTERVAL |
//! | [`PgRange<T>`] | INT8RANGE, INT4RANGE, TSRANGE, TSTZTRANGE, DATERANGE, NUMRANGE |
//! | [`PgMoney`] | MONEY |
//!
//! [`PgInterval`]: struct.PgInterval.html
//! [`PgRange<T>`]: struct.PgRange.html
//! [`PgMoney`]: struct.PgMoney.html
//!
//! ### [`chrono`](https://crates.io/crates/chrono)
//!
@ -143,6 +145,7 @@ mod bytes;
mod float;
mod int;
mod interval;
mod money;
mod range;
mod record;
mod str;
@ -170,6 +173,7 @@ mod json;
mod ipnetwork;
pub use interval::PgInterval;
pub use money::PgMoney;
pub use range::PgRange;
// used in derive(Type) for `struct`

View file

@ -0,0 +1,217 @@
use crate::{
decode::Decode,
encode::{Encode, IsNull},
error::BoxDynError,
postgres::{PgArgumentBuffer, PgTypeInfo, PgValueFormat, PgValueRef, Postgres},
types::Type,
};
use byteorder::{BigEndian, ByteOrder};
use std::{
io,
ops::{Add, AddAssign, Sub, SubAssign},
};
/// The PostgreSQL [`MONEY`] type stores a currency amount with a fixed fractional
/// precision. The fractional precision is determined by the database's
/// `lc_monetary` setting.
///
/// Data is read and written as 64-bit signed integers, and conversion into a
/// decimal should be done using the right precision.
///
/// Reading `MONEY` value in text format is not supported and will cause an error.
///
/// [`MONEY`]: https://www.postgresql.org/docs/current/datatype-money.html
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct PgMoney(pub i64);
impl PgMoney {
/// Convert the money value into a [`BigDecimal`] using the correct precision
/// defined in the PostgreSQL settings. The default precision is two.
///
/// [`BigDecimal`]: ../../types/struct.BigDecimal.html
#[cfg(feature = "bigdecimal")]
pub fn as_bigdecimal(&self, scale: i64) -> bigdecimal::BigDecimal {
let digits = num_bigint::BigInt::from(self.0);
bigdecimal::BigDecimal::new(digits, scale)
}
}
impl Type<Postgres> for PgMoney {
fn type_info() -> PgTypeInfo {
PgTypeInfo::MONEY
}
}
impl Type<Postgres> for [PgMoney] {
fn type_info() -> PgTypeInfo {
PgTypeInfo::MONEY_ARRAY
}
}
impl Type<Postgres> for Vec<PgMoney> {
fn type_info() -> PgTypeInfo {
<[PgMoney] as Type<Postgres>>::type_info()
}
}
impl<T> From<T> for PgMoney
where
T: Into<i64>,
{
fn from(num: T) -> Self {
Self(num.into())
}
}
impl Encode<'_, Postgres> for PgMoney {
fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> IsNull {
buf.extend(&self.0.to_be_bytes());
IsNull::No
}
}
impl Decode<'_, Postgres> for PgMoney {
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
match value.format() {
PgValueFormat::Binary => {
let cents = BigEndian::read_i64(value.as_bytes()?);
Ok(PgMoney(cents))
}
PgValueFormat::Text => {
let error = io::Error::new(
io::ErrorKind::InvalidData,
"Reading a `MONEY` value in text format is not supported.",
);
Err(Box::new(error))
}
}
}
}
impl Add<PgMoney> for PgMoney {
type Output = PgMoney;
/// Adds two monetary values.
///
/// # Panics
/// Panics if overflowing the `i64::MAX`.
fn add(self, rhs: PgMoney) -> Self::Output {
self.0
.checked_add(rhs.0)
.map(PgMoney)
.expect("overflow adding money amounts")
}
}
impl AddAssign<PgMoney> for PgMoney {
/// An assigning add for two monetary values.
///
/// # Panics
/// Panics if overflowing the `i64::MAX`.
fn add_assign(&mut self, rhs: PgMoney) {
self.0 = self
.0
.checked_add(rhs.0)
.expect("overflow adding money amounts")
}
}
impl Sub<PgMoney> for PgMoney {
type Output = PgMoney;
/// Subtracts two monetary values.
///
/// # Panics
/// Panics if underflowing the `i64::MIN`.
fn sub(self, rhs: PgMoney) -> Self::Output {
self.0
.checked_sub(rhs.0)
.map(PgMoney)
.expect("overflow subtracting money amounts")
}
}
impl SubAssign<PgMoney> for PgMoney {
/// An assigning subtract for two monetary values.
///
/// # Panics
/// Panics if underflowing the `i64::MIN`.
fn sub_assign(&mut self, rhs: PgMoney) {
self.0 = self
.0
.checked_sub(rhs.0)
.expect("overflow subtracting money amounts")
}
}
#[cfg(test)]
mod tests {
use super::PgMoney;
#[test]
fn adding_works() {
assert_eq!(PgMoney(3), PgMoney(1) + PgMoney(2))
}
#[test]
fn add_assign_works() {
let mut money = PgMoney(1);
money += PgMoney(2);
assert_eq!(PgMoney(3), money);
}
#[test]
fn subtracting_works() {
assert_eq!(PgMoney(4), PgMoney(5) - PgMoney(1))
}
#[test]
fn sub_assign_works() {
let mut money = PgMoney(1);
money -= PgMoney(2);
assert_eq!(PgMoney(-1), money);
}
#[test]
#[should_panic]
fn add_overflow_panics() {
let _ = PgMoney(i64::MAX) + PgMoney(1);
}
#[test]
#[should_panic]
fn add_assign_overflow_panics() {
let mut money = PgMoney(i64::MAX);
money += PgMoney(1);
}
#[test]
#[should_panic]
fn sub_overflow_panics() {
let _ = PgMoney(i64::MIN) - PgMoney(1);
}
#[test]
#[should_panic]
fn sub_assign_overflow_panics() {
let mut money = PgMoney(i64::MIN);
money -= PgMoney(1);
}
#[test]
#[cfg(feature = "bigdecimal")]
fn conversion_to_bigdecimal_works() {
let money = PgMoney(12345);
assert_eq!(
bigdecimal::BigDecimal::new(num_bigint::BigInt::from(12345), 2),
money.as_bigdecimal(2)
);
}
}

View file

@ -2,7 +2,7 @@ extern crate time_ as time;
use std::ops::Bound;
use sqlx::postgres::types::{PgInterval, PgRange};
use sqlx::postgres::types::{PgInterval, PgMoney, PgRange};
use sqlx::postgres::Postgres;
use sqlx_test::{test_decode_type, test_prepared_type, test_type};
@ -388,3 +388,9 @@ test_prepared_type!(interval<PgInterval>(
microseconds: (3 * 3_600 + 10 * 60 + 20) * 1_000_000 + 116100
},
));
test_prepared_type!(money<PgMoney>(Postgres, "123.45::money" == PgMoney(12345)));
test_prepared_type!(money_vec<Vec<PgMoney>>(Postgres,
"array[123.45,420.00,666.66]::money[]" == vec![PgMoney(12345), PgMoney(42000), PgMoney(66666)],
));