From 2f6578cd9700f70bf7f0abd26cde35a19ae46731 Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Mon, 6 Apr 2020 16:53:27 +0800 Subject: [PATCH 1/7] implement `DECIMAL` type support for mysql --- sqlx-core/src/mysql/protocol/row.rs | 2 + sqlx-core/src/mysql/protocol/type.rs | 2 +- sqlx-core/src/mysql/types/bigdecimal.rs | 141 ++++++++++++++++++++++++ sqlx-core/src/mysql/types/mod.rs | 10 ++ sqlx-macros/src/database/mysql.rs | 3 + 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 sqlx-core/src/mysql/types/bigdecimal.rs diff --git a/sqlx-core/src/mysql/protocol/row.rs b/sqlx-core/src/mysql/protocol/row.rs index f3b82d40be..2fdd0fa88d 100644 --- a/sqlx-core/src/mysql/protocol/row.rs +++ b/sqlx-core/src/mysql/protocol/row.rs @@ -136,6 +136,8 @@ impl<'c> Row<'c> { (len_size, len.unwrap_or_default()) } + TypeId::NEWDECIMAL => (0, 1 + buffer[index] as usize), + id => { unimplemented!("encountered unknown field type id: {:?}", id); } diff --git a/sqlx-core/src/mysql/protocol/type.rs b/sqlx-core/src/mysql/protocol/type.rs index c839933ace..70f64789dd 100644 --- a/sqlx-core/src/mysql/protocol/type.rs +++ b/sqlx-core/src/mysql/protocol/type.rs @@ -31,7 +31,7 @@ impl TypeId { // Numeric: FLOAT, DOUBLE pub const FLOAT: TypeId = TypeId(4); pub const DOUBLE: TypeId = TypeId(5); - // pub const NEWDECIMAL: TypeId = TypeId(246); + pub const NEWDECIMAL: TypeId = TypeId(246); // Date/Time: DATE, TIME, DATETIME, TIMESTAMP pub const DATE: TypeId = TypeId(10); diff --git a/sqlx-core/src/mysql/types/bigdecimal.rs b/sqlx-core/src/mysql/types/bigdecimal.rs new file mode 100644 index 0000000000..0215d5b952 --- /dev/null +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -0,0 +1,141 @@ +use bigdecimal::{BigDecimal, Signed}; +use num_bigint::{BigInt, Sign}; + +use crate::decode::Decode; +use crate::encode::{Encode}; +use crate::types::Type; +use crate::mysql::protocol::TypeId; +use crate::mysql::{MySql, MySqlValue, MySqlTypeInfo, MySqlData}; +use crate::Error; +use crate::io::Buf; + +const SIGN_NEG: u8 = 0x2D; +const SCALE_START: u8 = 0x2E; + +impl Type for BigDecimal { + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::new(TypeId::NEWDECIMAL) + } +} + +impl Encode for BigDecimal { + fn encode(&self, buf: &mut Vec) { + let size = Encode::::size_hint(self) - 1; + + assert!(size <= u8::MAX as usize, "Too large size"); + + buf.push(size as u8); + + if self.is_negative() { + buf.push(SIGN_NEG); + } + + let (bi, scale) = self.as_bigint_and_exponent(); + let (_, mut radix) = bi.to_radix_be(10); + let mut scale_index: Option = None; + + if scale < 0 { + radix.append(&mut vec![0u8; -scale as usize]); + } else { + let scale = scale as usize; + if scale >= radix.len() { + let mut radix_temp = vec![0u8; scale - radix.len() + 1]; + radix_temp.append(&mut radix); + radix = radix_temp; + scale_index = Some(0); + } else { + scale_index = Some(radix.len() - scale - 1); + } + } + + for (i, data) in radix.iter().enumerate() { + buf.push(*data + 0x30); + if let Some(si) = scale_index { + if si == i { + buf.push(SCALE_START); + scale_index = None; + } + } + } + } + + /// 15, -2 => 1500 + /// 15, 1 => 1.5 + /// 15, 2 => 0.15 + /// 15, 3 => 0.015 + + fn size_hint(&self) -> usize { + let (bi, scale) = self.as_bigint_and_exponent(); + let (_, radix) = bi.to_radix_be(10); + let mut s = radix.len(); + + if scale < 0 { + s = s + (-scale) as usize + } else if scale > 0 { + let scale = scale as usize; + if scale >= s { + s = scale + 1 + } + s = s + 1; + } + + if self.is_negative() { + s = s + 1; + } + s + 1 + } +} + +impl Decode<'_, MySql> for BigDecimal { + fn decode(value: MySqlValue) -> crate::Result { + match value.try_get()? { + MySqlData::Binary(mut binary) => { + let len = binary.get_u8()?; + let mut negative = false; + let mut scale: Option = None; + let mut v: Vec = Vec::with_capacity(len as usize); + + loop { + if binary.len() < 1 { + break + } + let data = binary.get_u8()?; + match data { + SIGN_NEG => { + if !negative { + negative = true; + } else { + return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())); + } + }, + SCALE_START => { + if scale.is_none() { + scale = Some(0); + } else { + return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())); + } + }, + 0x30..=0x39 => { + scale = scale.map(|s| s + 1); + v.push(data - 0x30); + }, + _ => return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())), + } + } + + let r = BigInt::from_radix_be( + if negative { Sign::Minus } else { Sign::Plus }, + v.as_slice(), + 10, + ).ok_or(Error::Decode("Can't convert to BigInt".into()))?; + + Ok(BigDecimal::new(r, scale.unwrap_or(0))) + }, + MySqlData::Text(_) => { + Err(Error::Decode( + "`BigDecimal` can only be decoded from the binary protocol".into(), + )) + }, + } + } +} diff --git a/sqlx-core/src/mysql/types/mod.rs b/sqlx-core/src/mysql/types/mod.rs index d2fac66d13..11d31ab7eb 100644 --- a/sqlx-core/src/mysql/types/mod.rs +++ b/sqlx-core/src/mysql/types/mod.rs @@ -41,6 +41,13 @@ //! | `time::Date` | DATE | //! | `time::Time` | TIME | //! +//! ### [`bigdecimal`](https://crates.io/crates/bigdecimal) +//! Requires the `bigdecimal` Cargo feature flag. +//! +//! | Rust type | MySQL type(s) | +//! |---------------------------------------|------------------------------------------------------| +//! | `bigdecimal::BigDecimal` | DECIMAL | +//! //! # Nullable //! //! In addition, `Option` is supported where `T` implements `Type`. An `Option` represents @@ -54,6 +61,9 @@ mod int; mod str; mod uint; +#[cfg(feature = "bigdecimal")] +mod bigdecimal; + #[cfg(feature = "chrono")] mod chrono; diff --git a/sqlx-macros/src/database/mysql.rs b/sqlx-macros/src/database/mysql.rs index 1593f1114e..dee86a2c2b 100644 --- a/sqlx-macros/src/database/mysql.rs +++ b/sqlx-macros/src/database/mysql.rs @@ -40,6 +40,9 @@ impl_database_ext! { #[cfg(feature = "time")] sqlx::types::time::OffsetDateTime, + + #[cfg(feature = "bigdecimal")] + sqlx::types::BigDecimal, }, ParamChecking::Weak, feature-types: info => info.type_feature_gate(), From 0e3ad506d8bd1438f1219602d6138a49024aef56 Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Mon, 6 Apr 2020 18:09:45 +0800 Subject: [PATCH 2/7] add test_encode_decimal, test_decode_decimal --- sqlx-core/src/mysql/types/bigdecimal.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/sqlx-core/src/mysql/types/bigdecimal.rs b/sqlx-core/src/mysql/types/bigdecimal.rs index 0215d5b952..a343bce913 100644 --- a/sqlx-core/src/mysql/types/bigdecimal.rs +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -139,3 +139,42 @@ impl Decode<'_, MySql> for BigDecimal { } } } + +#[test] +fn test_encode_decimal() { + let v = BigDecimal::new(BigInt::from(-105), 2); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]); + + let v = BigDecimal::new(BigInt::from(-105), -3); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x07, 0x2D, 0x31, 0x30, 0x35, 0x30, 0x30, 0x30]); + + let v = BigDecimal::new(BigInt::from(105), 5); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x07, 0x30, 0x2E, 0x30, 0x30, 0x31, 0x30, 0x35]); +} + +#[test] +fn test_decode_decimal() { + let buf: Vec = vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), + )).unwrap(); + assert_eq!(v.to_string(), "-1.05"); + + let buf: Vec = vec![0x04, 0x30, 0x2E, 0x30, 0x35]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), + )).unwrap(); + assert_eq!(v.to_string(), "0.05"); + + let buf: Vec = vec![0x06, 0x2D, 0x39, 0x30, 0x30, 0x30, 0x30]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), + )).unwrap(); + assert_eq!(v.to_string(), "-90000"); +} \ No newline at end of file From 25f0f32d6c985cd8edf0d0063eff2b14d5820eac Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Mon, 6 Apr 2020 18:51:34 +0800 Subject: [PATCH 3/7] fix: change u8::MAX to std::u8::MAX --- sqlx-core/src/mysql/types/bigdecimal.rs | 301 ++++++++++++------------ 1 file changed, 157 insertions(+), 144 deletions(-) diff --git a/sqlx-core/src/mysql/types/bigdecimal.rs b/sqlx-core/src/mysql/types/bigdecimal.rs index a343bce913..4be6c3249c 100644 --- a/sqlx-core/src/mysql/types/bigdecimal.rs +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -2,179 +2,192 @@ use bigdecimal::{BigDecimal, Signed}; use num_bigint::{BigInt, Sign}; use crate::decode::Decode; -use crate::encode::{Encode}; -use crate::types::Type; +use crate::encode::Encode; +use crate::io::Buf; use crate::mysql::protocol::TypeId; -use crate::mysql::{MySql, MySqlValue, MySqlTypeInfo, MySqlData}; +use crate::mysql::{MySql, MySqlData, MySqlTypeInfo, MySqlValue}; +use crate::types::Type; use crate::Error; -use crate::io::Buf; const SIGN_NEG: u8 = 0x2D; const SCALE_START: u8 = 0x2E; impl Type for BigDecimal { - fn type_info() -> MySqlTypeInfo { - MySqlTypeInfo::new(TypeId::NEWDECIMAL) - } + fn type_info() -> MySqlTypeInfo { + MySqlTypeInfo::new(TypeId::NEWDECIMAL) + } } impl Encode for BigDecimal { - fn encode(&self, buf: &mut Vec) { - let size = Encode::::size_hint(self) - 1; + fn encode(&self, buf: &mut Vec) { + let size = Encode::::size_hint(self) - 1; - assert!(size <= u8::MAX as usize, "Too large size"); + assert!(size <= std::u8::MAX as usize, "Too large size"); - buf.push(size as u8); + buf.push(size as u8); - if self.is_negative() { - buf.push(SIGN_NEG); - } + if self.is_negative() { + buf.push(SIGN_NEG); + } - let (bi, scale) = self.as_bigint_and_exponent(); - let (_, mut radix) = bi.to_radix_be(10); - let mut scale_index: Option = None; - - if scale < 0 { - radix.append(&mut vec![0u8; -scale as usize]); - } else { - let scale = scale as usize; - if scale >= radix.len() { - let mut radix_temp = vec![0u8; scale - radix.len() + 1]; - radix_temp.append(&mut radix); - radix = radix_temp; - scale_index = Some(0); - } else { - scale_index = Some(radix.len() - scale - 1); - } - } + let (bi, scale) = self.as_bigint_and_exponent(); + let (_, mut radix) = bi.to_radix_be(10); + let mut scale_index: Option = None; + + if scale < 0 { + radix.append(&mut vec![0u8; -scale as usize]); + } else { + let scale = scale as usize; + if scale >= radix.len() { + let mut radix_temp = vec![0u8; scale - radix.len() + 1]; + radix_temp.append(&mut radix); + radix = radix_temp; + scale_index = Some(0); + } else { + scale_index = Some(radix.len() - scale - 1); + } + } - for (i, data) in radix.iter().enumerate() { - buf.push(*data + 0x30); - if let Some(si) = scale_index { - if si == i { - buf.push(SCALE_START); - scale_index = None; + for (i, data) in radix.iter().enumerate() { + buf.push(*data + 0x30); + if let Some(si) = scale_index { + if si == i { + buf.push(SCALE_START); + scale_index = None; + } + } } - } - } - } - - /// 15, -2 => 1500 - /// 15, 1 => 1.5 - /// 15, 2 => 0.15 - /// 15, 3 => 0.015 - - fn size_hint(&self) -> usize { - let (bi, scale) = self.as_bigint_and_exponent(); - let (_, radix) = bi.to_radix_be(10); - let mut s = radix.len(); - - if scale < 0 { - s = s + (-scale) as usize - } else if scale > 0 { - let scale = scale as usize; - if scale >= s { - s = scale + 1 - } - s = s + 1; } - if self.is_negative() { - s = s + 1; + /// 15, -2 => 1500 + /// 15, 1 => 1.5 + /// 15, 2 => 0.15 + /// 15, 3 => 0.015 + + fn size_hint(&self) -> usize { + let (bi, scale) = self.as_bigint_and_exponent(); + let (_, radix) = bi.to_radix_be(10); + let mut s = radix.len(); + + if scale < 0 { + s = s + (-scale) as usize + } else if scale > 0 { + let scale = scale as usize; + if scale >= s { + s = scale + 1 + } + s = s + 1; + } + + if self.is_negative() { + s = s + 1; + } + s + 1 } - s + 1 - } } impl Decode<'_, MySql> for BigDecimal { - fn decode(value: MySqlValue) -> crate::Result { - match value.try_get()? { - MySqlData::Binary(mut binary) => { - let len = binary.get_u8()?; - let mut negative = false; - let mut scale: Option = None; - let mut v: Vec = Vec::with_capacity(len as usize); - - loop { - if binary.len() < 1 { - break - } - let data = binary.get_u8()?; - match data { - SIGN_NEG => { - if !negative { - negative = true; - } else { - return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())); - } - }, - SCALE_START => { - if scale.is_none() { - scale = Some(0); - } else { - return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())); - } - }, - 0x30..=0x39 => { - scale = scale.map(|s| s + 1); - v.push(data - 0x30); - }, - _ => return Err(Error::Decode(format!("Unexpected byte: {:X?}", data).into())), - } + fn decode(value: MySqlValue) -> crate::Result { + match value.try_get()? { + MySqlData::Binary(mut binary) => { + let len = binary.get_u8()?; + let mut negative = false; + let mut scale: Option = None; + let mut v: Vec = Vec::with_capacity(len as usize); + + loop { + if binary.len() < 1 { + break; + } + let data = binary.get_u8()?; + match data { + SIGN_NEG => { + if !negative { + negative = true; + } else { + return Err(Error::Decode( + format!("Unexpected byte: {:X?}", data).into(), + )); + } + } + SCALE_START => { + if scale.is_none() { + scale = Some(0); + } else { + return Err(Error::Decode( + format!("Unexpected byte: {:X?}", data).into(), + )); + } + } + 0x30..=0x39 => { + scale = scale.map(|s| s + 1); + v.push(data - 0x30); + } + _ => { + return Err(Error::Decode( + format!("Unexpected byte: {:X?}", data).into(), + )) + } + } + } + + let r = BigInt::from_radix_be( + if negative { Sign::Minus } else { Sign::Plus }, + v.as_slice(), + 10, + ) + .ok_or(Error::Decode("Can't convert to BigInt".into()))?; + + Ok(BigDecimal::new(r, scale.unwrap_or(0))) + } + MySqlData::Text(_) => Err(Error::Decode( + "`BigDecimal` can only be decoded from the binary protocol".into(), + )), } - - let r = BigInt::from_radix_be( - if negative { Sign::Minus } else { Sign::Plus }, - v.as_slice(), - 10, - ).ok_or(Error::Decode("Can't convert to BigInt".into()))?; - - Ok(BigDecimal::new(r, scale.unwrap_or(0))) - }, - MySqlData::Text(_) => { - Err(Error::Decode( - "`BigDecimal` can only be decoded from the binary protocol".into(), - )) - }, } - } } #[test] fn test_encode_decimal() { - let v = BigDecimal::new(BigInt::from(-105), 2); - let mut buf: Vec = vec![]; - v.encode(&mut buf); - assert_eq!(buf, vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]); - - let v = BigDecimal::new(BigInt::from(-105), -3); - let mut buf: Vec = vec![]; - v.encode(&mut buf); - assert_eq!(buf, vec![0x07, 0x2D, 0x31, 0x30, 0x35, 0x30, 0x30, 0x30]); - - let v = BigDecimal::new(BigInt::from(105), 5); - let mut buf: Vec = vec![]; - v.encode(&mut buf); - assert_eq!(buf, vec![0x07, 0x30, 0x2E, 0x30, 0x30, 0x31, 0x30, 0x35]); + let v = BigDecimal::new(BigInt::from(-105), 2); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]); + + let v = BigDecimal::new(BigInt::from(-105), -3); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x07, 0x2D, 0x31, 0x30, 0x35, 0x30, 0x30, 0x30]); + + let v = BigDecimal::new(BigInt::from(105), 5); + let mut buf: Vec = vec![]; + v.encode(&mut buf); + assert_eq!(buf, vec![0x07, 0x30, 0x2E, 0x30, 0x30, 0x31, 0x30, 0x35]); } #[test] fn test_decode_decimal() { - let buf: Vec = vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]; - let v = BigDecimal::decode(MySqlValue::binary( - MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), - )).unwrap(); - assert_eq!(v.to_string(), "-1.05"); - - let buf: Vec = vec![0x04, 0x30, 0x2E, 0x30, 0x35]; - let v = BigDecimal::decode(MySqlValue::binary( - MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), - )).unwrap(); - assert_eq!(v.to_string(), "0.05"); - - let buf: Vec = vec![0x06, 0x2D, 0x39, 0x30, 0x30, 0x30, 0x30]; - let v = BigDecimal::decode(MySqlValue::binary( - MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), - )).unwrap(); - assert_eq!(v.to_string(), "-90000"); -} \ No newline at end of file + let buf: Vec = vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), + buf.as_slice(), + )) + .unwrap(); + assert_eq!(v.to_string(), "-1.05"); + + let buf: Vec = vec![0x04, 0x30, 0x2E, 0x30, 0x35]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), + buf.as_slice(), + )) + .unwrap(); + assert_eq!(v.to_string(), "0.05"); + + let buf: Vec = vec![0x06, 0x2D, 0x39, 0x30, 0x30, 0x30, 0x30]; + let v = BigDecimal::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), + buf.as_slice(), + )) + .unwrap(); + assert_eq!(v.to_string(), "-90000"); +} From 9cd3effdceb297582410051b6b4a622ab420609e Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Mon, 6 Apr 2020 19:04:34 +0800 Subject: [PATCH 4/7] fix: test --- sqlx-core/src/mysql/types/bigdecimal.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sqlx-core/src/mysql/types/bigdecimal.rs b/sqlx-core/src/mysql/types/bigdecimal.rs index 4be6c3249c..c5461f203d 100644 --- a/sqlx-core/src/mysql/types/bigdecimal.rs +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -151,24 +151,24 @@ impl Decode<'_, MySql> for BigDecimal { fn test_encode_decimal() { let v = BigDecimal::new(BigInt::from(-105), 2); let mut buf: Vec = vec![]; - v.encode(&mut buf); + >::encode(&v, &mut buf); assert_eq!(buf, vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]); let v = BigDecimal::new(BigInt::from(-105), -3); let mut buf: Vec = vec![]; - v.encode(&mut buf); + >::encode(&v, &mut buf); assert_eq!(buf, vec![0x07, 0x2D, 0x31, 0x30, 0x35, 0x30, 0x30, 0x30]); let v = BigDecimal::new(BigInt::from(105), 5); let mut buf: Vec = vec![]; - v.encode(&mut buf); + >::encode(&v, &mut buf); assert_eq!(buf, vec![0x07, 0x30, 0x2E, 0x30, 0x30, 0x31, 0x30, 0x35]); } #[test] fn test_decode_decimal() { let buf: Vec = vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]; - let v = BigDecimal::decode(MySqlValue::binary( + let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), )) @@ -176,7 +176,7 @@ fn test_decode_decimal() { assert_eq!(v.to_string(), "-1.05"); let buf: Vec = vec![0x04, 0x30, 0x2E, 0x30, 0x35]; - let v = BigDecimal::decode(MySqlValue::binary( + let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), )) @@ -184,7 +184,7 @@ fn test_decode_decimal() { assert_eq!(v.to_string(), "0.05"); let buf: Vec = vec![0x06, 0x2D, 0x39, 0x30, 0x30, 0x30, 0x30]; - let v = BigDecimal::decode(MySqlValue::binary( + let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), )) From 4041de15d90163358246c4e34e6c61681203247f Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Wed, 8 Apr 2020 15:29:15 +0800 Subject: [PATCH 5/7] add integration tests mysql-types.rs/decimal --- tests/mysql-types.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/mysql-types.rs b/tests/mysql-types.rs index 1b88eafc60..45af828da4 100644 --- a/tests/mysql-types.rs +++ b/tests/mysql-types.rs @@ -123,3 +123,15 @@ mod time_tests { .assume_utc() )); } + +#[cfg(feature = "bigdecimal")] +test_type!(decimal( + MySql, + sqlx::types::BigDecimal, + "CAST(1 AS DECIMAL(1, 0))" == "1".parse::().unwrap(), + "CAST(10000 AS DECIMAL(5, 0))" == "10000".parse::().unwrap(), + "CAST(0.1 AS DECIMAL(2, 1))" == "0.1".parse::().unwrap(), + "CAST(0.01234 AS DECIMAL(6, 5))" == "0.01234".parse::().unwrap(), + "CAST(12.34 AS DECIMAL(4, 2))" == "12.34".parse::().unwrap(), + "CAST(12345.6789 AS DECIMAL(9, 4))" == "12345.6789".parse::().unwrap(), +)); From 56799c281cb8e071d5801d13b78f6d4f98399038 Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Wed, 8 Apr 2020 17:24:13 +0800 Subject: [PATCH 6/7] refactor: mysql decimal value is just ascii str --- sqlx-core/src/mysql/types/bigdecimal.rs | 145 ++++-------------------- 1 file changed, 22 insertions(+), 123 deletions(-) diff --git a/sqlx-core/src/mysql/types/bigdecimal.rs b/sqlx-core/src/mysql/types/bigdecimal.rs index c5461f203d..5cdcc21361 100644 --- a/sqlx-core/src/mysql/types/bigdecimal.rs +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -1,5 +1,4 @@ -use bigdecimal::{BigDecimal, Signed}; -use num_bigint::{BigInt, Sign}; +use bigdecimal::BigDecimal; use crate::decode::Decode; use crate::encode::Encode; @@ -8,9 +7,7 @@ use crate::mysql::protocol::TypeId; use crate::mysql::{MySql, MySqlData, MySqlTypeInfo, MySqlValue}; use crate::types::Type; use crate::Error; - -const SIGN_NEG: u8 = 0x2D; -const SCALE_START: u8 = 0x2E; +use std::str::FromStr; impl Type for BigDecimal { fn type_info() -> MySqlTypeInfo { @@ -21,68 +18,15 @@ impl Type for BigDecimal { impl Encode for BigDecimal { fn encode(&self, buf: &mut Vec) { let size = Encode::::size_hint(self) - 1; - assert!(size <= std::u8::MAX as usize, "Too large size"); - buf.push(size as u8); - - if self.is_negative() { - buf.push(SIGN_NEG); - } - - let (bi, scale) = self.as_bigint_and_exponent(); - let (_, mut radix) = bi.to_radix_be(10); - let mut scale_index: Option = None; - - if scale < 0 { - radix.append(&mut vec![0u8; -scale as usize]); - } else { - let scale = scale as usize; - if scale >= radix.len() { - let mut radix_temp = vec![0u8; scale - radix.len() + 1]; - radix_temp.append(&mut radix); - radix = radix_temp; - scale_index = Some(0); - } else { - scale_index = Some(radix.len() - scale - 1); - } - } - - for (i, data) in radix.iter().enumerate() { - buf.push(*data + 0x30); - if let Some(si) = scale_index { - if si == i { - buf.push(SCALE_START); - scale_index = None; - } - } - } + let s = self.to_string(); + buf.extend_from_slice(s.as_bytes()); } - /// 15, -2 => 1500 - /// 15, 1 => 1.5 - /// 15, 2 => 0.15 - /// 15, 3 => 0.015 - fn size_hint(&self) -> usize { - let (bi, scale) = self.as_bigint_and_exponent(); - let (_, radix) = bi.to_radix_be(10); - let mut s = radix.len(); - - if scale < 0 { - s = s + (-scale) as usize - } else if scale > 0 { - let scale = scale as usize; - if scale >= s { - s = scale + 1 - } - s = s + 1; - } - - if self.is_negative() { - s = s + 1; - } - s + 1 + let s = self.to_string(); + s.as_bytes().len() + 1 } } @@ -90,84 +34,39 @@ impl Decode<'_, MySql> for BigDecimal { fn decode(value: MySqlValue) -> crate::Result { match value.try_get()? { MySqlData::Binary(mut binary) => { - let len = binary.get_u8()?; - let mut negative = false; - let mut scale: Option = None; - let mut v: Vec = Vec::with_capacity(len as usize); - - loop { - if binary.len() < 1 { - break; - } - let data = binary.get_u8()?; - match data { - SIGN_NEG => { - if !negative { - negative = true; - } else { - return Err(Error::Decode( - format!("Unexpected byte: {:X?}", data).into(), - )); - } - } - SCALE_START => { - if scale.is_none() { - scale = Some(0); - } else { - return Err(Error::Decode( - format!("Unexpected byte: {:X?}", data).into(), - )); - } - } - 0x30..=0x39 => { - scale = scale.map(|s| s + 1); - v.push(data - 0x30); - } - _ => { - return Err(Error::Decode( - format!("Unexpected byte: {:X?}", data).into(), - )) - } - } - } - - let r = BigInt::from_radix_be( - if negative { Sign::Minus } else { Sign::Plus }, - v.as_slice(), - 10, - ) - .ok_or(Error::Decode("Can't convert to BigInt".into()))?; - - Ok(BigDecimal::new(r, scale.unwrap_or(0))) + let _len = binary.get_u8()?; + let s = std::str::from_utf8(binary).map_err(Error::decode)?; + Ok(BigDecimal::from_str(s).map_err(Error::decode)?) + } + MySqlData::Text(s) => { + let s = std::str::from_utf8(s).map_err(Error::decode)?; + Ok(BigDecimal::from_str(s).map_err(Error::decode)?) } - MySqlData::Text(_) => Err(Error::Decode( - "`BigDecimal` can only be decoded from the binary protocol".into(), - )), } } } #[test] fn test_encode_decimal() { - let v = BigDecimal::new(BigInt::from(-105), 2); + let v: BigDecimal = BigDecimal::from_str("-1.05").unwrap(); let mut buf: Vec = vec![]; >::encode(&v, &mut buf); - assert_eq!(buf, vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]); + assert_eq!(buf, vec![0x05, b'-', b'1', b'.', b'0', b'5']); - let v = BigDecimal::new(BigInt::from(-105), -3); + let v: BigDecimal = BigDecimal::from_str("-105000").unwrap(); let mut buf: Vec = vec![]; >::encode(&v, &mut buf); - assert_eq!(buf, vec![0x07, 0x2D, 0x31, 0x30, 0x35, 0x30, 0x30, 0x30]); + assert_eq!(buf, vec![0x07, b'-', b'1', b'0', b'5', b'0', b'0', b'0']); - let v = BigDecimal::new(BigInt::from(105), 5); + let v: BigDecimal = BigDecimal::from_str("0.00105").unwrap(); let mut buf: Vec = vec![]; >::encode(&v, &mut buf); - assert_eq!(buf, vec![0x07, 0x30, 0x2E, 0x30, 0x30, 0x31, 0x30, 0x35]); + assert_eq!(buf, vec![0x07, b'0', b'.', b'0', b'0', b'1', b'0', b'5']); } #[test] fn test_decode_decimal() { - let buf: Vec = vec![0x05, 0x2D, 0x31, 0x2E, 0x30, 0x35]; + let buf: Vec = vec![0x05, b'-', b'1', b'.', b'0', b'5']; let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), @@ -175,7 +74,7 @@ fn test_decode_decimal() { .unwrap(); assert_eq!(v.to_string(), "-1.05"); - let buf: Vec = vec![0x04, 0x30, 0x2E, 0x30, 0x35]; + let buf: Vec = vec![0x04, b'0', b'.', b'0', b'5']; let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), @@ -183,7 +82,7 @@ fn test_decode_decimal() { .unwrap(); assert_eq!(v.to_string(), "0.05"); - let buf: Vec = vec![0x06, 0x2D, 0x39, 0x30, 0x30, 0x30, 0x30]; + let buf: Vec = vec![0x06, b'-', b'9', b'0', b'0', b'0', b'0']; let v = >::decode(MySqlValue::binary( MySqlTypeInfo::new(TypeId::NEWDECIMAL), buf.as_slice(), From 11663bdf82f6f0ad3e420887a7c023b336858cab Mon Sep 17 00:00:00 2001 From: xiaopengli89 Date: Wed, 8 Apr 2020 17:34:39 +0800 Subject: [PATCH 7/7] Mysql/DECIMAL: add to CI --- .github/workflows/rust.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b18a8ed3e6..07133452de 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -203,56 +203,56 @@ jobs: # ----------------------------------------------------- # integration test: async-std (chrono) - - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid chrono tls' + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid chrono tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # integration test: async-std (time) - - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time tls' + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # integration test: async-std (time + chrono) - - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time chrono tls' + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros uuid time chrono tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # integration test: tokio (chrono) - - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls' + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # integration test: tokio (time) - - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time tls' + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid time tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # integration test: tokio (time + chrono) - - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono time tls' + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros uuid chrono time tls bigdecimal' env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # UI feature gate tests: async-std - - run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls' --test ui-tests + - run: cargo test --no-default-features --features 'runtime-async-std mysql macros tls bigdecimal' --test ui-tests env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly DATABASE_URL: mysql://root:password@localhost:${{ job.services.mysql.ports[3306] }}/sqlx?ssl-mode=VERIFY_CA&ssl-ca=%2Fdata%2Fmysql%2Fca.pem # UI feature gate tests: tokio - - run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls' --test ui-tests + - run: cargo test --no-default-features --features 'runtime-tokio mysql macros tls bigdecimal' --test ui-tests env: # pass the path to the CA that the MySQL service generated # NOTE: Github Actions' YML parser doesn't handle multiline strings correctly