This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git
The following commit(s) were added to refs/heads/main by this push:
new f533cd617 fix(spec): reject non-finite decimal values (#3331)
f533cd617 is described below
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 {