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 {

Reply via email to