From c7d19151c39600fd827a59223eacc1a48fa09443 Mon Sep 17 00:00:00 2001 From: c5soft Date: Fri, 23 May 2025 21:31:44 +0800 Subject: [PATCH 1/4] add Money support to mssql --- Cargo.lock | 18 +++++++---- sqlx-core/src/mssql/options/parse.rs | 1 + sqlx-core/src/mssql/types/bigdecimal.rs | 41 +++++++++++++++++++------ sqlx-core/src/mssql/types/decimal.rs | 41 +++++++++++++++++++------ sqlx-macros/src/database/mssql.rs | 10 ++++++ 5 files changed, 87 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cf92e9c1b..c1c0eeed21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "addr2line" @@ -115,12 +115,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] @@ -2419,6 +2419,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "oorandom" version = "11.1.5" @@ -3203,9 +3209,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustyline" diff --git a/sqlx-core/src/mssql/options/parse.rs b/sqlx-core/src/mssql/options/parse.rs index b7c73ba08b..6dbf6399bd 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. diff --git a/sqlx-core/src/mssql/types/bigdecimal.rs b/sqlx-core/src/mssql/types/bigdecimal.rs index 4b7a420948..c5e4b9dbb6 100644 --- a/sqlx-core/src/mssql/types/bigdecimal.rs +++ b/sqlx-core/src/mssql/types/bigdecimal.rs @@ -23,7 +23,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,15 +64,32 @@ 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 => { + // Money is stored as an 8-byte integer representing the amount in ten-thousandths of a currency unit + let bytes = value.as_bytes()?; + // println!("bytes: {:?}", bytes); + if bytes.len() != 8 && bytes.len() != 4 { + return Err( + err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).into(), + ); + } + let amount = 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(BigDecimal::new(BigInt::from(amount), 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) } } diff --git a/sqlx-core/src/mssql/types/decimal.rs b/sqlx-core/src/mssql/types/decimal.rs index 6540b59e95..ad0db15a66 100644 --- a/sqlx-core/src/mssql/types/decimal.rs +++ b/sqlx-core/src/mssql/types/decimal.rs @@ -21,7 +21,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,15 +55,32 @@ 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 => { + // Money is stored as an 8-byte integer representing the amount in ten-thousandths of a currency unit + let bytes = value.as_bytes()?; + // println!("bytes: {:?}", bytes); + 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(Decimal::new(amount, 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) } } 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, From a02401e568ce609edc58538bbfa4b805b2358094 Mon Sep 17 00:00:00 2001 From: c5soft Date: Thu, 5 Jun 2025 11:35:26 +0800 Subject: [PATCH 2/4] add Money support to mssql, extract shared code to decimal_tools --- Cargo.lock | 132 +++++++------ sqlx-core/src/mssql/types/bigdecimal.rs | 30 +-- sqlx-core/src/mssql/types/decimal.rs | 27 +-- sqlx-core/src/mssql/types/decimal_tools.rs | 211 +++++++++++++++++++++ sqlx-core/src/mssql/types/mod.rs | 3 + 5 files changed, 290 insertions(+), 113 deletions(-) create mode 100644 sqlx-core/src/mssql/types/decimal_tools.rs diff --git a/Cargo.lock b/Cargo.lock index c1c0eeed21..e11fa1a511 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -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", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" dependencies = [ "async-lock", "cfg-if", @@ -222,7 +222,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 0.38.44", + "rustix 1.0.7", "slab", "tracing", "windows-sys 0.59.0", @@ -253,9 +253,9 @@ dependencies = [ [[package]] name = "async-process" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" dependencies = [ "async-channel 2.3.1", "async-io", @@ -266,15 +266,15 @@ dependencies = [ "cfg-if", "event-listener 5.4.0", "futures-lite", - "rustix 0.38.44", + "rustix 1.0.7", "tracing", ] [[package]] name = "async-signal" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" dependencies = [ "async-io", "async-lock", @@ -282,7 +282,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.44", + "rustix 1.0.7", "signal-hook-registry", "slab", "windows-sys 0.59.0", @@ -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" @@ -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", ] @@ -707,9 +707,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.23" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "jobserver", "libc", @@ -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" @@ -1641,15 +1641,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - -[[package]] -name = "hermit-abi" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -2068,9 +2062,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", "windows-targets 0.53.0", @@ -2095,9 +2089,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.33.0" +version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" +checksum = "91632f3b4fb6bd1d72aa3d78f41ffecfcf2b1a6648d8c241dbe7dbfaf4875e15" dependencies = [ "cc", "openssl-sys", @@ -2137,9 +2131,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -2227,13 +2221,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2396,11 +2390,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.5.1", "libc", ] @@ -2433,9 +2427,9 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags 2.9.1", "cfg-if", @@ -2474,9 +2468,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.108" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -2505,9 +2499,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2515,9 +2509,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -2658,15 +2652,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.7.4" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.4.0", + "hermit-abi 0.5.1", "pin-project-lite", - "rustix 0.38.44", + "rustix 1.0.7", "tracing", "windows-sys 0.59.0", ] @@ -2742,9 +2736,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", "syn 2.0.101", @@ -3466,9 +3460,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -4047,9 +4041,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -4342,11 +4336,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ + "js-sys", "serde", + "wasm-bindgen", ] [[package]] diff --git a/sqlx-core/src/mssql/types/bigdecimal.rs b/sqlx-core/src/mssql/types/bigdecimal.rs index c5e4b9dbb6..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 { @@ -70,35 +72,17 @@ impl Decode<'_, Mssql> for BigDecimal { let scale = value.type_info.0.scale; decode_numeric(value.as_bytes()?, precision, scale) } - DataType::MoneyN | DataType::Money | DataType::SmallMoney => { - // Money is stored as an 8-byte integer representing the amount in ten-thousandths of a currency unit - let bytes = value.as_bytes()?; - // println!("bytes: {:?}", bytes); - if bytes.len() != 8 && bytes.len() != 4 { - return Err( - err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).into(), - ); - } - let amount = 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(BigDecimal::new(BigInt::from(amount), 4)) - } + 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()), } } } 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 ad0db15a66..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 { @@ -62,22 +64,7 @@ impl Decode<'_, Mssql> for Decimal { decode_numeric(value.as_bytes()?, precision, scale) } DataType::MoneyN | DataType::Money | DataType::SmallMoney => { - // Money is stored as an 8-byte integer representing the amount in ten-thousandths of a currency unit - let bytes = value.as_bytes()?; - // println!("bytes: {:?}", bytes); - 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(Decimal::new(amount, 4)) + Ok(Decimal::new(decode_money_bytes(value.as_bytes()?)?, 4)) } _ => Err(err_protocol!("expected numeric type, got {:?}", value.type_info.0).into()), } @@ -85,11 +72,7 @@ impl Decode<'_, Mssql> for Decimal { } 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..4acca638ed --- /dev/null +++ b/sqlx-core/src/mssql/types/decimal_tools.rs @@ -0,0 +1,211 @@ +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; + + fn err_of(msg: &str) -> String { + format!("encountered unexpected or invalid data: {}", msg) + } + + // ========== 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_of("expected 8/4 bytes for Money, got 0") + ); + } + + #[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_of("expected 8/4 bytes for Money, got 3") + ); + } + + #[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); + } + + // ========== 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_of("numeric bytes cannot be empty") + ); + } + + #[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_of("invalid sign byte: 0x02") + ); + } + + #[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_of("numeric value exceeds 16 bytes") + ); + } + + #[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; From 296c027e981376e5f7605886a8480e94ba346087 Mon Sep 17 00:00:00 2001 From: c5soft Date: Thu, 5 Jun 2025 21:17:44 +0800 Subject: [PATCH 3/4] add Money support to mssql, extract shared code to decimal_tools and write test code --- Cargo.toml | 8 +++++--- sqlx-core/src/mssql/options/parse.rs | 4 ++-- sqlx-core/src/mssql/types/decimal_tools.rs | 18 +++++++----------- sqlx-macros/src/lib.rs | 7 ++++++- tests/mssql/types.rs | 11 +++++++---- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 76aa03cb89..497cbdac08 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 ", @@ -126,7 +126,7 @@ bstr = ["sqlx-core/bstr"] git2 = ["sqlx-core/git2"] [dependencies] -sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.42", path = "sqlx-core", default-features = false } +sqlx-core = { package = "sqlx-core-oldapi", version = "0.6.42", path = "sqlx-core", default-features = false } sqlx-macros = { package = "sqlx-macros-oldapi", version = "0.6.42", path = "sqlx-macros", default-features = false, optional = true } [dev-dependencies] @@ -149,7 +149,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 6dbf6399bd..dd5e6549c3 100644 --- a/sqlx-core/src/mssql/options/parse.rs +++ b/sqlx-core/src/mssql/options/parse.rs @@ -143,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); @@ -151,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/decimal_tools.rs b/sqlx-core/src/mssql/types/decimal_tools.rs index 4acca638ed..720ee89031 100644 --- a/sqlx-core/src/mssql/types/decimal_tools.rs +++ b/sqlx-core/src/mssql/types/decimal_tools.rs @@ -41,11 +41,7 @@ mod tests { use super::*; use std::error::Error; - fn err_of(msg: &str) -> String { - format!("encountered unexpected or invalid data: {}", msg) - } - - // ========== decode_money_bytes ========== + // ========== test decode_money_bytes ========== #[test] fn test_decode_money_bytes_empty() { @@ -54,7 +50,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - err_of("expected 8/4 bytes for Money, got 0") + err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).to_string() ); } @@ -65,7 +61,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - err_of("expected 8/4 bytes for Money, got 3") + err_protocol!("expected 8/4 bytes for Money, got {}", bytes.len()).to_string() ); } @@ -118,7 +114,7 @@ mod tests { assert_eq!(amount, 0); } - // ========== decode_numeric_bytes ========== + // ========== test decode_numeric_bytes ========== #[test] fn test_decode_numeric_bytes_empty() { @@ -127,7 +123,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - err_of("numeric bytes cannot be empty") + err_protocol!("numeric bytes cannot be empty").to_string() ); } @@ -138,7 +134,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - err_of("invalid sign byte: 0x02") + err_protocol!("invalid sign byte: 0x02").to_string() ); } @@ -149,7 +145,7 @@ mod tests { assert!(result.is_err()); assert_eq!( result.unwrap_err().to_string(), - err_of("numeric value exceeds 16 bytes") + err_protocol!("numeric value exceeds 16 bytes").to_string() ); } 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 a3cec18c44..1d9cfda97b 100644 --- a/tests/mssql/types.rs +++ b/tests/mssql/types.rs @@ -67,10 +67,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, @@ -184,6 +183,8 @@ 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(), )); } @@ -204,6 +205,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(), )); } From 6d2218085d9593606e03739724941ff5cc475a72 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Thu, 5 Jun 2025 23:18:43 +0200 Subject: [PATCH 4/4] add additional tests for MONEY and SMALLMONEY type casting in MSSQL --- tests/mssql/types.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/mssql/types.rs b/tests/mssql/types.rs index 6c2397ad8a..de787f94b6 100644 --- a/tests/mssql/types.rs +++ b/tests/mssql/types.rs @@ -217,6 +217,15 @@ mod decimal { == 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(), )); }