diff --git a/Cargo.lock b/Cargo.lock index 7315383652..ea3d18f7d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3075,7 +3075,6 @@ dependencies = [ "env_logger", "futures", "hex", - "libsqlite3-sys", "paste", "rand 0.8.5", "rand_xoshiro", diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index 0fd05720c5..34ad38e13a 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -16,7 +16,7 @@ migrate = ["sqlx-core/migrate"] offline = ["sqlx-core/offline"] # Type integration features which require additional dependencies -rust_decimal = ["dep:rust_decimal", "dep:num-bigint"] +rust_decimal = ["dep:rust_decimal", "rust_decimal/maths"] bigdecimal = ["dep:bigdecimal", "dep:num-bigint"] [dependencies] diff --git a/sqlx-postgres/src/types/rust_decimal.rs b/sqlx-postgres/src/types/rust_decimal.rs index 7447564c69..9eea096247 100644 --- a/sqlx-postgres/src/types/rust_decimal.rs +++ b/sqlx-postgres/src/types/rust_decimal.rs @@ -1,8 +1,4 @@ -use num_bigint::{BigInt, Sign}; -use rust_decimal::{ - prelude::{ToPrimitive, Zero}, - Decimal, -}; +use rust_decimal::{prelude::Zero, Decimal}; use crate::decode::Decode; use crate::encode::{Encode, IsNull}; @@ -11,6 +7,8 @@ use crate::types::numeric::{PgNumeric, PgNumericSign}; use crate::types::Type; use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use rust_decimal::MathematicalOps; + impl Type for Decimal { fn type_info() -> PgTypeInfo { PgTypeInfo::NUMERIC @@ -27,7 +25,7 @@ impl TryFrom for Decimal { type Error = BoxDynError; fn try_from(numeric: PgNumeric) -> Result { - let (digits, sign, weight) = match numeric { + let (digits, sign, mut weight) = match numeric { PgNumeric::Number { digits, sign, @@ -41,39 +39,33 @@ impl TryFrom for Decimal { }; if digits.is_empty() { - // Postgres returns an empty digit array for 0 but BigInt expects at least one zero + // Postgres returns an empty digit array for 0 return Ok(0u64.into()); } - let sign = match sign { - PgNumericSign::Positive => Sign::Plus, - PgNumericSign::Negative => Sign::Minus, - }; + let mut value = Decimal::ZERO; + + // Sum over `digits`, multiply each by its weight and add it to `value`. + for digit in digits { + let mul = Decimal::from(10_000i16) + .checked_powi(weight as i64) + .ok_or("value not representable as rust_decimal::Decimal")?; + + let part = Decimal::from(digit) * mul; - // weight is 0 if the decimal point falls after the first base-10000 digit - let scale = (digits.len() as i64 - weight as i64 - 1) * 4; + value = value + .checked_add(part) + .ok_or("value not representable as rust_decimal::Decimal")?; - // no optimized algorithm for base-10 so use base-100 for faster processing - let mut cents = Vec::with_capacity(digits.len() * 2); - for digit in &digits { - cents.push((digit / 100) as u8); - cents.push((digit % 100) as u8); + weight = weight.checked_sub(1).ok_or("weight underflowed")?; } - let bigint = BigInt::from_radix_be(sign, ¢s, 100) - .ok_or("PgNumeric contained an out-of-range digit")?; - - match (bigint.to_i128(), scale) { - // A negative scale, meaning we have nothing on the right and must - // add zeroes to the left. - (Some(num), scale) if scale < 0 => Ok(Decimal::from_i128_with_scale( - num * 10i128.pow(scale.abs() as u32), - 0, - )), - // A positive scale, so we have decimals on the right. - (Some(num), _) => Ok(Decimal::from_i128_with_scale(num, scale as u32)), - (None, _) => Err("Decimal's integer part out of range.".into()), + match sign { + PgNumericSign::Positive => value.set_sign_positive(true), + PgNumericSign::Negative => value.set_sign_negative(true), } + + Ok(value) } } @@ -403,4 +395,7 @@ mod decimal_to_pgnumeric { } ); } + + #[test] + fn issue_666_trailing_zeroes_at_max_precision() {} } diff --git a/tests/mysql/types.rs b/tests/mysql/types.rs index da931959da..497e4576b7 100644 --- a/tests/mysql/types.rs +++ b/tests/mysql/types.rs @@ -1,6 +1,6 @@ extern crate time_ as time; -#[cfg(feature = "decimal")] +#[cfg(feature = "rust_decimal")] use std::str::FromStr; use sqlx::mysql::MySql; @@ -223,7 +223,7 @@ test_type!(bigdecimal( "CAST(12345.6789 AS DECIMAL(9, 4))" == "12345.6789".parse::().unwrap(), )); -#[cfg(feature = "decimal")] +#[cfg(feature = "rust_decimal")] test_type!(decimal(MySql, "CAST(0 as DECIMAL(0, 0))" == sqlx::types::Decimal::from_str("0").unwrap(), "CAST(1 AS DECIMAL(1, 0))" == sqlx::types::Decimal::from_str("1").unwrap(), diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 445e9fafe9..57cf0b9dc6 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -6,7 +6,6 @@ use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange}; use sqlx::postgres::Postgres; use sqlx_test::{test_decode_type, test_prepared_type, test_type}; -#[cfg(any(postgres_14, postgres_15))] use std::str::FromStr; test_type!(null>(Postgres, @@ -475,7 +474,7 @@ test_type!(numrange_bigdecimal>(Postgres, Bound::Excluded("2.4".parse::().unwrap()))) )); -#[cfg(feature = "decimal")] +#[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), "1::numeric" == sqlx::types::Decimal::from_str("1").unwrap(), @@ -484,9 +483,12 @@ test_type!(decimal(Postgres, "0.01234::numeric" == sqlx::types::Decimal::from_str("0.01234").unwrap(), "12.34::numeric" == sqlx::types::Decimal::from_str("12.34").unwrap(), "12345.6789::numeric" == sqlx::types::Decimal::from_str("12345.6789").unwrap(), + // https://github.com/launchbadge/sqlx/issues/666#issuecomment-683872154 + "17.905625985174584660842500258::numeric" == sqlx::types::Decimal::from_str("17.905625985174584660842500258").unwrap(), + "-17.905625985174584660842500258::numeric" == sqlx::types::Decimal::from_str("-17.905625985174584660842500258").unwrap(), )); -#[cfg(feature = "decimal")] +#[cfg(feature = "rust_decimal")] test_type!(numrange_decimal>(Postgres, "'(1.3,2.4)'::numrange" == PgRange::from( (Bound::Excluded(sqlx::types::Decimal::from_str("1.3").unwrap()),