diff --git a/crates/dbx-drivers/src/db/sqlserver.rs b/crates/dbx-drivers/src/db/sqlserver.rs index 962a985663..eacd192c3c 100644 --- a/crates/dbx-drivers/src/db/sqlserver.rs +++ b/crates/dbx-drivers/src/db/sqlserver.rs @@ -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)) = ::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)) = ::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::(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::().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::(&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). diff --git a/crates/dbx-drivers/tests/live_sqlserver_issue_9984_real.rs b/crates/dbx-drivers/tests/live_sqlserver_issue_9984_real.rs new file mode 100644 index 0000000000..e581d948b1 --- /dev/null +++ b/crates/dbx-drivers/tests/live_sqlserver_issue_9984_real.rs @@ -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::(1).unwrap().to_bits(), expected.to_bits()); + assert_eq!(row.get::(2).unwrap().to_bits(), expected.to_bits()); + } + println!( + "Raw f32 widened to f64: {:?}", + raw.iter().take(4).map(|row| f64::from(row.get::(1).unwrap())).collect::>() + ); + + 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::>>(&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::(column).map(f32::to_bits), after.get::(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" + ); +}