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-3476-7c39c3748d2351cdb8b2b5432937c96766fc07bf in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git
commit 217e66dbdf2e206b843e6e68acd85fb4feeeea7b Author: Spicy Shark <[email protected]> AuthorDate: Sat Oct 24 08:52:39 2026 +0000 fix(spec): read float JSON values like iceberg-java (#3476) * fix(spec): read float JSON values like iceberg-java The Float branch of RawLiteral::try_into rejected a double whose distance to the narrowed f32 exceeded the absolute f32::EPSILON, so valid values such as 100.1 and 123.456 failed. iceberg-java narrows with floatValue() and does no precision check; do the same. * test(spec): pin non-finite and negative zero double to float Check NaN, both infinities, and the sign of zero when narrowing Avro doubles. --- crates/iceberg/src/spec/values/serde.rs | 12 +----- crates/iceberg/src/spec/values/tests.rs | 69 +++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/crates/iceberg/src/spec/values/serde.rs b/crates/iceberg/src/spec/values/serde.rs index 7fa41159d..028c2d796 100644 --- a/crates/iceberg/src/spec/values/serde.rs +++ b/crates/iceberg/src/spec/values/serde.rs @@ -467,17 +467,7 @@ pub(crate) mod _serde { _ => Err(invalid_err("float")), }, RawLiteralEnum::Double(v) => match ty { - Type::Primitive(PrimitiveType::Float) => { - let v_32 = v as f32; - if v_32.is_finite() { - let v_64 = f64::from(v_32); - if (v_64 - v).abs() > f32::EPSILON as f64 { - // there is a precision loss - return Err(invalid_err("double")); - } - } - Ok(Some(Literal::float(v_32))) - } + Type::Primitive(PrimitiveType::Float) => Ok(Some(Literal::float(v as f32))), Type::Primitive(PrimitiveType::Double) => Ok(Some(Literal::double(v))), _ => Err(invalid_err("double")), }, diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs index a6945015b..847fbd64c 100644 --- a/crates/iceberg/src/spec/values/tests.rs +++ b/crates/iceberg/src/spec/values/tests.rs @@ -187,6 +187,75 @@ fn json_double() { ); } +const FLOAT_VALUES: [f32; 6] = [ + 100.1, + 123.456, + -123.456, + f32::MAX, + f32::MIN, + f32::from_bits(1), +]; + +#[test] +fn raw_literal_json_number_to_float() { + for value in FLOAT_VALUES { + let raw: RawLiteral = serde_json::from_str(&format!("{value:e}")).unwrap(); + + assert_eq!( + raw.try_into(&Primitive(PrimitiveType::Float)).unwrap(), + Some(Literal::float(value)) + ); + } +} + +#[test] +fn raw_literal_avro_double_to_float() { + for value in FLOAT_VALUES { + for double in [f64::from(value), format!("{value:e}").parse().unwrap()] { + let raw: RawLiteral = apache_avro::from_value(&Value::Double(double)).unwrap(); + + assert_eq!( + raw.try_into(&Primitive(PrimitiveType::Float)).unwrap(), + Some(Literal::float(value)) + ); + } + } +} + +#[test] +fn raw_literal_double_without_exact_float_narrows_to_nearest() { + for (double, expected) in [(16777217.0_f64, 16777216.0_f32), (1.0000000001, 1.0)] { + let raw: RawLiteral = serde_json::from_str(&format!("{double:?}")).unwrap(); + + assert_eq!( + raw.try_into(&Primitive(PrimitiveType::Float)).unwrap(), + Some(Literal::float(expected)) + ); + } +} + +#[test] +fn raw_literal_avro_non_finite_and_negative_zero_double_to_float() { + for (double, expected) in [ + (f64::NAN, f32::NAN), + (f64::INFINITY, f32::INFINITY), + (f64::NEG_INFINITY, f32::NEG_INFINITY), + (-0.0, -0.0_f32), + ] { + let raw: RawLiteral = apache_avro::from_value(&Value::Double(double)).unwrap(); + let value = match raw.try_into(&Primitive(PrimitiveType::Float)).unwrap() { + Some(Literal::Primitive(PrimitiveLiteral::Float(value))) => value.0, + _ => panic!("unexpected literal type"), + }; + + if expected.is_nan() { + assert!(value.is_nan()); + } else { + assert_eq!(value.to_bits(), expected.to_bits()); + } + } +} + #[test] fn json_date() { let record = r#""2017-11-16""#;
