fix(sqlserver): preserve real value precision in JSON

Closes #9984
This commit is contained in:
t8y2 committed 2026-09-23 09:22:33 +08:00
1 parent 01c3d6f517
commit 2c0d2bfb63
2 files changed
+245 -3

No files matched your search

+100 -3
View File
@@ -1972,9 +1972,7 @@ fn sqlserver_cell_to_json(cell: &ColumnData<'static>) -> serde_json::Value {
return super::safe_i64_to_json(v);
}
if let Ok(Some(v)) = <f32 as FromSql>::from_sql(cell) {
return serde_json::Number::from_f64(v as f64)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null);
return serde_json::Value::from(v);
}
if let Ok(Some(v)) = <f64 as FromSql>::from_sql(cell) {
return serde_json::Number::from_f64(v).map(serde_json::Value::Number).unwrap_or(serde_json::Value::Null);
@@ -5302,6 +5300,105 @@ mod tests {
assert_eq!(sqlserver_cell_to_json(&ColumnData::U8(Some(7))), serde_json::json!(7));
}
#[test]
fn sqlserver_real_cells_use_shortest_round_trip_numbers() {
for (value, expected) in [
(18.2_f32, "18.2"),
(18.3, "18.3"),
(59.3, "59.3"),
(4.6, "4.6"),
(-18.2, "-18.2"),
(-59.3, "-59.3"),
(18.5, "18.5"),
(1.2345678, "1.2345678"),
(0.0, "0.0"),
(-0.0, "-0.0"),
(1.0e-20, "1e-20"),
(1.0e20, "1e+20"),
] {
let converted = sqlserver_cell_to_json(&ColumnData::F32(Some(value)));
assert!(converted.is_number(), "{value}: {converted:?}");
assert_eq!(converted.to_string(), expected, "{value}");
assert_eq!(serde_json::from_value::<f32>(converted).unwrap().to_bits(), value.to_bits());
}
}
#[test]
fn sqlserver_real_cells_preserve_extremes_and_subnormals() {
for value in [
f32::MIN,
f32::MAX,
f32::MIN_POSITIVE,
-f32::MIN_POSITIVE,
f32::from_bits(1),
-f32::from_bits(1),
f32::from_bits(0x007f_ffff),
f32::from_bits(0x3f80_0001),
] {
let converted = sqlserver_cell_to_json(&ColumnData::F32(Some(value)));
assert!(converted.is_number(), "{value}: {converted:?}");
let serialized = converted.to_string();
assert_eq!(serialized.parse::<f32>().unwrap().to_bits(), value.to_bits());
assert_eq!((converted.as_f64().unwrap() as f32).to_bits(), value.to_bits());
assert_eq!(serde_json::from_str::<f32>(&serialized).unwrap().to_bits(), value.to_bits());
}
}
#[test]
fn sqlserver_real_and_float_null_and_non_finite_cells_remain_null() {
assert_eq!(sqlserver_cell_to_json(&ColumnData::F32(None)), serde_json::Value::Null);
assert_eq!(sqlserver_cell_to_json(&ColumnData::F64(None)), serde_json::Value::Null);
for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(sqlserver_cell_to_json(&ColumnData::F32(Some(value))), serde_json::Value::Null);
}
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert_eq!(sqlserver_cell_to_json(&ColumnData::F64(Some(value))), serde_json::Value::Null);
}
}
#[test]
fn sqlserver_float_cells_keep_double_precision() {
for value in [
18.200000762939453_f64,
18.299999237060547,
59.29999923706055,
4.599999904632568,
1.2345678901234567,
-1.2345678901234567,
0.0,
-0.0,
f64::MIN_POSITIVE,
f64::MAX,
f64::from_bits(1),
] {
let converted = sqlserver_cell_to_json(&ColumnData::F64(Some(value)));
assert_eq!(converted, serde_json::json!(value));
assert_eq!(converted.as_f64().unwrap().to_bits(), value.to_bits());
}
}
#[test]
fn sqlserver_real_conversion_keeps_other_cell_types_unchanged() {
for (cell, expected) in [
(ColumnData::I16(Some(-18)), serde_json::json!(-18)),
(ColumnData::I32(Some(59)), serde_json::json!(59)),
(ColumnData::I64(Some(9_007_199_254_740_991)), serde_json::json!(9_007_199_254_740_991_i64)),
(ColumnData::I64(Some(i64::MAX)), serde_json::json!(i64::MAX.to_string())),
(ColumnData::I32(None), serde_json::Value::Null),
(ColumnData::String(Some(Cow::Borrowed("18.200000762939453"))), serde_json::json!("18.200000762939453")),
(ColumnData::String(None), serde_json::Value::Null),
(ColumnData::Bit(Some(true)), serde_json::json!(true)),
(ColumnData::Bit(None), serde_json::Value::Null),
(
ColumnData::Numeric(Some(tiberius::numeric::Numeric::new_with_scale(18200, 3))),
serde_json::json!("18.200"),
),
(ColumnData::Numeric(None), serde_json::Value::Null),
] {
assert_eq!(sqlserver_cell_to_json(&cell), expected, "{cell:?}");
}
}
#[test]
fn sqlserver_numeric_cells_with_scale_over_28_do_not_panic() {
// NUMERIC(38, 29) with data used to abort the app: rust_decimal caps scale at 28 (issue #3648).
@@ -0,0 +1,145 @@
use dbx_drivers::db::sqlserver::{self, SqlServerStreamItem};
use dbx_drivers::models::connection::DatabaseType;
use dbx_sql::data_grid_sql::format_grid_sql_literal;
use serde_json::Value;
use std::time::Duration;
#[tokio::test]
#[ignore = "requires DBX_LIVE_SQLSERVER_HOST/PORT/USER/PASSWORD for a remote SQL Server"]
async fn live_sqlserver_issue_9984_real_round_trip() {
let host = std::env::var("DBX_LIVE_SQLSERVER_HOST").expect("DBX_LIVE_SQLSERVER_HOST");
let port = std::env::var("DBX_LIVE_SQLSERVER_PORT").expect("DBX_LIVE_SQLSERVER_PORT").parse().unwrap();
let user = std::env::var("DBX_LIVE_SQLSERVER_USER").expect("DBX_LIVE_SQLSERVER_USER");
let password = std::env::var("DBX_LIVE_SQLSERVER_PASSWORD").expect("DBX_LIVE_SQLSERVER_PASSWORD");
let database = std::env::var("DBX_LIVE_SQLSERVER_DATABASE").unwrap_or_else(|_| "tempdb".to_string());
let mut client = sqlserver::connect_with_port_explicit(
&host,
port,
true,
&user,
&password,
Some(&database),
Duration::from_secs(15),
)
.await
.expect("connect to remote SQL Server");
let version = sqlserver::execute_query(&mut client, "SELECT @@VERSION AS server_version").await.unwrap();
println!("Server version: {}", version.rows[0][0]);
client
.simple_query("CREATE TABLE #dbx_issue_9984_real (id int NOT NULL, WDZ real, SDZ float(24))")
.await
.unwrap()
.into_results()
.await
.unwrap();
let values = [
("18.2", 18.2_f32),
("18.3", 18.3),
("59.3", 59.3),
("4.6", 4.6),
("-18.2", -18.2),
("-59.3", -59.3),
("18.5", 18.5),
("0", 0.0),
("1e-20", 1.0e-20),
("1e20", 1.0e20),
("1.2345678", 1.2345678),
];
for (index, (literal, _)) in values.iter().enumerate() {
client
.execute(format!("INSERT INTO #dbx_issue_9984_real VALUES ({index}, {literal}, {literal})"), &[])
.await
.unwrap();
}
client.execute("INSERT INTO #dbx_issue_9984_real VALUES (99, NULL, NULL)", &[]).await.unwrap();
let raw_sql = "SELECT id, WDZ, SDZ FROM #dbx_issue_9984_real ORDER BY id";
let raw = client.query(raw_sql, &[]).await.unwrap().into_first_result().await.unwrap();
assert_eq!(raw.len(), values.len() + 1);
for (row, (_, expected)) in raw.iter().zip(&values) {
assert_eq!(row.get::<f32, _>(1).unwrap().to_bits(), expected.to_bits());
assert_eq!(row.get::<f32, _>(2).unwrap().to_bits(), expected.to_bits());
}
println!(
"Raw f32 widened to f64: {:?}",
raw.iter().take(4).map(|row| f64::from(row.get::<f32, _>(1).unwrap())).collect::<Vec<_>>()
);
let select = "SELECT id, WDZ, SDZ, \
CAST(18.200000762939453 AS float(53)) AS double_value, \
CAST(1.2345678901234567 AS float) AS default_float, \
CAST(18.200 AS decimal(10, 3)) AS decimal_value, \
CAST(1234567890123456789012345678.9012345678 AS numeric(38, 10)) AS numeric_value, \
CAST(7 AS tinyint) AS tiny_value, CAST(9223372036854775807 AS bigint) AS big_value, \
CAST('18.200000762939453' AS nvarchar(30)) AS text_value, \
CAST(NULL AS float) AS null_float \
FROM #dbx_issue_9984_real ORDER BY id";
let result = sqlserver::execute_query(&mut client, select).await.unwrap();
assert_eq!(result.rows.len(), values.len() + 1);
assert_eq!(&result.column_types[1..3], &["float4", "float4"]);
for (row, (_, expected)) in result.rows.iter().zip(&values) {
for converted in &row[1..3] {
assert!(converted.is_number());
assert_eq!(converted.to_string(), serde_json::to_string(expected).unwrap());
assert_eq!((converted.as_f64().unwrap() as f32).to_bits(), expected.to_bits());
}
assert_eq!(row[3], serde_json::json!(18.200000762939453_f64));
assert_eq!(row[4], serde_json::json!(1.2345678901234567_f64));
assert_eq!(row[5], serde_json::json!("18.200"));
assert_eq!(row[6], serde_json::json!("1234567890123456789012345678.9012345678"));
assert_eq!(row[7], serde_json::json!(7));
assert_eq!(row[8], serde_json::json!("9223372036854775807"));
assert_eq!(row[9], serde_json::json!("18.200000762939453"));
assert_eq!(row[10], Value::Null);
}
assert_eq!(&result.rows.last().unwrap()[1..3], &[Value::Null, Value::Null]);
println!("Converted reported rows: {}", serde_json::to_string(&result.rows[..4]).unwrap());
let batches = sqlserver::execute_batch(&mut client, &format!("{select}; {select}")).await.unwrap();
let row_sets: Vec<_> = batches.iter().filter(|batch| !batch.columns.is_empty()).collect();
assert_eq!(row_sets.len(), 2);
for batch in row_sets {
assert_eq!(batch.rows, result.rows);
}
let simple_batches = sqlserver::execute_simple_batch_with_max_rows(&mut client, select, None).await.unwrap();
assert_eq!(simple_batches.iter().find(|batch| !batch.columns.is_empty()).unwrap().rows, result.rows);
let mut streamed_rows = Vec::new();
let summary = sqlserver::stream_first_result_set(&mut client, select, None, None, |item| {
match item {
SqlServerStreamItem::Columns { columns, column_types } => {
assert_eq!(columns, result.columns);
assert_eq!(column_types, result.column_types);
}
SqlServerStreamItem::Row(row) => streamed_rows.push(row.to_vec()),
}
Ok(())
})
.await
.unwrap();
assert_eq!(streamed_rows, result.rows);
assert_eq!(summary.rows_exported, result.rows.len() as u64);
let serialized = serde_json::to_string(&streamed_rows).unwrap();
assert_eq!(serde_json::from_str::<Vec<Vec<Value>>>(&serialized).unwrap(), result.rows);
for row in &result.rows[..values.len()] {
let wdz = format_grid_sql_literal(&row[1], Some(DatabaseType::SqlServer), None);
let sdz = format_grid_sql_literal(&row[2], Some(DatabaseType::SqlServer), None);
let update = format!("UPDATE #dbx_issue_9984_real SET WDZ = {wdz}, SDZ = {sdz} WHERE id = {}", row[0]);
assert_eq!(sqlserver::execute_query(&mut client, &update).await.unwrap().affected_rows, 1);
}
let reread = client.query(raw_sql, &[]).await.unwrap().into_first_result().await.unwrap();
assert_eq!(reread.len(), raw.len());
for (before, after) in raw.iter().zip(&reread) {
for column in [1, 2] {
assert_eq!(before.get::<f32, _>(column).map(f32::to_bits), after.get::<f32, _>(column).map(f32::to_bits));
}
}
assert!(sqlserver::execute_query(&mut client, "SELECT CAST('not-a-number' AS real)").await.is_err());
let recovered = sqlserver::execute_query(&mut client, select).await.unwrap();
assert_eq!(recovered.rows, result.rows);
println!(
"Verified query, RPC batch, simple batch, stream export, JSON, numeric SQL write-back, and error recovery"
);
}