diff --git a/sqlx-core/src/mysql/protocol/row.rs b/sqlx-core/src/mysql/protocol/row.rs index f3b82d40..2fdd0fa8 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 c839933a..70f64789 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 00000000..0215d5b9 --- /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 d2fac66d..11d31ab7 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 1593f111..dee86a2c 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(),