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

xuanwo 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 6165cd94c feat(spec): replace rust_decimal with fastnum for 38-digit 
precision (#2063)
6165cd94c is described below

commit 6165cd94c61c4e25af8d2e742c8d1671c9a5d01c
Author: Nathan Metzger <[email protected]>
AuthorDate: Thu Jan 29 18:20:57 2026 +0100

    feat(spec): replace rust_decimal with fastnum for 38-digit precision (#2063)
    
    ## Summary
    
    Replace rust_decimal with fastnum::D128 to support 38-digit decimal
    precision as required by the Iceberg spec, addressing issue #669.
    
    ## Changes
    
    - Add `crates/iceberg/src/spec/values/decimal_utils.rs` with:
    - Compatibility layer providing rust_decimal-like API using
    fastnum::D128
    - `decimal_from_i128_with_scale()`, `decimal_mantissa()`,
    `decimal_scale()` helpers
    - `i128_from_be_bytes()` / `i128_to_be_bytes_min()` for binary
    serialization
    - Unit tests for all helper functions including 38-digit precision
    validation
    
    - Update `Cargo.toml` (workspace and iceberg crate):
      - Add fastnum dependency with std and serde features
      - Remove rust_decimal, num-bigint, num-traits dependencies
    
    - Update `crates/iceberg/src/spec/values/datum.rs`:
      - Replace rust_decimal imports with decimal_utils
      - Update `from_bytes()` and `to_bytes()` for new decimal API
      - Update `decimal_with_precision()` validation
    
    - Update `crates/iceberg/src/spec/values/literal.rs`:
      - Update JSON serialization/deserialization for decimals
    
    - Update `crates/iceberg/src/transform/bucket.rs` and `truncate.rs`:
      - Update decimal transform implementations
    
    - Update `crates/iceberg/src/arrow/schema.rs` and `parquet_writer.rs`:
      - Update Arrow/Parquet decimal statistics handling
      - Replace `BigInt` byte conversions with helper functions
    
    - Update `.cargo/audit.toml`:
    - Remove RUSTSEC-2024-0399 ignore (rust_decimal vulnerability no longer
    applies)
    
    ## Notes
    
    fastnum::D128 provides exactly 38-digit precision with stack-based
    storage (no heap allocation), meeting the Iceberg spec requirement that
    rust_decimal (28-digit max) could not satisfy.
    
    The decimal_utils module abstracts the API differences between
    rust_decimal and fastnum, making the migration transparent to the rest
    of the codebase.
    
    Closes #669
---
 .cargo/audit.toml                                  |   3 -
 Cargo.lock                                         |  34 ++-
 Cargo.toml                                         |   3 +-
 bindings/python/Cargo.toml                         |   5 +-
 crates/iceberg/Cargo.toml                          |   3 +-
 crates/iceberg/src/arrow/schema.rs                 |  20 +-
 crates/iceberg/src/error.rs                        |   6 -
 crates/iceberg/src/spec/mod.rs                     |   1 +
 crates/iceberg/src/spec/transform.rs               |  13 +-
 crates/iceberg/src/spec/values/datum.rs            |  66 ++---
 crates/iceberg/src/spec/values/decimal_utils.rs    | 322 +++++++++++++++++++++
 crates/iceberg/src/spec/values/literal.rs          |  21 +-
 crates/iceberg/src/spec/values/mod.rs              |   2 +
 crates/iceberg/src/spec/values/tests.rs            |  23 +-
 crates/iceberg/src/transform/bucket.rs             |   3 +-
 crates/iceberg/src/transform/truncate.rs           |  11 +-
 .../src/writer/file_writer/parquet_writer.rs       |  27 +-
 17 files changed, 447 insertions(+), 116 deletions(-)

diff --git a/.cargo/audit.toml b/.cargo/audit.toml
index d403f0ac5..09e2d35c5 100644
--- a/.cargo/audit.toml
+++ b/.cargo/audit.toml
@@ -33,7 +33,4 @@ ignore = [
   #
   # Introduced by object_store, see 
https://github.com/apache/arrow-rs-object-store/issues/564
   "RUSTSEC-2025-0134",
-
-  # Tracked here: https://github.com/paupino/rust-decimal/issues/766
-  "RUSTSEC-2026-0001",
 ]
diff --git a/Cargo.lock b/Cargo.lock
index 211b6416c..577229645 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1058,6 +1058,16 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "bnum"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f781dba93de3a5ef6dc5b17c9958b208f6f3f021623b360fb605ea51ce443f10"
+dependencies = [
+ "serde",
+ "serde-big-array",
+]
+
 [[package]]
 name = "bon"
 version = "3.8.1"
@@ -2703,6 +2713,18 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
 
+[[package]]
+name = "fastnum"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "4089ab2dfd45d8ddc92febb5ca80644389d5ebb954f40231274a3f18341762e2"
+dependencies = [
+ "bnum",
+ "num-integer",
+ "num-traits",
+ "serde",
+]
+
 [[package]]
 name = "fastrand"
 version = "2.3.0"
@@ -3367,6 +3389,7 @@ dependencies = [
  "chrono",
  "derive_builder",
  "expect-test",
+ "fastnum",
  "flate2",
  "fnv",
  "futures",
@@ -3376,7 +3399,6 @@ dependencies = [
  "mockall",
  "moka",
  "murmur3",
- "num-bigint",
  "once_cell",
  "opendal",
  "ordered-float 4.6.0",
@@ -3387,7 +3409,6 @@ dependencies = [
  "reqsign",
  "reqwest",
  "roaring",
- "rust_decimal",
  "serde",
  "serde_bytes",
  "serde_derive",
@@ -5966,6 +5987,15 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-big-array"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "serde_bytes"
 version = "0.11.19"
diff --git a/Cargo.toml b/Cargo.toml
index 46ac4736b..900377c22 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -97,7 +97,6 @@ mockall = "0.13.1"
 mockito = "1"
 motore-macros = "0.4.3"
 murmur3 = "0.5.2"
-num-bigint = "0.4.6"
 once_cell = "1.20"
 opendal = "0.55.0"
 ordered-float = "4"
@@ -108,7 +107,7 @@ rand = "0.8.5"
 regex = "1.11.3"
 reqwest = { version = "0.12.12", default-features = false, features = ["json"] 
}
 roaring = { version = "0.11" }
-rust_decimal = { version = "1.39", default-features = false, features = 
["std"] }
+fastnum = { version = "0.7", default-features = false, features = ["std", 
"serde"] }
 serde = { version = "1.0.219", features = ["rc"] }
 serde_bytes = "0.11.17"
 serde_derive = "1.0.219"
diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml
index 8346d0270..c179e89f9 100644
--- a/bindings/python/Cargo.toml
+++ b/bindings/python/Cargo.toml
@@ -37,8 +37,6 @@ pyo3 = { version = "0.26", features = ["extension-module", 
"abi3-py310"] }
 iceberg-datafusion = { path = "../../crates/integrations/datafusion" }
 datafusion-ffi = { version = "51.0" }
 tokio = { version = "1.46.1", default-features = false }
-# Security: disable rkyv feature to avoid RUSTSEC-2026-0001 (rkyv 0.7.45 
vulnerability)
-rust_decimal = { version = "1.39", default-features = false, features = 
["std"] }
 
 [profile.release]
 codegen-units = 1
@@ -48,5 +46,4 @@ opt-level = "z"
 strip = true
 
 [package.metadata.cargo-machete]
-# rust_decimal is included to override feature flags for security (disable 
rkyv)
-ignored = ["rust_decimal"]
+ignored = []
diff --git a/crates/iceberg/Cargo.toml b/crates/iceberg/Cargo.toml
index 3835e77d1..d6d931c86 100644
--- a/crates/iceberg/Cargo.toml
+++ b/crates/iceberg/Cargo.toml
@@ -67,7 +67,6 @@ futures = { workspace = true }
 itertools = { workspace = true }
 moka = { version = "0.12.10", features = ["future"] }
 murmur3 = { workspace = true }
-num-bigint = { workspace = true }
 once_cell = { workspace = true }
 opendal = { workspace = true }
 ordered-float = { workspace = true }
@@ -76,7 +75,7 @@ rand = { workspace = true }
 reqsign = { version = "0.16.3", optional = true, default-features = false }
 reqwest = { workspace = true }
 roaring = { workspace = true }
-rust_decimal = { workspace = true }
+fastnum = { workspace = true }
 serde = { workspace = true }
 serde_bytes = { workspace = true }
 serde_derive = { workspace = true }
diff --git a/crates/iceberg/src/arrow/schema.rs 
b/crates/iceberg/src/arrow/schema.rs
index b15e141c8..e00f79a3b 100644
--- a/crates/iceberg/src/arrow/schema.rs
+++ b/crates/iceberg/src/arrow/schema.rs
@@ -27,13 +27,12 @@ use arrow_array::{
     TimestampMicrosecondArray,
 };
 use arrow_schema::{DataType, Field, Fields, Schema as ArrowSchema, TimeUnit};
-use num_bigint::BigInt;
 use parquet::arrow::PARQUET_FIELD_ID_META_KEY;
 use parquet::file::statistics::Statistics;
-use rust_decimal::prelude::ToPrimitive;
 use uuid::Uuid;
 
 use crate::error::Result;
+use crate::spec::decimal_utils::i128_from_be_bytes;
 use crate::spec::{
     Datum, FIRST_FIELD_ID, ListType, MapType, NestedField, NestedFieldRef, 
PrimitiveLiteral,
     PrimitiveType, Schema, SchemaVisitor, StructType, Type,
@@ -680,7 +679,8 @@ impl SchemaVisitor for ToArrowSchemaConverter {
                 DataType::FixedSizeBinary(16),
             )),
             crate::spec::PrimitiveType::Fixed(len) => 
Ok(ArrowSchemaOrFieldOrType::Type(
-                len.to_i32()
+                i32::try_from(*len)
+                    .ok()
                     .map(DataType::FixedSizeBinary)
                     .unwrap_or(DataType::LargeBinary),
             )),
@@ -722,10 +722,10 @@ pub(crate) fn get_arrow_datum(datum: &Datum) -> 
Result<Arc<dyn ArrowDatum + Send
             Ok(Arc::new(Int64Array::new_scalar(*value)))
         }
         (PrimitiveType::Float, PrimitiveLiteral::Float(value)) => {
-            Ok(Arc::new(Float32Array::new_scalar(value.to_f32().unwrap())))
+            Ok(Arc::new(Float32Array::new_scalar(value.into_inner())))
         }
         (PrimitiveType::Double, PrimitiveLiteral::Double(value)) => {
-            Ok(Arc::new(Float64Array::new_scalar(value.to_f64().unwrap())))
+            Ok(Arc::new(Float64Array::new_scalar(value.into_inner())))
         }
         (PrimitiveType::String, PrimitiveLiteral::String(value)) => {
             Ok(Arc::new(StringArray::new_scalar(value.as_str())))
@@ -835,10 +835,9 @@ pub(crate) fn get_parquet_stat_min_as_datum(
             let Some(bytes) = stats.min_bytes_opt() else {
                 return Ok(None);
             };
-            let unscaled_value = BigInt::from_signed_bytes_be(bytes);
             Some(Datum::new(
                 primitive_type.clone(),
-                
PrimitiveLiteral::Int128(unscaled_value.to_i128().ok_or_else(|| {
+                
PrimitiveLiteral::Int128(i128_from_be_bytes(bytes).ok_or_else(|| {
                     Error::new(
                         ErrorKind::DataInvalid,
                         format!("Can't convert bytes to i128: {bytes:?}"),
@@ -982,10 +981,9 @@ pub(crate) fn get_parquet_stat_max_as_datum(
             let Some(bytes) = stats.max_bytes_opt() else {
                 return Ok(None);
             };
-            let unscaled_value = BigInt::from_signed_bytes_be(bytes);
             Some(Datum::new(
                 primitive_type.clone(),
-                
PrimitiveLiteral::Int128(unscaled_value.to_i128().ok_or_else(|| {
+                
PrimitiveLiteral::Int128(i128_from_be_bytes(bytes).ok_or_else(|| {
                     Error::new(
                         ErrorKind::DataInvalid,
                         format!("Can't convert bytes to i128: {bytes:?}"),
@@ -1295,9 +1293,9 @@ mod tests {
     use std::sync::Arc;
 
     use arrow_schema::{DataType, Field, Schema as ArrowSchema, TimeUnit};
-    use rust_decimal::Decimal;
 
     use super::*;
+    use crate::spec::decimal_utils::decimal_new;
     use crate::spec::{Literal, Schema};
 
     /// Create a simple field with metadata.
@@ -2127,7 +2125,7 @@ mod tests {
             assert_eq!(array.value(0), 42);
         }
         {
-            let datum = Datum::decimal_with_precision(Decimal::new(123, 2), 
30).unwrap();
+            let datum = Datum::decimal_with_precision(decimal_new(123, 2), 
30).unwrap();
             let arrow_datum = get_arrow_datum(&datum).unwrap();
             let (array, is_scalar) = arrow_datum.get();
             let array = 
array.as_any().downcast_ref::<Decimal128Array>().unwrap();
diff --git a/crates/iceberg/src/error.rs b/crates/iceberg/src/error.rs
index 6ab3a78c8..8810ef500 100644
--- a/crates/iceberg/src/error.rs
+++ b/crates/iceberg/src/error.rs
@@ -408,12 +408,6 @@ define_from_err!(
     "Failed to parse json string"
 );
 
-define_from_err!(
-    rust_decimal::Error,
-    ErrorKind::DataInvalid,
-    "Failed to convert decimal literal to rust decimal"
-);
-
 define_from_err!(
     parquet::errors::ParquetError,
     ErrorKind::Unexpected,
diff --git a/crates/iceberg/src/spec/mod.rs b/crates/iceberg/src/spec/mod.rs
index a2b540f08..707ebbb63 100644
--- a/crates/iceberg/src/spec/mod.rs
+++ b/crates/iceberg/src/spec/mod.rs
@@ -52,6 +52,7 @@ pub use table_metadata::*;
 pub(crate) use table_metadata_builder::FIRST_FIELD_ID;
 pub use table_properties::*;
 pub use transform::*;
+pub(crate) use values::decimal_utils;
 pub use values::*;
 pub use view_metadata::*;
 pub use view_version::*;
diff --git a/crates/iceberg/src/spec/transform.rs 
b/crates/iceberg/src/spec/transform.rs
index 026b12613..73fd290ee 100644
--- a/crates/iceberg/src/spec/transform.rs
+++ b/crates/iceberg/src/spec/transform.rs
@@ -24,6 +24,7 @@ use std::str::FromStr;
 use fnv::FnvHashSet;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 
+use super::values::decimal_utils::decimal_from_i128_with_scale;
 use super::{Datum, PrimitiveLiteral};
 use crate::ErrorKind;
 use crate::error::{Error, Result};
@@ -660,7 +661,7 @@ impl Transform {
                 (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => 
Some(Datum::int(v - 1)),
                 (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => 
Some(Datum::long(v - 1)),
                 (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) 
=> {
-                    Some(Datum::decimal(v - 1)?)
+                    Some(Datum::decimal(decimal_from_i128_with_scale(v - 1, 
0))?)
                 }
                 (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => 
Some(Datum::date(v - 1)),
                 (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
@@ -672,7 +673,7 @@ impl Transform {
                 (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => 
Some(Datum::int(v + 1)),
                 (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => 
Some(Datum::long(v + 1)),
                 (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) 
=> {
-                    Some(Datum::decimal(v + 1)?)
+                    Some(Datum::decimal(decimal_from_i128_with_scale(v + 1, 
0))?)
                 }
                 (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => 
Some(Datum::date(v + 1)),
                 (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
@@ -806,7 +807,9 @@ impl Transform {
         match (datum.data_type(), datum.literal()) {
             (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v 
+ 1)),
             (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => 
Ok(Datum::long(v + 1)),
-            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => 
Datum::decimal(v + 1),
+            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
+                Datum::decimal(decimal_from_i128_with_scale(v + 1, 0))
+            }
             (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => 
Ok(Datum::date(v + 1)),
             (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                 Ok(Datum::timestamp_micros(v + 1))
@@ -842,7 +845,9 @@ impl Transform {
         match (datum.data_type(), datum.literal()) {
             (PrimitiveType::Int, PrimitiveLiteral::Int(v)) => Ok(Datum::int(v 
- 1)),
             (PrimitiveType::Long, PrimitiveLiteral::Long(v)) => 
Ok(Datum::long(v - 1)),
-            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => 
Datum::decimal(v - 1),
+            (PrimitiveType::Decimal { .. }, PrimitiveLiteral::Int128(v)) => {
+                Datum::decimal(decimal_from_i128_with_scale(v - 1, 0))
+            }
             (PrimitiveType::Date, PrimitiveLiteral::Int(v)) => 
Ok(Datum::date(v - 1)),
             (PrimitiveType::Timestamp, PrimitiveLiteral::Long(v)) => {
                 Ok(Datum::timestamp_micros(v - 1))
diff --git a/crates/iceberg/src/spec/values/datum.rs 
b/crates/iceberg/src/spec/values/datum.rs
index 88209ae95..3d4abc019 100644
--- a/crates/iceberg/src/spec/values/datum.rs
+++ b/crates/iceberg/src/spec/values/datum.rs
@@ -22,15 +22,16 @@ use std::fmt::{Display, Formatter};
 use std::str::FromStr;
 
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
-use num_bigint::BigInt;
 use ordered_float::{Float, OrderedFloat};
-use rust_decimal::Decimal;
-use rust_decimal::prelude::ToPrimitive;
 use serde::de::{self, MapAccess};
 use serde::ser::SerializeStruct;
 use serde::{Deserialize, Serialize};
 use serde_bytes::ByteBuf;
 
+use super::decimal_utils::{
+    Decimal, decimal_from_i128_with_scale, decimal_from_str_exact, 
decimal_mantissa, decimal_scale,
+    i128_from_be_bytes, i128_to_be_bytes_min,
+};
 use super::literal::Literal;
 use super::primitive::PrimitiveLiteral;
 use super::serde::_serde::RawLiteral;
@@ -268,8 +269,8 @@ impl PartialOrd for Datum {
                     scale: other_scale,
                 },
             ) => {
-                let val = Decimal::from_i128_with_scale(*val, *scale);
-                let other_val = Decimal::from_i128_with_scale(*other_val, 
*other_scale);
+                let val = decimal_from_i128_with_scale(*val, *scale);
+                let other_val = decimal_from_i128_with_scale(*other_val, 
*other_scale);
                 val.partial_cmp(&other_val)
             }
             _ => None,
@@ -315,7 +316,7 @@ impl Display for Datum {
                 },
                 PrimitiveLiteral::Int128(val),
             ) => {
-                write!(f, "{}", Decimal::from_i128_with_scale(*val, *scale))
+                write!(f, "{}", decimal_from_i128_with_scale(*val, *scale))
             }
             (_, _) => {
                 unreachable!()
@@ -407,8 +408,7 @@ impl Datum {
             PrimitiveType::Fixed(_) => 
PrimitiveLiteral::Binary(Vec::from(bytes)),
             PrimitiveType::Binary => 
PrimitiveLiteral::Binary(Vec::from(bytes)),
             PrimitiveType::Decimal { .. } => {
-                let unscaled_value = BigInt::from_signed_bytes_be(bytes);
-                
PrimitiveLiteral::Int128(unscaled_value.to_i128().ok_or_else(|| {
+                
PrimitiveLiteral::Int128(i128_from_be_bytes(bytes).ok_or_else(|| {
                     Error::new(
                         ErrorKind::DataInvalid,
                         format!("Can't convert bytes to i128: {bytes:?}"),
@@ -461,10 +461,8 @@ impl Datum {
                 };
 
                 // The primitive literal is unscaled value.
-                let unscaled_value = BigInt::from(*val);
-                // Convert into two's-complement byte representation of the 
BigInt
-                // in big-endian byte order.
-                let mut bytes = unscaled_value.to_signed_bytes_be();
+                // Convert into two's-complement byte representation in 
big-endian byte order.
+                let mut bytes = i128_to_be_bytes_min(*val);
                 // Truncate with required bytes to make sure.
                 bytes.truncate(required_bytes as usize);
 
@@ -993,22 +991,18 @@ impl Datum {
         }
     }
 
-    /// Creates decimal literal from string. See [`Decimal::from_str_exact`].
+    /// Creates decimal literal from string.
     ///
     /// Example:
     ///
     /// ```rust
     /// use iceberg::spec::Datum;
-    /// use itertools::assert_equal;
-    /// use rust_decimal::Decimal;
     /// let t = Datum::decimal_from_str("123.45").unwrap();
     ///
     /// assert_eq!(&format!("{t}"), "123.45");
     /// ```
     pub fn decimal_from_str<S: AsRef<str>>(s: S) -> Result<Self> {
-        let decimal = Decimal::from_str_exact(s.as_ref()).map_err(|e| {
-            Error::new(ErrorKind::DataInvalid, "Can't parse 
decimal.").with_source(e)
-        })?;
+        let decimal = decimal_from_str_exact(s.as_ref())?;
 
         Self::decimal(decimal)
     }
@@ -1019,21 +1013,19 @@ impl Datum {
     ///
     /// ```rust
     /// use iceberg::spec::Datum;
-    /// use rust_decimal::Decimal;
     ///
-    /// let t = Datum::decimal(Decimal::new(123, 2)).unwrap();
+    /// let t = Datum::decimal_from_str("1.23").unwrap();
     ///
     /// assert_eq!(&format!("{t}"), "1.23");
     /// ```
-    pub fn decimal(value: impl Into<Decimal>) -> Result<Self> {
-        let decimal = value.into();
-        let scale = decimal.scale();
+    pub fn decimal(value: Decimal) -> Result<Self> {
+        let scale = decimal_scale(&value);
 
         let r#type = Type::decimal(MAX_DECIMAL_PRECISION, scale)?;
         if let Type::Primitive(p) = r#type {
             Ok(Self {
                 r#type: p,
-                literal: PrimitiveLiteral::Int128(decimal.mantissa()),
+                literal: PrimitiveLiteral::Int128(decimal_mantissa(&value)),
             })
         } else {
             unreachable!("Decimal type must be primitive.")
@@ -1042,27 +1034,19 @@ impl Datum {
 
     /// Try to create a decimal literal from [`Decimal`] with precision.
     ///
-    /// Example:
-    ///
-    /// ```rust
-    /// use iceberg::spec::Datum;
-    /// use rust_decimal::Decimal;
-    ///
-    /// let t = Datum::decimal_with_precision(Decimal::new(123, 2), 
30).unwrap();
-    ///
-    /// assert_eq!(&format!("{t}"), "1.23");
-    /// ```
-    pub fn decimal_with_precision(value: impl Into<Decimal>, precision: u32) 
-> Result<Self> {
-        let decimal = value.into();
-        let scale = decimal.scale();
+    /// This method allows specifying a custom precision for the decimal type,
+    /// 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> {
+        let scale = decimal_scale(&value);
+        let mantissa = decimal_mantissa(&value);
 
         let available_bytes = Type::decimal_required_bytes(precision)? as 
usize;
-        let unscaled_value = BigInt::from(decimal.mantissa());
-        let actual_bytes = unscaled_value.to_signed_bytes_be();
+        let actual_bytes = i128_to_be_bytes_min(mantissa);
         if actual_bytes.len() > available_bytes {
             return Err(Error::new(
                 ErrorKind::DataInvalid,
-                format!("Decimal value {decimal} is too large for precision 
{precision}"),
+                format!("Decimal value {value} is too large for precision 
{precision}"),
             ));
         }
 
@@ -1070,7 +1054,7 @@ impl Datum {
         if let Type::Primitive(p) = r#type {
             Ok(Self {
                 r#type: p,
-                literal: PrimitiveLiteral::Int128(decimal.mantissa()),
+                literal: PrimitiveLiteral::Int128(mantissa),
             })
         } else {
             unreachable!("Decimal type must be primitive.")
diff --git a/crates/iceberg/src/spec/values/decimal_utils.rs 
b/crates/iceberg/src/spec/values/decimal_utils.rs
new file mode 100644
index 000000000..88e3f72f6
--- /dev/null
+++ b/crates/iceberg/src/spec/values/decimal_utils.rs
@@ -0,0 +1,322 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Compatibility layer for decimal operations.
+//!
+//! Provides rust_decimal-compatible API using fastnum's D128 internally.
+//! D128 supports 38-digit precision, meeting the Iceberg spec requirement.
+
+use fastnum::D128;
+use fastnum::decimal::Context;
+
+use crate::{Error, ErrorKind, Result};
+
+/// Re-export D128 as the Decimal type for use throughout the crate.
+pub type Decimal = D128;
+
+/// Create a D128 from mantissa (i128) and scale (u32).
+///
+/// This is equivalent to rust_decimal's `Decimal::from_i128_with_scale`.
+/// The value is computed as: mantissa * 10^(-scale)
+///
+/// For example:
+/// - mantissa=12345, scale=2 => 123.45
+/// - mantissa=-456, scale=3 => -0.456
+pub fn decimal_from_i128_with_scale(mantissa: i128, scale: u32) -> Decimal {
+    if scale == 0 {
+        return D128::from_i128(mantissa).expect("i128 always fits in D128");
+    }
+
+    // Convert mantissa to string and insert decimal point at the right 
position
+    let is_negative = mantissa < 0;
+    let abs_str = mantissa.unsigned_abs().to_string();
+    let scale_usize = scale as usize;
+
+    let decimal_str = if abs_str.len() <= scale_usize {
+        // Need leading zeros: e.g., mantissa=456, scale=3 => "0.456"
+        // Or mantissa=5, scale=3 => "0.005"
+        let zeros_needed = scale_usize - abs_str.len();
+        format!(
+            "{}0.{}{}",
+            if is_negative { "-" } else { "" },
+            "0".repeat(zeros_needed),
+            abs_str
+        )
+    } else {
+        // Insert decimal point: e.g., mantissa=12345, scale=2 => "123.45"
+        let decimal_pos = abs_str.len() - scale_usize;
+        format!(
+            "{}{}.{}",
+            if is_negative { "-" } else { "" },
+            &abs_str[..decimal_pos],
+            &abs_str[decimal_pos..]
+        )
+    };
+
+    D128::from_str(&decimal_str, Context::default())
+        .expect("constructed decimal string is always valid")
+}
+
+/// Try to create a D128 from mantissa and scale, with validation.
+///
+/// This is equivalent to rust_decimal's `Decimal::try_from_i128_with_scale`.
+/// Currently always succeeds for 38-digit decimals.
+pub fn try_decimal_from_i128_with_scale(mantissa: i128, scale: u32) -> 
Result<Decimal> {
+    // For now, always succeeds since D128 supports full 38-digit precision
+    Ok(decimal_from_i128_with_scale(mantissa, scale))
+}
+
+/// Create a D128 from i64 mantissa and scale.
+///
+/// This is equivalent to rust_decimal's `Decimal::new`.
+#[allow(dead_code)]
+pub fn decimal_new(mantissa: i64, scale: u32) -> Decimal {
+    decimal_from_i128_with_scale(mantissa as i128, scale)
+}
+
+/// Parse a decimal from string with exact representation.
+///
+/// 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| Error::new(ErrorKind::DataInvalid, format!("Can't parse 
decimal: {e}")))
+}
+
+/// Get the mantissa (unscaled coefficient) as i128.
+///
+/// This is equivalent to rust_decimal's `decimal.mantissa()`.
+///
+/// The mantissa is signed: negative decimals return negative mantissa.
+pub fn decimal_mantissa(d: &Decimal) -> i128 {
+    // digits() returns unsigned coefficient as UInt<N>
+    // For Iceberg decimals (max 38 digits), this always fits in u128/i128
+    let digits = d.digits();
+
+    // Convert UInt<2> to u128 - this always succeeds for Iceberg-compliant 
decimals
+    // since 38 digits requires ~127 bits and u128 has 128 bits
+    let unsigned: u128 = digits
+        .to_u128()
+        .expect("Iceberg decimals (max 38 digits) always fit in u128");
+
+    let signed = unsigned as i128;
+    if d.is_sign_negative() {
+        -signed
+    } else {
+        signed
+    }
+}
+
+/// Get the scale (number of digits after decimal point).
+///
+/// This is equivalent to rust_decimal's `decimal.scale()`.
+pub fn decimal_scale(d: &Decimal) -> u32 {
+    let frac = d.fractional_digits_count();
+    if frac < 0 { 0 } else { frac as u32 }
+}
+
+/// Rescale a decimal to the given scale, returning the rescaled value.
+///
+/// This is equivalent to rust_decimal's `decimal.rescale(scale)`.
+pub fn decimal_rescale(d: Decimal, scale: u32) -> Decimal {
+    d.rescale(scale as i16)
+}
+
+/// Convert big-endian signed bytes to i128.
+///
+/// This handles variable-length byte arrays (up to 16 bytes) with sign 
extension.
+/// Returns None if the byte array is longer than 16 bytes.
+pub fn i128_from_be_bytes(bytes: &[u8]) -> Option<i128> {
+    if bytes.is_empty() {
+        return Some(0);
+    }
+    if bytes.len() > 16 {
+        return None; // Too large for i128
+    }
+
+    // Check sign bit (most significant bit of first byte)
+    let is_negative = bytes[0] & 0x80 != 0;
+
+    // Pad to 16 bytes with sign extension
+    let mut padded = if is_negative { [0xFF; 16] } else { [0; 16] };
+    let start = 16 - bytes.len();
+    padded[start..].copy_from_slice(bytes);
+
+    Some(i128::from_be_bytes(padded))
+}
+
+/// Convert i128 to big-endian signed bytes with minimum length.
+///
+/// This produces the shortest two's complement representation of the value.
+/// The result is suitable for Iceberg decimal binary serialization.
+pub fn i128_to_be_bytes_min(value: i128) -> Vec<u8> {
+    let bytes = value.to_be_bytes();
+
+    // Find the first significant byte
+    // For positive numbers, skip leading 0x00 bytes (but keep sign bit)
+    // For negative numbers, skip leading 0xFF bytes (but keep sign bit)
+    let is_negative = value < 0;
+    let skip_byte = if is_negative { 0xFF } else { 0x00 };
+
+    let mut start = 0;
+    while start < 15 && bytes[start] == skip_byte {
+        // Check if the next byte has the correct sign bit
+        let next_byte = bytes[start + 1];
+        let next_is_negative = (next_byte & 0x80) != 0;
+        if next_is_negative == is_negative {
+            start += 1;
+        } else {
+            break;
+        }
+    }
+
+    bytes[start..].to_vec()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_decimal_from_i128_with_scale() {
+        let d = decimal_from_i128_with_scale(12345, 2);
+        assert_eq!(d.to_string(), "123.45");
+
+        let d = decimal_from_i128_with_scale(-12345, 2);
+        assert_eq!(d.to_string(), "-123.45");
+
+        let d = decimal_from_i128_with_scale(0, 5);
+        assert_eq!(d.to_string(), "0.00000");
+    }
+
+    #[test]
+    fn test_decimal_new() {
+        let d = decimal_new(123, 2);
+        assert_eq!(d.to_string(), "1.23");
+
+        let d = decimal_new(-456, 3);
+        assert_eq!(d.to_string(), "-0.456");
+    }
+
+    #[test]
+    fn test_decimal_from_str_exact() {
+        let d = decimal_from_str_exact("123.45").unwrap();
+        assert_eq!(d.to_string(), "123.45");
+
+        let d = decimal_from_str_exact("-0.001").unwrap();
+        assert_eq!(d.to_string(), "-0.001");
+
+        let d = 
decimal_from_str_exact("99999999999999999999999999999999999999").unwrap();
+        assert_eq!(d.to_string(), "99999999999999999999999999999999999999");
+    }
+
+    #[test]
+    fn test_decimal_mantissa() {
+        let d = decimal_from_i128_with_scale(12345, 2);
+        assert_eq!(decimal_mantissa(&d), 12345);
+
+        let d = decimal_from_i128_with_scale(-12345, 2);
+        assert_eq!(decimal_mantissa(&d), -12345);
+    }
+
+    #[test]
+    fn test_decimal_scale() {
+        let d = decimal_from_i128_with_scale(12345, 2);
+        assert_eq!(decimal_scale(&d), 2);
+
+        let d = decimal_from_i128_with_scale(12345, 0);
+        assert_eq!(decimal_scale(&d), 0);
+    }
+
+    #[test]
+    fn test_decimal_rescale() {
+        let d = decimal_from_str_exact("123.45").unwrap();
+        let rescaled = decimal_rescale(d, 4);
+        assert_eq!(decimal_scale(&rescaled), 4);
+        assert_eq!(decimal_mantissa(&rescaled), 1234500);
+    }
+
+    #[test]
+    fn test_38_digit_precision() {
+        // Test that we can handle 38-digit decimals (Iceberg spec requirement)
+        let max_38_digits = "99999999999999999999999999999999999999";
+        let d = decimal_from_str_exact(max_38_digits).unwrap();
+        assert_eq!(d.to_string(), max_38_digits);
+
+        let min_38_digits = "-99999999999999999999999999999999999999";
+        let d = decimal_from_str_exact(min_38_digits).unwrap();
+        assert_eq!(d.to_string(), min_38_digits);
+    }
+
+    #[test]
+    fn test_i128_from_be_bytes() {
+        // Empty bytes
+        assert_eq!(i128_from_be_bytes(&[]), Some(0));
+
+        // Positive values
+        assert_eq!(i128_from_be_bytes(&[0x01]), Some(1));
+        assert_eq!(i128_from_be_bytes(&[0x7F]), Some(127));
+        assert_eq!(i128_from_be_bytes(&[0x00, 0xFF]), Some(255));
+        assert_eq!(i128_from_be_bytes(&[0x04, 0xD2]), Some(1234));
+
+        // Negative values (sign extension)
+        assert_eq!(i128_from_be_bytes(&[0xFF]), Some(-1));
+        assert_eq!(i128_from_be_bytes(&[0x80]), Some(-128));
+        assert_eq!(i128_from_be_bytes(&[0xFB, 0x2E]), Some(-1234));
+
+        // Too large (> 16 bytes)
+        assert_eq!(i128_from_be_bytes(&[0; 17]), None);
+    }
+
+    #[test]
+    fn test_i128_to_be_bytes_min() {
+        // Positive values
+        assert_eq!(i128_to_be_bytes_min(0), vec![0x00]);
+        assert_eq!(i128_to_be_bytes_min(1), vec![0x01]);
+        assert_eq!(i128_to_be_bytes_min(127), vec![0x7F]);
+        assert_eq!(i128_to_be_bytes_min(128), vec![0x00, 0x80]);
+        assert_eq!(i128_to_be_bytes_min(255), vec![0x00, 0xFF]);
+        assert_eq!(i128_to_be_bytes_min(1234), vec![0x04, 0xD2]);
+
+        // Negative values
+        assert_eq!(i128_to_be_bytes_min(-1), vec![0xFF]);
+        assert_eq!(i128_to_be_bytes_min(-128), vec![0x80]);
+        assert_eq!(i128_to_be_bytes_min(-129), vec![0xFF, 0x7F]);
+        assert_eq!(i128_to_be_bytes_min(-1234), vec![0xFB, 0x2E]);
+
+        // Round trip test
+        for val in [
+            0i128,
+            1,
+            -1,
+            127,
+            -128,
+            255,
+            -256,
+            12345,
+            -12345,
+            i128::MAX,
+            i128::MIN,
+        ] {
+            let bytes = i128_to_be_bytes_min(val);
+            assert_eq!(
+                i128_from_be_bytes(&bytes),
+                Some(val),
+                "Round trip failed for {val}"
+            );
+        }
+    }
+}
diff --git a/crates/iceberg/src/spec/values/literal.rs 
b/crates/iceberg/src/spec/values/literal.rs
index d6e502e8f..e82fa197c 100644
--- a/crates/iceberg/src/spec/values/literal.rs
+++ b/crates/iceberg/src/spec/values/literal.rs
@@ -22,11 +22,13 @@ use std::str::FromStr;
 
 use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
 use ordered_float::OrderedFloat;
-use rust_decimal::Decimal;
 use serde_json::{Map as JsonMap, Number, Value as JsonValue};
 use uuid::Uuid;
 
 use super::Map;
+use super::decimal_utils::{
+    decimal_from_str_exact, decimal_mantissa, decimal_rescale, 
try_decimal_from_i128_with_scale,
+};
 use super::primitive::PrimitiveLiteral;
 use super::struct_value::Struct;
 use super::temporal::{date, time, timestamp, timestamptz};
@@ -396,23 +398,20 @@ impl Literal {
         Self::Primitive(PrimitiveLiteral::Int128(decimal))
     }
 
-    /// Creates decimal literal from string. See [`Decimal::from_str_exact`].
+    /// Creates decimal literal from string.
     ///
     /// Example:
     ///
     /// ```rust
     /// use iceberg::spec::Literal;
-    /// use rust_decimal::Decimal;
     /// let t1 = Literal::decimal(12345);
     /// let t2 = Literal::decimal_from_str("123.45").unwrap();
     ///
     /// assert_eq!(t1, t2);
     /// ```
     pub fn decimal_from_str<S: AsRef<str>>(s: S) -> Result<Self> {
-        let decimal = Decimal::from_str_exact(s.as_ref()).map_err(|e| {
-            Error::new(ErrorKind::DataInvalid, "Can't parse 
decimal.").with_source(e)
-        })?;
-        Ok(Self::decimal(decimal.mantissa()))
+        let decimal = decimal_from_str_exact(s.as_ref())?;
+        Ok(Self::decimal(decimal_mantissa(&decimal)))
     }
 
     /// Attempts to convert the Literal to a PrimitiveLiteral
@@ -509,10 +508,10 @@ impl Literal {
                     },
                     JsonValue::String(s),
                 ) => {
-                    let mut decimal = Decimal::from_str_exact(&s)?;
-                    decimal.rescale(*scale);
+                    let decimal = decimal_from_str_exact(&s)?;
+                    let rescaled = decimal_rescale(decimal, *scale);
                     Ok(Some(Literal::Primitive(PrimitiveLiteral::Int128(
-                        decimal.mantissa(),
+                        decimal_mantissa(&rescaled),
                     ))))
                 }
                 (_, JsonValue::Null) => Ok(None),
@@ -672,7 +671,7 @@ impl Literal {
                         precision: _precision,
                         scale,
                     }) => {
-                        let decimal = Decimal::try_from_i128_with_scale(val, 
*scale)?;
+                        let decimal = try_decimal_from_i128_with_scale(val, 
*scale)?;
                         Ok(JsonValue::String(decimal.to_string()))
                     }
                     _ => Err(Error::new(
diff --git a/crates/iceberg/src/spec/values/mod.rs 
b/crates/iceberg/src/spec/values/mod.rs
index 2bc967191..65374c74b 100644
--- a/crates/iceberg/src/spec/values/mod.rs
+++ b/crates/iceberg/src/spec/values/mod.rs
@@ -18,6 +18,7 @@
 //! This module contains Iceberg value types
 
 pub(crate) mod datum;
+pub(crate) mod decimal_utils;
 mod literal;
 mod map;
 mod primitive;
@@ -30,6 +31,7 @@ mod tests;
 
 // Re-export all public types
 pub use datum::Datum;
+pub use decimal_utils::Decimal;
 pub use literal::Literal;
 pub use map::Map;
 pub use primitive::PrimitiveLiteral;
diff --git a/crates/iceberg/src/spec/values/tests.rs 
b/crates/iceberg/src/spec/values/tests.rs
index bb10701d8..41238ed89 100644
--- a/crates/iceberg/src/spec/values/tests.rs
+++ b/crates/iceberg/src/spec/values/tests.rs
@@ -20,11 +20,11 @@
 use apache_avro::to_value;
 use apache_avro::types::Value;
 use ordered_float::OrderedFloat;
-use rust_decimal::Decimal;
 use serde_bytes::ByteBuf;
 use serde_json::Value as JsonValue;
 use uuid::Uuid;
 
+use super::decimal_utils::{decimal_from_i128_with_scale, decimal_new};
 use crate::ErrorKind;
 use crate::avro::schema_to_avro_schema;
 use crate::spec::Schema;
@@ -395,11 +395,8 @@ fn avro_bytes_decimal() {
     for (input_bytes, decimal_num, expect_scale, expect_precision) in cases {
         check_avro_bytes_serde(
             input_bytes,
-            Datum::decimal_with_precision(
-                Decimal::new(decimal_num, expect_scale),
-                expect_precision,
-            )
-            .unwrap(),
+            Datum::decimal_with_precision(decimal_new(decimal_num, 
expect_scale), expect_precision)
+                .unwrap(),
             &PrimitiveType::Decimal {
                 precision: expect_precision,
                 scale: expect_scale,
@@ -414,10 +411,8 @@ fn avro_bytes_decimal_expect_error() {
     let cases = vec![(1234, 2, 1)];
 
     for (decimal_num, expect_scale, expect_precision) in cases {
-        let result = Datum::decimal_with_precision(
-            Decimal::new(decimal_num, expect_scale),
-            expect_precision,
-        );
+        let result =
+            Datum::decimal_with_precision(decimal_new(decimal_num, 
expect_scale), expect_precision);
         assert!(result.is_err(), "expect error but got {result:?}");
         assert_eq!(
             result.unwrap_err().kind(),
@@ -1053,7 +1048,7 @@ fn test_datum_ser_deser() {
     test_fn(datum);
     let datum = 
Datum::uuid(Uuid::parse_str("f79c3e09-677c-4bbd-a479-3f349cb785e7").unwrap());
     test_fn(datum);
-    let datum = Datum::decimal(1420).unwrap();
+    let datum = Datum::decimal(decimal_new(1420, 0)).unwrap();
     test_fn(datum);
     let datum = Datum::binary(vec![1, 2, 3, 4, 5]);
     test_fn(datum);
@@ -1140,7 +1135,7 @@ fn test_datum_long_convert_to_timestamptz() {
 
 #[test]
 fn test_datum_decimal_convert_to_long() {
-    let datum = Datum::decimal(12345).unwrap();
+    let datum = Datum::decimal(decimal_new(12345, 0)).unwrap();
 
     let result = datum.to(&Primitive(PrimitiveType::Long)).unwrap();
 
@@ -1151,7 +1146,7 @@ fn test_datum_decimal_convert_to_long() {
 
 #[test]
 fn test_datum_decimal_convert_to_long_above_max() {
-    let datum = Datum::decimal(LONG_MAX as i128 + 1).unwrap();
+    let datum = Datum::decimal(decimal_from_i128_with_scale(LONG_MAX as i128 + 
1, 0)).unwrap();
 
     let result = datum.to(&Primitive(PrimitiveType::Long)).unwrap();
 
@@ -1162,7 +1157,7 @@ fn test_datum_decimal_convert_to_long_above_max() {
 
 #[test]
 fn test_datum_decimal_convert_to_long_below_min() {
-    let datum = Datum::decimal(LONG_MIN as i128 - 1).unwrap();
+    let datum = Datum::decimal(decimal_from_i128_with_scale(LONG_MIN as i128 - 
1, 0)).unwrap();
 
     let result = datum.to(&Primitive(PrimitiveType::Long)).unwrap();
 
diff --git a/crates/iceberg/src/transform/bucket.rs 
b/crates/iceberg/src/transform/bucket.rs
index e6786a70c..52fb72f1c 100644
--- a/crates/iceberg/src/transform/bucket.rs
+++ b/crates/iceberg/src/transform/bucket.rs
@@ -294,6 +294,7 @@ mod test {
         TimestampNs, Timestamptz, TimestamptzNs, Uuid,
     };
     use crate::spec::Type::{Primitive, Struct};
+    use crate::spec::decimal_utils::decimal_new;
     use crate::spec::{Datum, NestedField, PrimitiveType, StructType, 
Transform, Type};
     use crate::transform::TransformFunction;
     use crate::transform::test::{TestProjectionFixture, TestTransformFixture};
@@ -848,7 +849,7 @@ mod test {
         let bucket = Bucket::new(10);
         assert_eq!(
             bucket
-                .transform_literal(&Datum::decimal(1420).unwrap())
+                .transform_literal(&Datum::decimal(decimal_new(1420, 
0)).unwrap())
                 .unwrap()
                 .unwrap(),
             Datum::int(9)
diff --git a/crates/iceberg/src/transform/truncate.rs 
b/crates/iceberg/src/transform/truncate.rs
index 84ef7c0da..4fac48f7d 100644
--- a/crates/iceberg/src/transform/truncate.rs
+++ b/crates/iceberg/src/transform/truncate.rs
@@ -22,6 +22,7 @@ use arrow_schema::DataType;
 
 use super::TransformFunction;
 use crate::Error;
+use crate::spec::decimal_utils::decimal_from_i128_with_scale;
 use crate::spec::{Datum, PrimitiveLiteral};
 
 #[derive(Debug)]
@@ -163,7 +164,10 @@ impl TransformFunction for Truncate {
             })),
             PrimitiveLiteral::Int128(v) => Ok(Some({
                 let width = self.width as i128;
-                Datum::decimal(Self::truncate_decimal_i128(*v, width))?
+                Datum::decimal(decimal_from_i128_with_scale(
+                    Self::truncate_decimal_i128(*v, width),
+                    0,
+                ))?
             })),
             PrimitiveLiteral::String(v) => Ok(Some({
                 let len = self.width as usize;
@@ -195,6 +199,7 @@ mod test {
         TimestampNs, Timestamptz, TimestamptzNs, Uuid,
     };
     use crate::spec::Type::{Primitive, Struct};
+    use crate::spec::decimal_utils::decimal_new;
     use crate::spec::{Datum, NestedField, PrimitiveType, StructType, 
Transform, Type};
     use crate::transform::TransformFunction;
     use crate::transform::test::{TestProjectionFixture, TestTransformFixture};
@@ -831,12 +836,12 @@ mod test {
 
     #[test]
     fn test_decimal_literal() {
-        let input = Datum::decimal(1065).unwrap();
+        let input = Datum::decimal(decimal_new(1065, 0)).unwrap();
         let res = super::Truncate::new(50)
             .transform_literal(&input)
             .unwrap()
             .unwrap();
-        assert_eq!(res, Datum::decimal(1050).unwrap(),);
+        assert_eq!(res, Datum::decimal(decimal_new(1050, 0)).unwrap(),);
     }
 
     #[test]
diff --git a/crates/iceberg/src/writer/file_writer/parquet_writer.rs 
b/crates/iceberg/src/writer/file_writer/parquet_writer.rs
index 8fe40df71..a75c74b3b 100644
--- a/crates/iceberg/src/writer/file_writer/parquet_writer.rs
+++ b/crates/iceberg/src/writer/file_writer/parquet_writer.rs
@@ -622,13 +622,13 @@ mod tests {
     use arrow_select::concat::concat_batches;
     use parquet::arrow::PARQUET_FIELD_ID_META_KEY;
     use parquet::file::statistics::ValueStatistics;
-    use rust_decimal::Decimal;
     use tempfile::TempDir;
     use uuid::Uuid;
 
     use super::*;
     use crate::arrow::schema_to_arrow_schema;
     use crate::io::FileIOBuilder;
+    use crate::spec::decimal_utils::{decimal_mantissa, decimal_new, 
decimal_scale};
     use crate::spec::{PrimitiveLiteral, Struct, *};
     use crate::writer::file_writer::location_generator::{
         DefaultFileNameGenerator, DefaultLocationGenerator, FileNameGenerator, 
LocationGenerator,
@@ -1378,12 +1378,12 @@ mod tests {
             .unwrap();
         assert_eq!(
             data_file.upper_bounds().get(&0),
-            Some(Datum::decimal_with_precision(Decimal::new(22000000000_i64, 
10), 28).unwrap())
+            Some(Datum::decimal_with_precision(decimal_new(22000000000_i64, 
10), 28).unwrap())
                 .as_ref()
         );
         assert_eq!(
             data_file.lower_bounds().get(&0),
-            Some(Datum::decimal_with_precision(Decimal::new(11000000000_i64, 
10), 28).unwrap())
+            Some(Datum::decimal_with_precision(decimal_new(11000000000_i64, 
10), 28).unwrap())
                 .as_ref()
         );
 
@@ -1430,19 +1430,22 @@ mod tests {
             .unwrap();
         assert_eq!(
             data_file.upper_bounds().get(&0),
-            Some(Datum::decimal_with_precision(Decimal::new(-11000000000_i64, 
10), 28).unwrap())
+            Some(Datum::decimal_with_precision(decimal_new(-11000000000_i64, 
10), 28).unwrap())
                 .as_ref()
         );
         assert_eq!(
             data_file.lower_bounds().get(&0),
-            Some(Datum::decimal_with_precision(Decimal::new(-22000000000_i64, 
10), 28).unwrap())
+            Some(Datum::decimal_with_precision(decimal_new(-22000000000_i64, 
10), 28).unwrap())
                 .as_ref()
         );
 
-        // test max and min of rust_decimal
-        let decimal_max = Decimal::MAX;
-        let decimal_min = Decimal::MIN;
-        assert_eq!(decimal_max.scale(), decimal_min.scale());
+        // test 38-digit precision decimal values (Iceberg spec max)
+        // Note: fastnum D128::MAX/MIN have impractical exponents, so we use 
meaningful values
+        use crate::spec::decimal_utils::decimal_from_str_exact;
+        let decimal_max = 
decimal_from_str_exact("99999999999999999999999999999999999999").unwrap();
+        let decimal_min =
+            
decimal_from_str_exact("-99999999999999999999999999999999999999").unwrap();
+        assert_eq!(decimal_scale(&decimal_max), decimal_scale(&decimal_min));
         let schema = Arc::new(
             Schema::builder()
                 .with_fields(vec![
@@ -1451,7 +1454,7 @@ mod tests {
                         "decimal",
                         Type::Primitive(PrimitiveType::Decimal {
                             precision: 38,
-                            scale: decimal_max.scale(),
+                            scale: decimal_scale(&decimal_max),
                         }),
                     )
                     .into(),
@@ -1468,8 +1471,8 @@ mod tests {
             .await?;
         let col0 = Arc::new(
             Decimal128Array::from(vec![
-                Some(decimal_max.mantissa()),
-                Some(decimal_min.mantissa()),
+                Some(decimal_mantissa(&decimal_max)),
+                Some(decimal_mantissa(&decimal_min)),
             ])
             .with_data_type(DataType::Decimal128(38, 0)),
         ) as ArrayRef;


Reply via email to