Skip to content

Add Money type support to mssql #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <leckey.ryan@gmail.com>",
Expand Down Expand Up @@ -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]
Expand Down
5 changes: 3 additions & 2 deletions sqlx-core/src/mssql/options/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -142,15 +143,15 @@ 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);
}

#[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);
Expand Down
35 changes: 21 additions & 14 deletions sqlx-core/src/mssql/types/bigdecimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mssql> for BigDecimal {
fn type_info() -> MssqlTypeInfo {
MssqlTypeInfo(TypeInfo {
Expand All @@ -23,7 +25,13 @@ impl Type<Mssql> 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
)
}
}
Expand Down Expand Up @@ -58,24 +66,23 @@ impl Encode<'_, Mssql> for BigDecimal {
impl Decode<'_, Mssql> for BigDecimal {
fn decode(value: MssqlValueRef<'_>) -> Result<Self, BoxDynError> {
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<BigDecimal, BoxDynError> {
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)))
}
36 changes: 21 additions & 15 deletions sqlx-core/src/mssql/types/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mssql> for Decimal {
fn type_info() -> MssqlTypeInfo {
MssqlTypeInfo(TypeInfo {
Expand All @@ -21,7 +23,13 @@ impl Type<Mssql> 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
)
}
}
Expand Down Expand Up @@ -49,24 +57,22 @@ impl Encode<'_, Mssql> for Decimal {
impl Decode<'_, Mssql> for Decimal {
fn decode(value: MssqlValueRef<'_>) -> Result<Self, BoxDynError> {
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<Decimal, BoxDynError> {
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)))
}
Loading