diff --git a/Cargo.lock b/Cargo.lock index a066327cc5..05127bea2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -91,33 +91,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -491,9 +491,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bigdecimal" @@ -630,9 +630,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "c1b094a32014c3d1f3944e4808e0e7c70e97dae0660886a8eb6dbc52d745badc" [[package]] name = "bytecheck" @@ -670,9 +670,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -839,9 +839,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "concurrent-queue" diff --git a/Cargo.toml b/Cargo.toml index 26ade0ad88..31bcb849bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ repository = "https://github.com/lovasoa/sqlx" documentation = "https://docs.rs/sqlx" description = "🧰 The Rust SQL Toolkit. An async, pure Rust SQL crate featuring compile-time checked queries without a DSL. Supports PostgreSQL, MySQL, and SQLite." edition = "2021" -keywords = ["database", "async", "postgres", "mysql", "sqlite"] +keywords = ["database", "async", "postgres", "mysql", "sqlite", "mssql"] categories = ["database", "asynchronous"] authors = [ "Ryan Leckey ", @@ -166,7 +166,9 @@ rand_xoshiro = "0.7.0" hex = "0.4.3" tempdir = "0.3.7" # Needed to test SQLCipher -libsqlite3-sys = { version = "0", features = ["bundled-sqlcipher-vendored-openssl"] } +libsqlite3-sys = { version = "0", features = [ + "bundled-sqlcipher-vendored-openssl", +] } [lints.rust] diff --git a/sqlx-core/src/mssql/options/parse.rs b/sqlx-core/src/mssql/options/parse.rs index b7c73ba08b..dd5e6549c3 100644 --- a/sqlx-core/src/mssql/options/parse.rs +++ b/sqlx-core/src/mssql/options/parse.rs @@ -28,6 +28,7 @@ impl FromStr for MssqlConnectOptions { /// - `strict`: Requires encryption and validates the server certificate. /// - `mandatory` or `true` or `yes`: Requires encryption but doesn't validate the server certificate. /// - `optional` or `false` or `no`: Uses encryption if available, falls back to unencrypted. + /// - `not_supported`: No encryption. /// - `sslrootcert` or `ssl-root-cert` or `ssl-ca`: Path to the root certificate for validating the server's SSL certificate. /// - `trust_server_certificate`: When true, skips validation of the server's SSL certificate. Use with caution as it makes the connection vulnerable to man-in-the-middle attacks. /// - `hostname_in_certificate`: The hostname expected in the server's SSL certificate. Use this when the server's hostname doesn't match the certificate. @@ -142,7 +143,7 @@ impl std::error::Error for MssqlInvalidOption {} #[test] fn it_parses_username_with_at_sign_correctly() { - let url = "mysql://user@hostname:password@hostname:5432/database"; + let url = "mssql://user@hostname:password@hostname:5432/database"; let opts = MssqlConnectOptions::from_str(url).unwrap(); assert_eq!("user@hostname", &opts.username); @@ -150,7 +151,7 @@ fn it_parses_username_with_at_sign_correctly() { #[test] fn it_parses_password_with_non_ascii_chars_correctly() { - let url = "mysql://username:p@ssw0rd@hostname:5432/database"; + let url = "mssql://username:p@ssw0rd@hostname:5432/database"; let opts = MssqlConnectOptions::from_str(url).unwrap(); assert_eq!(Some("p@ssw0rd".into()), opts.password); diff --git a/sqlx-core/src/mssql/types/bigdecimal.rs b/sqlx-core/src/mssql/types/bigdecimal.rs index 4b7a420948..5978456537 100644 --- a/sqlx-core/src/mssql/types/bigdecimal.rs +++ b/sqlx-core/src/mssql/types/bigdecimal.rs @@ -9,6 +9,8 @@ use crate::mssql::protocol::type_info::{DataType, TypeInfo}; use crate::mssql::{Mssql, MssqlTypeInfo, MssqlValueRef}; use crate::types::Type; +use super::decimal_tools::{decode_money_bytes, decode_numeric_bytes}; + impl Type for BigDecimal { fn type_info() -> MssqlTypeInfo { MssqlTypeInfo(TypeInfo { @@ -23,7 +25,13 @@ impl Type for BigDecimal { fn compatible(ty: &MssqlTypeInfo) -> bool { matches!( ty.0.ty, - DataType::Numeric | DataType::NumericN | DataType::Decimal | DataType::DecimalN + DataType::Numeric + | DataType::NumericN + | DataType::Decimal + | DataType::DecimalN + | DataType::MoneyN + | DataType::Money + | DataType::SmallMoney ) } } @@ -58,24 +66,23 @@ impl Encode<'_, Mssql> for BigDecimal { impl Decode<'_, Mssql> for BigDecimal { fn decode(value: MssqlValueRef<'_>) -> Result { let ty = value.type_info.0.ty; - if !matches!( - ty, - DataType::Decimal | DataType::DecimalN | DataType::Numeric | DataType::NumericN - ) { - return Err(err_protocol!("expected numeric type, got {:?}", value.type_info.0).into()); + match ty { + DataType::Decimal | DataType::DecimalN | DataType::Numeric | DataType::NumericN => { + let precision = value.type_info.0.precision; + let scale = value.type_info.0.scale; + decode_numeric(value.as_bytes()?, precision, scale) + } + DataType::MoneyN | DataType::Money | DataType::SmallMoney => Ok(BigDecimal::new( + BigInt::from(decode_money_bytes(value.as_bytes()?)?), + 4, + )), + _ => Err(err_protocol!("expected numeric type, got {:?}", value.type_info.0).into()), } - let precision = value.type_info.0.precision; - let scale = value.type_info.0.scale; - decode_numeric(value.as_bytes()?, precision, scale) } } fn decode_numeric(bytes: &[u8], _precision: u8, scale: u8) -> Result { - let sign = if bytes[0] == 0 { -1 } else { 1 }; - let rest = &bytes[1..]; - let mut fixed_bytes = [0u8; 16]; - fixed_bytes[0..rest.len()].copy_from_slice(rest); - let numerator = u128::from_le_bytes(fixed_bytes); + let (sign, numerator) = decode_numeric_bytes(bytes)?; let small_num = sign * BigInt::from(numerator); Ok(BigDecimal::new(small_num, i64::from(scale))) } diff --git a/sqlx-core/src/mssql/types/decimal.rs b/sqlx-core/src/mssql/types/decimal.rs index 6540b59e95..ffe6a7057b 100644 --- a/sqlx-core/src/mssql/types/decimal.rs +++ b/sqlx-core/src/mssql/types/decimal.rs @@ -7,6 +7,8 @@ use crate::mssql::protocol::type_info::{DataType, TypeInfo}; use crate::mssql::{Mssql, MssqlTypeInfo, MssqlValueRef}; use crate::types::Type; +use super::decimal_tools::{decode_money_bytes, decode_numeric_bytes}; + impl Type for Decimal { fn type_info() -> MssqlTypeInfo { MssqlTypeInfo(TypeInfo { @@ -21,7 +23,13 @@ impl Type for Decimal { fn compatible(ty: &MssqlTypeInfo) -> bool { matches!( ty.0.ty, - DataType::Numeric | DataType::NumericN | DataType::Decimal | DataType::DecimalN + DataType::Numeric + | DataType::NumericN + | DataType::Decimal + | DataType::DecimalN + | DataType::MoneyN + | DataType::Money + | DataType::SmallMoney ) } } @@ -49,24 +57,22 @@ impl Encode<'_, Mssql> for Decimal { impl Decode<'_, Mssql> for Decimal { fn decode(value: MssqlValueRef<'_>) -> Result { let ty = value.type_info.0.ty; - if !matches!( - ty, - DataType::Decimal | DataType::DecimalN | DataType::Numeric | DataType::NumericN - ) { - return Err(err_protocol!("expected numeric type, got {:?}", value.type_info.0).into()); + match ty { + DataType::Decimal | DataType::DecimalN | DataType::Numeric | DataType::NumericN => { + let precision = value.type_info.0.precision; + let scale = value.type_info.0.scale; + decode_numeric(value.as_bytes()?, precision, scale) + } + DataType::MoneyN | DataType::Money | DataType::SmallMoney => { + Ok(Decimal::new(decode_money_bytes(value.as_bytes()?)?, 4)) + } + _ => Err(err_protocol!("expected numeric type, got {:?}", value.type_info.0).into()), } - let precision = value.type_info.0.precision; - let scale = value.type_info.0.scale; - decode_numeric(value.as_bytes()?, precision, scale) } } fn decode_numeric(bytes: &[u8], _precision: u8, scale: u8) -> Result { - let sign = if bytes[0] == 0 { -1 } else { 1 }; - let rest = &bytes[1..]; - let mut fixed_bytes = [0u8; 16]; - fixed_bytes[0..rest.len()].copy_from_slice(rest); - let numerator = u128::from_le_bytes(fixed_bytes); - let small_num = sign * i64::try_from(numerator)?; + let (sign, numerator) = decode_numeric_bytes(bytes)?; + let small_num: i64 = sign as i64 * i64::try_from(numerator)?; Ok(Decimal::new(small_num, u32::from(scale))) } diff --git a/sqlx-core/src/mssql/types/decimal_tools.rs b/sqlx-core/src/mssql/types/decimal_tools.rs new file mode 100644 index 0000000000..720ee89031 --- /dev/null +++ b/sqlx-core/src/mssql/types/decimal_tools.rs @@ -0,0 +1,207 @@ +use crate::error::BoxDynError; + +pub(crate) fn decode_money_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 8 && bytes.len() != 4 { + return Err(err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).into()); + } + let amount: i64 = if bytes.len() == 8 { + let amount_h = i32::from_le_bytes(bytes[0..4].try_into()?) as i64; + let amount_l = u32::from_le_bytes(bytes[4..8].try_into()?) as i64; + (amount_h << 32) | amount_l + } else { + i32::from_le_bytes(bytes.try_into()?) as i64 + }; + Ok(amount) +} +pub(crate) fn decode_numeric_bytes(bytes: &[u8]) -> Result<(i8, u128), BoxDynError> { + if bytes.is_empty() { + return Err(err_protocol!("numeric bytes cannot be empty").into()); + } + + let sign = match bytes[0] { + 0 => -1, + 1 => 1, + other => return Err(err_protocol!("invalid sign byte: 0x{:02x}", other).into()), + }; + + let rest = &bytes[1..]; + if rest.len() > 16 { + return Err(err_protocol!("numeric value exceeds 16 bytes").into()); + } + + let mut fixed_bytes = [0u8; 16]; + fixed_bytes[..rest.len()].copy_from_slice(rest); + let amount = u128::from_le_bytes(fixed_bytes); + + Ok((sign, amount)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::error::Error; + + // ========== test decode_money_bytes ========== + + #[test] + fn test_decode_money_bytes_empty() { + let bytes: &[u8] = &[]; + let result = decode_money_bytes(bytes); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).to_string() + ); + } + + #[test] + fn test_decode_money_bytes_invalid_length() { + let bytes = [0x01, 0x02, 0x03]; + let result = decode_money_bytes(&bytes); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).to_string() + ); + } + + #[test] + fn test_decode_money_bytes_4bytes_positive() { + let bytes = [0xd2, 0xe8, 0x95, 0x49]; // 1234561234 + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, 1234561234); + } + + #[test] + fn test_decode_money_bytes_4bytes_negative() { + let bytes = [0x2e, 0x17, 0x6a, 0xb6]; // -1234561234 + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, -1234561234); + } + + #[test] + fn test_decode_money_bytes_8bytes_positive() { + let bytes = [0x1f, 0x01, 0x00, 0x00, 0x22, 0x09, 0xfb, 0x71]; // 1234567891234 + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, 1234567891234); + } + + #[test] + fn test_decode_money_bytes_8bytes_negative() { + let bytes = [0xe0, 0xfe, 0xff, 0xff, 0xde, 0xf6, 0x04, 0x8e]; // -1234567891234 + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, -1234567891234); + } + + #[test] + fn test_decode_money_bytes_max_i64() { + let bytes = [0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0xff]; + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, i64::MAX); + } + + #[test] + fn test_decode_money_bytes_min_i64() { + let bytes = [0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00]; + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, i64::MIN); + } + + #[test] + fn test_decode_money_bytes_all_zero() { + let bytes = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let amount = decode_money_bytes(&bytes).unwrap(); + assert_eq!(amount, 0); + } + + // ========== test decode_numeric_bytes ========== + + #[test] + fn test_decode_numeric_bytes_empty() { + let bytes: &[u8] = &[]; + let result = decode_numeric_bytes(bytes); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + err_protocol!("numeric bytes cannot be empty").to_string() + ); + } + + #[test] + fn test_decode_numeric_bytes_invalid_sign() { + let bytes = [0x02, 0x01, 0x02]; + let result = decode_numeric_bytes(&bytes); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + err_protocol!("invalid sign byte: 0x02").to_string() + ); + } + + #[test] + fn test_decode_numeric_bytes_overflow() { + let bytes = vec![0x01; 18]; // 1 sign + 17 data + let result = decode_numeric_bytes(&bytes); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + err_protocol!("numeric value exceeds 16 bytes").to_string() + ); + } + + #[test] + fn test_decode_numeric_bytes_positive() { + let bytes = [0x01, 0xb9, 0x4a, 0x06, 0x00]; // +412345 + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, 1); + assert_eq!(amount, 412345); + } + + #[test] + fn test_decode_numeric_bytes_negative() { + let bytes = [0x00, 0x48, 0x91, 0x0f, 0x86, 0x48, 0x70, 0x00, 0x00]; // -123456789123400 + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, -1); + assert_eq!(amount, 123456789123400); + } + + #[test] + fn test_decode_numeric_bytes_positive_zero() { + let bytes = [0x01, 0x00, 0x00, 0x00, 0x00]; + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, 1); + assert_eq!(amount, 0); + } + + #[test] + fn test_decode_numeric_bytes_negative_zero() { + let bytes = [0x00, 0x00, 0x00, 0x00, 0x00]; + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, -1); + assert_eq!(amount, 0); + } + + #[test] + fn test_decode_numeric_bytes_max_u128() { + let bytes = [0x01] // sign + .iter() + .chain([0xff; 16].iter()) // 16 bytes of 0xff + .cloned() + .collect::>(); + + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, 1); + assert_eq!(amount, u128::MAX); + } + + #[test] + fn test_decode_numeric_bytes_16bytes_value() { + let bytes = [ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, + ]; + let (sign, amount) = decode_numeric_bytes(&bytes).unwrap(); + assert_eq!(sign, 1); + assert_eq!(amount, 1 << 120); + } +} diff --git a/sqlx-core/src/mssql/types/mod.rs b/sqlx-core/src/mssql/types/mod.rs index 1636f632ad..b5df1128a8 100644 --- a/sqlx-core/src/mssql/types/mod.rs +++ b/sqlx-core/src/mssql/types/mod.rs @@ -9,6 +9,9 @@ mod int; mod str; mod uint; +#[cfg(any(feature = "decimal", feature = "bigdecimal"))] +mod decimal_tools; + #[cfg(feature = "chrono")] mod chrono; diff --git a/sqlx-macros/src/database/mssql.rs b/sqlx-macros/src/database/mssql.rs index 70aaeeed79..f4503f10ae 100644 --- a/sqlx-macros/src/database/mssql.rs +++ b/sqlx-macros/src/database/mssql.rs @@ -25,6 +25,16 @@ impl_database_ext! { #[cfg(feature = "chrono")] sqlx::types::chrono::DateTime, + + #[cfg(feature = "bigdecimal")] + sqlx::types::BigDecimal, + + #[cfg(feature = "decimal")] + sqlx::types::Decimal, + + #[cfg(feature = "json")] + sqlx::types::JsonValue, + }, ParamChecking::Weak, feature-types: _info => None, diff --git a/sqlx-macros/src/lib.rs b/sqlx-macros/src/lib.rs index 67911c50ac..c157600b46 100644 --- a/sqlx-macros/src/lib.rs +++ b/sqlx-macros/src/lib.rs @@ -1,6 +1,11 @@ #![allow(clippy::large_enum_variant)] #![cfg_attr( - not(any(feature = "postgres", feature = "mysql", feature = "offline")), + not(any( + feature = "postgres", + feature = "mysql", + feature = "mssql", + feature = "offline" + )), allow(dead_code, unused_macros, unused_imports) )] #![cfg_attr( diff --git a/tests/mssql/types.rs b/tests/mssql/types.rs index e3b48e7283..de787f94b6 100644 --- a/tests/mssql/types.rs +++ b/tests/mssql/types.rs @@ -99,10 +99,9 @@ test_type!(numeric(Mssql, "CAST(0.0000000000000001 AS NUMERIC(38,16))" == 0.0000000000000001_f64, "CAST(939399419.1225182 AS NUMERIC(15,2))" == 939399419.12_f64, "CAST(939399419.1225182 AS DECIMAL(15,2))" == 939399419.12_f64, - "CAST(123456789.0123456789 AS NUMERIC(38,10))" == 123_456_789.012_345_67_f64, - "CAST(123456789.0123456789012 AS NUMERIC(38,13))" == 123_456_789.012_345_67_f64, - "CAST(123456789.012345678901234 AS NUMERIC(38,15))" == 123_456_789.012_345_67_f64, - + "CAST(123456789.0123456789 AS NUMERIC(38,10))" == 123_456_789.012_345_678_9_f64, + "CAST(123456789.0123456789012 AS NUMERIC(38,13))" == 123_456_789.012_345_678_901_2_f64, + "CAST(123456789.012345678901234 AS NUMERIC(38,15))" == 123_456_789.012_345_678_901_234_f64, "CAST(1.0000000000000001 AS NUMERIC(18,16))" == 1.0000000000000001_f64, "CAST(0.99999999999999 AS NUMERIC(18,14))" == 0.99999999999999_f64, "CAST(2.00000000000001 AS NUMERIC(18,14))" == 2.00000000000001_f64, @@ -216,6 +215,17 @@ mod decimal { == Decimal::from_str_exact("0.01234567890123456789").unwrap(), "CAST('-12345678901234' AS DECIMAL(28,5))" == Decimal::from_str_exact("-12345678901234").unwrap(), + "CAST('-1234567890.1234' AS MONEY)" == Decimal::from_str_exact("-1234567890.1234").unwrap(), + "CAST('-123456.1234' AS SMALLMONEY)" == Decimal::from_str_exact("-123456.1234").unwrap(), + "CAST('922337203685477.5807' AS MONEY)" == Decimal::from_str_exact("922337203685477.5807").unwrap(), + "CAST('-922337203685477.5808' AS MONEY)" == Decimal::from_str_exact("-922337203685477.5808").unwrap(), + "CAST('214748.3647' AS SMALLMONEY)" == Decimal::from_str_exact("214748.3647").unwrap(), + "CAST('-214748.3648' AS SMALLMONEY)" == Decimal::from_str_exact("-214748.3648").unwrap(), + "CAST('0.0001' AS MONEY)" == Decimal::from_str_exact("0.0001").unwrap(), + "CAST('-0.0001' AS MONEY)" == Decimal::from_str_exact("-0.0001").unwrap(), + "CAST('0.0000' AS MONEY)" == Decimal::from_str_exact("0.0000").unwrap(), + "CAST('999999999999999.9999' AS MONEY)" == Decimal::from_str_exact("999999999999999.9999").unwrap(), + "CAST('-999999999999999.9999' AS MONEY)" == Decimal::from_str_exact("-999999999999999.9999").unwrap(), )); } @@ -236,6 +246,8 @@ mod bigdecimal { == BigDecimal::from_str("-12345678901234567890").unwrap(), "CAST('-12345678901234567890.012345678901234' AS DECIMAL(38,15))" == BigDecimal::from_str("-12345678901234567890.012345678901234").unwrap(), + "CAST('-1234567890.1234' AS MONEY)" == BigDecimal::from_str("-1234567890.1234").unwrap(), + "CAST('-123456.1234' AS SMALLMONEY)" == BigDecimal::from_str("-123456.1234").unwrap(), )); }