diff --git a/agents/drivers/tdengine/src/value.rs b/agents/drivers/tdengine/src/value.rs index d40137c215..e56ea63529 100644 --- a/agents/drivers/tdengine/src/value.rs +++ b/agents/drivers/tdengine/src/value.rs @@ -18,7 +18,7 @@ pub fn borrowed_value_to_json(value: BorrowedValue<'_>, timezone: Option) -> BorrowedValue::USmallInt(value) => json!(value), BorrowedValue::UInt(value) => json!(value), BorrowedValue::UBigInt(value) => json!(value), - BorrowedValue::Float(value) => finite_number(value as f64), + BorrowedValue::Float(value) => finite_float_number(value), BorrowedValue::Double(value) => finite_number(value), BorrowedValue::VarChar(value) => Value::String(value.to_string()), BorrowedValue::NChar(value) => Value::String(value.into_owned()), @@ -38,6 +38,17 @@ fn finite_number(value: f64) -> Value { Number::from_f64(value).map(Value::Number).unwrap_or_else(|| Value::String(value.to_string())) } +fn finite_float_number(value: f32) -> Value { + let shortest = value.to_string(); + if value.is_finite() { + // Widening f32 directly exposes binary approximation digits that were never significant in FLOAT. + let rounded = shortest.parse::().expect("a formatted finite f32 is a valid f64"); + Value::Number(Number::from_f64(rounded).expect("a finite f32 is a finite f64")) + } else { + Value::String(shortest) + } +} + fn binary_json(value: &[u8]) -> Value { json!({ "$binary": STANDARD.encode(value) }) } @@ -86,6 +97,40 @@ mod tests { use super::*; use taos::Ty; + #[test] + fn serializes_float_with_shortest_round_trippable_decimal() { + let cases = [ + (8.6_f32, "8.6"), + (0.72_f32, "0.72"), + (59.6_f32, "59.6"), + (-13.25_f32, "-13.25"), + (0.0_f32, "0.0"), + (-0.0_f32, "-0.0"), + (f32::from_bits(1), "1e-45"), + (f32::MIN_POSITIVE, "1.1754944e-38"), + (f32::MAX, "3.4028235e+38"), + (1.0e-7_f32, "1e-7"), + (1.0e20_f32, "1e+20"), + ]; + + for (value, expected) in cases { + let encoded = borrowed_value_to_json(BorrowedValue::Float(value), None); + assert!(encoded.is_number(), "finite FLOAT must remain a JSON number"); + assert_eq!(encoded.to_string(), expected); + assert_eq!((encoded.as_f64().unwrap() as f32).to_bits(), value.to_bits()); + } + } + + #[test] + fn keeps_float_fallback_and_other_numeric_types_unchanged() { + assert_eq!(borrowed_value_to_json(BorrowedValue::Float(f32::NAN), None), json!("NaN")); + assert_eq!(borrowed_value_to_json(BorrowedValue::Float(f32::INFINITY), None), json!("inf")); + assert_eq!(borrowed_value_to_json(BorrowedValue::Float(f32::NEG_INFINITY), None), json!("-inf")); + assert_eq!(borrowed_value_to_json(BorrowedValue::Null(Ty::Float), None), Value::Null); + assert_eq!(borrowed_value_to_json(BorrowedValue::Double(8.600000381469727), None), json!(8.600000381469727)); + assert_eq!(borrowed_value_to_json(BorrowedValue::Int(42), None), json!(42)); + } + #[test] fn preserves_timestamp_precision_with_at_least_milliseconds() { assert_eq!( diff --git a/agents/drivers/tdengine/tests/live.rs b/agents/drivers/tdengine/tests/live.rs index 7d6acf4b6b..8e88246580 100644 --- a/agents/drivers/tdengine/tests/live.rs +++ b/agents/drivers/tdengine/tests/live.rs @@ -142,6 +142,8 @@ fn tdengine_websocket_live_compatibility() { assert_eq!(query["rows"].as_array().unwrap().len(), 2); assert_eq!(query["truncated"], true); assert_eq!(query["rows"][0][0], "2026-08-04 10:00:00.001"); + assert_eq!(query["column_types"][1], "FLOAT"); + assert_eq!(query["rows"][0][1], 10.1); let connection_info = agent.call("connection_info", json!({"agentSessionId": session_id})); assert_eq!(connection_info["databaseInfo"]["currentDatabase"], database); @@ -158,6 +160,7 @@ fn tdengine_websocket_live_compatibility() { ); assert_eq!(first_page["has_more"], true); assert_eq!(first_page["truncated"], false); + assert_eq!(first_page["rows"][0][1], 10.1); let query_session_id = first_page["session_id"].as_str().expect("paged query session"); let final_page = agent.call( "fetch_query_page", @@ -169,6 +172,7 @@ fn tdengine_websocket_live_compatibility() { }), ); assert_eq!(final_page["rows"].as_array().unwrap().len(), 1); + assert_eq!(final_page["rows"][0][1], 10.2); assert_eq!(final_page["has_more"], false); assert_eq!(final_page["truncated"], true);