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-3331-f53314e4fab2e2708bd6b94041b701463653fcc2 in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git
commit f533cd6175bb42b5788537f589cd9450a3054d27 Author: Minh Vu <[email protected]> AuthorDate: Wed Nov 4 20:50:19 2026 +0000 fix(spec): reject non-finite decimal values (#3331) --- crates/iceberg/src/spec/values/datum.rs | 2 + crates/iceberg/src/spec/values/decimal_utils.rs | 11 ++- crates/iceberg/src/spec/values/tests.rs | 90 ++++++++++++++++++++++++- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/crates/iceberg/src/spec/values/datum.rs b/crates/iceberg/src/spec/values/datum.rs index 5153151c8..81c6708b9 100644 --- a/crates/iceberg/src/spec/values/datum.rs +++ b/crates/iceberg/src/spec/values/datum.rs @@ -1029,6 +1029,7 @@ impl Datum { /// assert_eq!(&format!("{t}"), "1.23"); /// ``` pub fn decimal(value: Decimal) -> Result<Self> { + ensure_data_valid!(value.is_finite(), "Decimal value must be finite: {}", value); let scale = decimal_scale(&value); let r#type = Type::decimal(MAX_DECIMAL_PRECISION, scale)?; @@ -1048,6 +1049,7 @@ impl Datum { /// which is useful when you need to control the storage requirements. /// Use [`Datum::decimal`] if you want to use the maximum precision (38). pub fn decimal_with_precision(value: Decimal, precision: u32) -> Result<Self> { + ensure_data_valid!(value.is_finite(), "Decimal value must be finite: {}", value); let scale = decimal_scale(&value); let mantissa = decimal_mantissa(&value); diff --git a/crates/iceberg/src/spec/values/decimal_utils.rs b/crates/iceberg/src/spec/values/decimal_utils.rs index 8bb90c973..942ec69ea 100644 --- a/crates/iceberg/src/spec/values/decimal_utils.rs +++ b/crates/iceberg/src/spec/values/decimal_utils.rs @@ -23,8 +23,8 @@ use fastnum::D128; use fastnum::decimal::Context; -use crate::Result; use crate::error::invalid_data; +use crate::{Result, ensure_data_valid}; /// Re-export D128 as the Decimal type for use throughout the crate. pub type Decimal = D128; @@ -93,7 +93,14 @@ pub fn decimal_new(mantissa: i64, scale: u32) -> Decimal { /// /// This is equivalent to rust_decimal's `Decimal::from_str_exact`. pub fn decimal_from_str_exact(s: &str) -> Result<Decimal> { - D128::from_str(s, Context::default()).map_err(|e| invalid_data!("Can't parse decimal: {e}")) + let decimal = D128::from_str(s, Context::default()) + .map_err(|e| invalid_data!("Can't parse decimal: {e}"))?; + ensure_data_valid!( + decimal.is_finite(), + "Decimal value must be finite: {}", + decimal + ); + Ok(decimal) } /// Get the mantissa (unscaled coefficient) as i128. diff --git a/crates/iceberg/src/spec/values/tests.rs b/crates/iceberg/src/spec/values/tests.rs index 2de6e2916..67fc82a56 100644 --- a/crates/iceberg/src/spec/values/tests.rs +++ b/crates/iceberg/src/spec/values/tests.rs @@ -35,7 +35,7 @@ use crate::spec::datatypes::{ ListType, MapType, NestedField, PrimitiveType, StructType, Type, VariantType, }; use crate::spec::values::datum::{INT_MAX, INT_MIN, LONG_MAX, LONG_MIN}; -use crate::spec::values::{Datum, Literal, Map, PrimitiveLiteral, RawLiteral, Struct}; +use crate::spec::values::{Datum, Decimal, Literal, Map, PrimitiveLiteral, RawLiteral, Struct}; fn check_json_serde(json: &str, expected_literal: Literal, expected_type: &Type) { let raw_json_value = serde_json::from_str::<JsonValue>(json).unwrap(); @@ -1907,6 +1907,94 @@ fn test_date_from_json_as_number() { // Both formats should produce the same Literal value } +#[test] +fn test_decimal_from_str_rejects_non_finite_values() { + for input in [ + "NaN", + "nan", + "NAN", + "Inf", + "inf", + "+Inf", + "-Inf", + "Infinity", + "+Infinity", + "-Infinity", + "infinity", + "INFINITY", + ] { + assert_eq!( + Datum::decimal_from_str(input).unwrap_err().kind(), + ErrorKind::DataInvalid, + "{input}" + ); + assert_eq!( + Literal::decimal_from_str(input).unwrap_err().kind(), + ErrorKind::DataInvalid, + "{input}" + ); + } +} + +#[test] +fn test_decimal_constructors_reject_non_finite_values() { + for value in [Decimal::NAN, Decimal::INFINITY, Decimal::NEG_INFINITY] { + assert_eq!( + Datum::decimal(value).unwrap_err().kind(), + ErrorKind::DataInvalid + ); + assert_eq!( + Datum::decimal_with_precision(value, 9).unwrap_err().kind(), + ErrorKind::DataInvalid + ); + } +} + +#[test] +fn test_decimal_from_json_rejects_non_finite_values() { + let decimal_type = Type::decimal(9, 2).unwrap(); + for input in ["NaN", "Infinity", "-Infinity"] { + assert_eq!( + Literal::try_from_json(JsonValue::String(input.to_string()), &decimal_type) + .unwrap_err() + .kind(), + ErrorKind::DataInvalid, + "{input}" + ); + } +} + +#[test] +fn test_decimal_constructors_accept_finite_values() { + for (input, mantissa, scale) in [ + ("0", 0, 0), + ("-0", 0, 0), + ("0.00", 0, 2), + ("-0.00", 0, 2), + ("123.45", 12345, 2), + ("-123.45", -12345, 2), + ] { + let datum = Datum::decimal_from_str(input).unwrap(); + assert_eq!(datum.literal(), &PrimitiveLiteral::Int128(mantissa)); + assert_eq!(datum.data_type(), &PrimitiveType::Decimal { + precision: 38, + scale, + }); + assert_eq!( + Literal::decimal_from_str(input).unwrap(), + Literal::decimal(mantissa) + ); + let value = Decimal::from_str(input, fastnum::decimal::Context::default()).unwrap(); + assert_eq!(Datum::decimal(value).unwrap(), datum); + let datum = Datum::decimal_with_precision(value, 9).unwrap(); + assert_eq!(datum.literal(), &PrimitiveLiteral::Int128(mantissa)); + assert_eq!(datum.data_type(), &PrimitiveType::Decimal { + precision: 9, + scale, + }); + } +} + #[test] fn test_datum_to_decimal_narrows_precision_when_scale_matches() { let target_type = Primitive(PrimitiveType::Decimal {
