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 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..5cdcc21361 --- /dev/null +++ b/sqlx-core/src/mysql/types/bigdecimal.rs @@ -0,0 +1,92 @@ +use bigdecimal::BigDecimal; + +use crate::decode::Decode; +use crate::encode::Encode; +use crate::io::Buf; +use crate::mysql::protocol::TypeId; +use crate::mysql::{MySql, MySqlData, MySqlTypeInfo, MySqlValue}; +use crate::types::Type; +use crate::Error; +use std::str::FromStr; + +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 <= std::u8::MAX as usize, "Too large size"); + buf.push(size as u8); + let s = self.to_string(); + buf.extend_from_slice(s.as_bytes()); + } + + fn size_hint(&self) -> usize { + let s = self.to_string(); + s.as_bytes().len() + 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 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)?) + } + } + } +} + +#[test] +fn test_encode_decimal() { + let v: BigDecimal = BigDecimal::from_str("-1.05").unwrap(); + let mut buf: Vec = vec![]; + >::encode(&v, &mut buf); + assert_eq!(buf, vec![0x05, b'-', b'1', b'.', b'0', b'5']); + + let v: BigDecimal = BigDecimal::from_str("-105000").unwrap(); + let mut buf: Vec = vec![]; + >::encode(&v, &mut buf); + assert_eq!(buf, vec![0x07, b'-', b'1', b'0', b'5', b'0', b'0', b'0']); + + let v: BigDecimal = BigDecimal::from_str("0.00105").unwrap(); + let mut buf: Vec = vec![]; + >::encode(&v, &mut buf); + 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, b'-', b'1', b'.', b'0', b'5']; + let v = >::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), + buf.as_slice(), + )) + .unwrap(); + assert_eq!(v.to_string(), "-1.05"); + + let buf: Vec = vec![0x04, b'0', b'.', b'0', b'5']; + let v = >::decode(MySqlValue::binary( + MySqlTypeInfo::new(TypeId::NEWDECIMAL), + buf.as_slice(), + )) + .unwrap(); + assert_eq!(v.to_string(), "0.05"); + + 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(), + )) + .unwrap(); + assert_eq!(v.to_string(), "-90000"); +} 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(), 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(), +));