You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by gi...@apache.org on 2026/10/17 17:24:50 UTC

(arrow-rs) 01/01: fix(parquet-variant-json): preserve whole float formatting (#11513)

This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11513-d45aaf6b15d527de66ac38330a2e1c4f4f444541
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git

commit 6db2d4fd99c766ecd65a90419cea717ad5fbb619
Author: Peter L <ce...@hotmail.com>
AuthorDate: Sat Oct 17 17:24:23 2026 +0000

    fix(parquet-variant-json): preserve whole float formatting (#11513)
    
    # Which issue does this PR close?
    
    - N/A
    
    # Rationale for this change
    
    `VariantToJson` writes `Variant::Float` and `Variant::Double` with
    `Display`, which drops the fractional part of a whole-valued float.
    `Variant::Double(2.0)` renders as `2`, and `Variant::Float(0.0)` as `0`.
    The JSON then reads as an integer, so a consumer that distinguishes
    integers from floats sees a different type, and a JSON string rebuilt
    from a Variant differs from the JSON the Variant was parsed from.
    
    # What changes are included in this PR?
    
    - Float and double values are formatted with `ryu`, as `serde_json` and
    `arrow-json` already do. `2.0` renders as `2.0`.
    - `ryu` is added to `parquet-variant-json`. It is already a dependency
    of `arrow-cast` and `arrow-json`, so the dependency tree does not
    change.
    
    `ryu` also uses exponent notation for very large and very small
    magnitudes, for example `1e21` and `1e-7`, where `Display` writes all
    the digits. Both forms are valid JSON.
    
    # Are these changes tested?
    
    Yes. The `to_json` tests now expect `0.0` for `Variant::Float(0.0)`, add
    a `Variant::Double(2.0)` case, and check that an object with a
    whole-valued double field renders it as `2.0`.
    
    # Are there any significant user-facing changes?
    
    Yes. The JSON text for whole-valued floats changes: `2.0` renders as
    `2.0` instead of `2`. The value is unchanged.
    
    Co-authored-by: Matthew Kim <38...@users.noreply.github.com>
---
 Cargo.lock                          |  1 +
 parquet-variant-json/Cargo.toml     |  1 +
 parquet-variant-json/src/to_json.rs | 22 +++++++++++++++++-----
 3 files changed, 19 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 045c6a552f..4d22fedb37 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2678,6 +2678,7 @@ dependencies = [
  "base64 0.23.1",
  "chrono",
  "parquet-variant",
+ "ryu",
  "serde_json",
  "uuid",
 ]
diff --git a/parquet-variant-json/Cargo.toml b/parquet-variant-json/Cargo.toml
index d2983f5794..d4f83f34f9 100644
--- a/parquet-variant-json/Cargo.toml
+++ b/parquet-variant-json/Cargo.toml
@@ -34,6 +34,7 @@ chrono = { workspace = true }
 serde_json = "1.0"
 base64 = "0.23"
 uuid = "1.18.0"
+ryu = "1.0"
 
 # uuid requires the `js` feature to run on wasm
 [target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/parquet-variant-json/src/to_json.rs b/parquet-variant-json/src/to_json.rs
index 5ae19aebb0..930ba935dc 100644
--- a/parquet-variant-json/src/to_json.rs
+++ b/parquet-variant-json/src/to_json.rs
@@ -175,8 +175,8 @@ impl VariantToJson for Variant<'_, '_> {
             Variant::Int16(i) => write!(buffer, "{i}")?,
             Variant::Int32(i) => write!(buffer, "{i}")?,
             Variant::Int64(i) => write!(buffer, "{i}")?,
-            Variant::Float(f) => write!(buffer, "{f}")?,
-            Variant::Double(f) => write!(buffer, "{f}")?,
+            Variant::Float(f) => write!(buffer, "{}", ryu::Buffer::new().format(*f))?,
+            Variant::Double(f) => write!(buffer, "{}", ryu::Buffer::new().format(*f))?,
             Variant::Decimal4(decimal) => write!(buffer, "{decimal}")?,
             Variant::Decimal8(decimal) => write!(buffer, "{decimal}")?,
             Variant::Decimal16(decimal) => write!(buffer, "{decimal}")?,
@@ -813,8 +813,10 @@ mod tests {
 
         JsonTest {
             variant: Variant::Float(0.0),
-            expected_json: "0",
-            expected_value: Value::Number(0.into()), // Use integer 0 to match JSON parsing
+            expected_json: "0.0",
+            expected_value: serde_json::Number::from_f64(0.0)
+                .map(Value::Number)
+                .unwrap(),
         }
         .run();
 
@@ -836,6 +838,15 @@ mod tests {
         }
         .run();
 
+        JsonTest {
+            variant: Variant::Double(2.0),
+            expected_json: "2.0",
+            expected_value: serde_json::Number::from_f64(2.0)
+                .map(Value::Number)
+                .unwrap(),
+        }
+        .run();
+
         // Decimals
         JsonTest {
             variant: Variant::from(VariantDecimal4::try_new(12345, 2).unwrap()),
@@ -1209,7 +1220,7 @@ mod tests {
             obj.insert("string_field", "test_string");
             obj.insert("int_field", 123i32);
             obj.insert("bool_field", true);
-            obj.insert("float_field", 2.71f64);
+            obj.insert("float_field", 2.0f64);
             obj.insert("null_field", ());
             obj.insert("long_field", 999i64);
             obj.finish();
@@ -1218,6 +1229,7 @@ mod tests {
         let (metadata, value) = builder.finish();
         let variant = Variant::try_new(&metadata, &value)?;
         let json = variant.to_json_string()?;
+        assert!(json.contains(r#""float_field":2.0"#));
 
         let parsed: Value = serde_json::from_str(&json).unwrap();
         let obj = parsed.as_object().expect("expected JSON object");