Skip to main content

duckdb/types/
decimal.rs

1use std::{
2    error, fmt,
3    hash::{Hash, Hasher},
4};
5
6use crate::{
7    Result, ToSql, ffi,
8    types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, Value, ValueRef},
9};
10
11use super::{TimeUnit, to_duckdb_hugeint};
12
13#[cfg(feature = "rust_decimal")]
14use rust_decimal::prelude::FromPrimitive as _;
15
16/// A DuckDB `DECIMAL(width, scale)` value.
17///
18/// `value` stores the scaled integer payload. For example,
19/// `123.45::DECIMAL(9, 2)` is represented as
20/// `Decimal::new(9, 2, 12345)?`.
21///
22/// The declared width is preserved when reading and writing DuckDB `DECIMAL`
23/// columns. Values converted from non-decimal sources, such as integers or
24/// `rust_decimal` values, infer the smallest width that can represent the
25/// scaled payload. Width is not part of equality or hashing: values with the
26/// same `scale` and scaled integer payload compare equal even if they arrived
27/// through paths with different declared widths. Use [`Decimal::width`] when
28/// the declared width matters; `Eq` and `Hash` do not preserve it.
29///
30/// Scale is part of identity: `DECIMAL(2, 1)` with payload `12` and
31/// `DECIMAL(3, 2)` with payload `120` compare different. `Decimal`
32/// intentionally does not implement `Ord` or `PartialOrd`; sort in SQL or
33/// compare an explicitly normalized representation when ordering is needed.
34#[derive(Copy, Clone, Debug)]
35pub struct Decimal {
36    width: u8,
37    scale: u8,
38    value: i128,
39}
40
41impl PartialEq for Decimal {
42    fn eq(&self, other: &Self) -> bool {
43        self.scale == other.scale && self.value == other.value
44    }
45}
46
47impl Eq for Decimal {}
48
49impl Hash for Decimal {
50    fn hash<H: Hasher>(&self, state: &mut H) {
51        self.scale.hash(state);
52        self.value.hash(state);
53    }
54}
55
56impl fmt::Display for Decimal {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        let scale = usize::from(self.scale);
59        let raw = self.value.unsigned_abs().to_string();
60
61        if scale == 0 {
62            return f.pad_integral(!self.value.is_negative(), "", &raw);
63        }
64
65        let mut magnitude = String::new();
66        if raw.len() <= scale {
67            magnitude.push_str("0.");
68            for _ in 0..(scale - raw.len()) {
69                magnitude.push('0');
70            }
71            magnitude.push_str(&raw);
72        } else {
73            let point = raw.len() - scale;
74            magnitude.push_str(&raw[..point]);
75            magnitude.push('.');
76            magnitude.push_str(&raw[point..]);
77        }
78
79        f.pad_integral(!self.value.is_negative(), "", &magnitude)
80    }
81}
82
83impl Decimal {
84    /// DuckDB's maximum `DECIMAL` width.
85    pub const MAX_WIDTH: u8 = 38;
86
87    /// Builds a valid `DECIMAL(width, scale)` wrapper.
88    ///
89    /// Returns [`DecimalError`] when `width` is outside DuckDB's `1..=38`
90    /// range, `scale` is larger than `width`, or `value` has more digits than
91    /// `width` allows.
92    pub fn new(width: u8, scale: u8, value: i128) -> std::result::Result<Self, DecimalError> {
93        if width == 0 || width > Self::MAX_WIDTH {
94            return Err(DecimalError::InvalidWidth {
95                width,
96                max: Self::MAX_WIDTH,
97            });
98        }
99        if scale > width {
100            return Err(DecimalError::ScaleExceedsWidth { scale, width });
101        }
102        let digits = decimal_digits(value);
103        if digits > width {
104            return Err(DecimalError::ValueExceedsWidth { digits, width });
105        }
106        Ok(Self { width, scale, value })
107    }
108
109    pub(crate) fn validate_signed_scale(width: u8, scale: i8) -> std::result::Result<u8, String> {
110        let scale = u8::try_from(scale).map_err(|_| format!("negative decimal scale is not supported: {scale}"))?;
111        Self::new(width, scale, 0).map_err(|err| err.to_string())?;
112        Ok(scale)
113    }
114
115    /// Builds a decimal value from trusted DuckDB output.
116    ///
117    /// The caller must ensure `width`, `scale`, and `value` are a valid DuckDB
118    /// decimal triple.
119    pub(crate) fn from_chunk(width: u8, scale: u8, value: i128) -> Self {
120        debug_assert!(
121            Self::new(width, scale, value).is_ok(),
122            "DuckDB produced an invalid decimal triple"
123        );
124        Self { width, scale, value }
125    }
126
127    /// Returns the declared precision for DuckDB `DECIMAL` values.
128    ///
129    /// Values converted from non-decimal sources infer the smallest width that
130    /// can represent the scaled payload.
131    #[inline]
132    pub const fn width(self) -> u8 {
133        self.width
134    }
135
136    /// Returns the number of fractional digits.
137    #[inline]
138    pub const fn scale(self) -> u8 {
139        self.scale
140    }
141
142    /// Returns the scaled integer payload.
143    #[inline]
144    pub const fn value(self) -> i128 {
145        self.value
146    }
147}
148
149/// Error returned when constructing an invalid [`Decimal`].
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum DecimalError {
152    /// The width is outside DuckDB's supported range.
153    InvalidWidth {
154        /// The requested width.
155        width: u8,
156        /// DuckDB's maximum decimal width.
157        max: u8,
158    },
159    /// The scale is larger than the width.
160    ScaleExceedsWidth {
161        /// The requested scale.
162        scale: u8,
163        /// The requested width.
164        width: u8,
165    },
166    /// The scaled integer payload has more digits than the width allows.
167    ValueExceedsWidth {
168        /// Number of digits in the payload.
169        digits: u8,
170        /// The requested width.
171        width: u8,
172    },
173}
174
175impl fmt::Display for DecimalError {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        match *self {
178            Self::InvalidWidth { width, max } => {
179                write!(f, "decimal width {width} is outside DuckDB's 1..={max} range")
180            }
181            Self::ScaleExceedsWidth { scale, width } => {
182                write!(f, "decimal scale {scale} exceeds width {width}")
183            }
184            Self::ValueExceedsWidth { digits, width } => {
185                write!(f, "decimal value has {digits} digits, exceeding width {width}")
186            }
187        }
188    }
189}
190
191impl error::Error for DecimalError {}
192
193impl From<DecimalError> for crate::Error {
194    fn from(err: DecimalError) -> Self {
195        Self::ToSqlConversionFailure(err.into())
196    }
197}
198
199pub(crate) fn to_duckdb_decimal(decimal: Decimal) -> ffi::duckdb_decimal {
200    ffi::duckdb_decimal {
201        width: decimal.width,
202        scale: decimal.scale,
203        value: to_duckdb_hugeint(decimal.value),
204    }
205}
206
207fn decimal_digits_unsigned(value: u128) -> u8 {
208    value.checked_ilog10().map_or(1, |digits| digits as u8 + 1)
209}
210
211fn decimal_digits(value: i128) -> u8 {
212    decimal_digits_unsigned(value.unsigned_abs())
213}
214
215fn integer_decimal(value: i128) -> FromSqlResult<Decimal> {
216    let digits = decimal_digits(value);
217    if digits > Decimal::MAX_WIDTH {
218        return Err(FromSqlError::OutOfRange(value));
219    }
220    Ok(Decimal::from_chunk(digits, 0, value))
221}
222
223fn unsigned_integer_decimal(value: u128) -> FromSqlResult<Decimal> {
224    let digits = decimal_digits_unsigned(value);
225    if digits > Decimal::MAX_WIDTH {
226        return Err(FromSqlError::OutOfRangeUnsigned(value));
227    }
228    Ok(Decimal::from_chunk(digits, 0, value as i128))
229}
230
231impl ToSql for Decimal {
232    fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
233        Ok(ToSqlOutput::Owned(Value::Decimal(*self)))
234    }
235}
236
237impl FromSql for Decimal {
238    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
239        match value {
240            ValueRef::Decimal(decimal) => Ok(decimal),
241            ValueRef::TinyInt(i) => integer_decimal(i as i128),
242            ValueRef::SmallInt(i) => integer_decimal(i as i128),
243            ValueRef::Int(i) => integer_decimal(i as i128),
244            ValueRef::BigInt(i) => integer_decimal(i as i128),
245            ValueRef::HugeInt(i) => integer_decimal(i),
246            ValueRef::UHugeInt(i) => unsigned_integer_decimal(i),
247            ValueRef::UTinyInt(i) => integer_decimal(i as i128),
248            ValueRef::USmallInt(i) => integer_decimal(i as i128),
249            ValueRef::UInt(i) => integer_decimal(i as i128),
250            ValueRef::UBigInt(i) => integer_decimal(i as i128),
251            ValueRef::Timestamp(_, i) => integer_decimal(i as i128),
252            ValueRef::Date32(i) => integer_decimal(i as i128),
253            ValueRef::Time64(TimeUnit::Microsecond, i) => integer_decimal(i as i128),
254            _ => Err(FromSqlError::InvalidType),
255        }
256    }
257}
258
259#[cfg(feature = "rust_decimal")]
260impl From<rust_decimal::Decimal> for Decimal {
261    fn from(decimal: rust_decimal::Decimal) -> Self {
262        let scale = decimal.scale() as u8;
263        let mantissa = decimal.mantissa();
264        let width = decimal_digits(mantissa).max(scale);
265
266        // rust_decimal's scale is <= 28 and its mantissa fits in 96 bits, so
267        // every valid rust_decimal value fits DuckDB's DECIMAL(38, scale).
268        Self::new(width, scale, mantissa).expect("rust_decimal values fit DuckDB DECIMAL")
269    }
270}
271
272#[cfg(feature = "rust_decimal")]
273impl TryFrom<Decimal> for rust_decimal::Decimal {
274    type Error = rust_decimal::Error;
275
276    fn try_from(decimal: Decimal) -> std::result::Result<Self, Self::Error> {
277        Self::try_from_i128_with_scale(decimal.value(), u32::from(decimal.scale()))
278    }
279}
280
281#[cfg(feature = "rust_decimal")]
282impl ToSql for rust_decimal::Decimal {
283    fn to_sql(&self) -> Result<ToSqlOutput<'_>> {
284        Ok(ToSqlOutput::Owned(Value::Decimal((*self).into())))
285    }
286}
287
288#[cfg(feature = "rust_decimal")]
289fn invalid_decimal_float(kind: &str, value: impl std::fmt::Debug) -> FromSqlError {
290    FromSqlError::Other(Box::new(std::io::Error::new(
291        std::io::ErrorKind::InvalidData,
292        format!("{kind} value {value:?} cannot be represented as Decimal"),
293    )))
294}
295
296#[cfg(feature = "rust_decimal")]
297fn decimal_from_f32(value: f32) -> FromSqlResult<rust_decimal::Decimal> {
298    rust_decimal::Decimal::from_f32(value).ok_or_else(|| invalid_decimal_float("FLOAT", value))
299}
300
301#[cfg(feature = "rust_decimal")]
302fn decimal_from_f64(value: f64) -> FromSqlResult<rust_decimal::Decimal> {
303    rust_decimal::Decimal::from_f64(value).ok_or_else(|| invalid_decimal_float("DOUBLE", value))
304}
305
306#[cfg(feature = "rust_decimal")]
307impl FromSql for rust_decimal::Decimal {
308    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
309        match value {
310            ValueRef::Decimal(decimal) => decimal.try_into().map_err(|err| FromSqlError::Other(Box::new(err))),
311            ValueRef::TinyInt(i) => rust_decimal::Decimal::from_i8(i).ok_or(FromSqlError::OutOfRange(i as i128)),
312            ValueRef::SmallInt(i) => rust_decimal::Decimal::from_i16(i).ok_or(FromSqlError::OutOfRange(i as i128)),
313            ValueRef::Int(i) => rust_decimal::Decimal::from_i32(i).ok_or(FromSqlError::OutOfRange(i as i128)),
314            ValueRef::BigInt(i) => rust_decimal::Decimal::from_i64(i).ok_or(FromSqlError::OutOfRange(i as i128)),
315            ValueRef::HugeInt(i) => rust_decimal::Decimal::from_i128(i).ok_or(FromSqlError::OutOfRange(i)),
316            ValueRef::UHugeInt(i) => rust_decimal::Decimal::from_u128(i).ok_or(FromSqlError::OutOfRangeUnsigned(i)),
317            ValueRef::UTinyInt(i) => rust_decimal::Decimal::from_u8(i).ok_or(FromSqlError::OutOfRange(i as i128)),
318            ValueRef::USmallInt(i) => rust_decimal::Decimal::from_u16(i).ok_or(FromSqlError::OutOfRange(i as i128)),
319            ValueRef::UInt(i) => rust_decimal::Decimal::from_u32(i).ok_or(FromSqlError::OutOfRange(i as i128)),
320            ValueRef::UBigInt(i) => rust_decimal::Decimal::from_u64(i).ok_or(FromSqlError::OutOfRange(i as i128)),
321            ValueRef::Float(f) => decimal_from_f32(f),
322            ValueRef::Double(d) => decimal_from_f64(d),
323            ValueRef::Timestamp(_, i) => rust_decimal::Decimal::from_i64(i).ok_or(FromSqlError::OutOfRange(i as i128)),
324            ValueRef::Date32(i) => rust_decimal::Decimal::from_i32(i).ok_or(FromSqlError::OutOfRange(i as i128)),
325            ValueRef::Time64(TimeUnit::Microsecond, i) => {
326                rust_decimal::Decimal::from_i64(i).ok_or(FromSqlError::OutOfRange(i as i128))
327            }
328            ValueRef::Text(_) => {
329                let s = value.as_str()?;
330                match s.parse::<rust_decimal::Decimal>() {
331                    Ok(decimal) => Ok(decimal),
332                    Err(_) => match s.parse::<i128>() {
333                        Ok(i) => Err(FromSqlError::OutOfRange(i)),
334                        Err(_) => Err(FromSqlError::InvalidType),
335                    },
336                }
337            }
338            _ => Err(FromSqlError::InvalidType),
339        }
340    }
341}
342
343#[cfg(test)]
344mod test {
345    use super::*;
346    use crate::{Connection, Error, Result};
347
348    #[test]
349    fn decimal_new_accepts_valid_values() -> Result<()> {
350        let decimal = Decimal::new(9, 2, 12345)?;
351
352        assert_eq!(decimal.width(), 9);
353        assert_eq!(decimal.scale(), 2);
354        assert_eq!(decimal.value(), 12345);
355
356        Ok(())
357    }
358
359    #[test]
360    fn decimal_identity_ignores_width() {
361        use std::collections::HashSet;
362
363        let declared = Decimal::new(38, 0, 3).unwrap();
364        let inferred = Decimal::new(1, 0, 3).unwrap();
365
366        assert_eq!(declared.width(), 38);
367        assert_eq!(inferred.width(), 1);
368        assert_eq!(declared, inferred);
369        assert_eq!(Value::Decimal(declared), Value::Decimal(inferred));
370
371        let mut values = HashSet::new();
372        values.insert(declared);
373        values.insert(inferred);
374
375        assert_eq!(values.len(), 1);
376    }
377
378    #[test]
379    fn decimal_identity_keeps_scale() {
380        use std::collections::HashSet;
381
382        let integer = Decimal::new(2, 0, 10).unwrap();
383        let fractional = Decimal::new(2, 1, 10).unwrap();
384
385        assert_ne!(integer, fractional);
386
387        let mut values = HashSet::new();
388        values.insert(integer);
389        values.insert(fractional);
390
391        assert_eq!(values.len(), 2);
392    }
393
394    #[test]
395    fn decimal_display_renders_scaled_value() {
396        assert_eq!(Decimal::new(9, 2, 12345).unwrap().to_string(), "123.45");
397        assert_eq!(Decimal::new(3, 3, 1).unwrap().to_string(), "0.001");
398        assert_eq!(Decimal::new(4, 3, -1).unwrap().to_string(), "-0.001");
399        assert_eq!(Decimal::new(3, 0, -123).unwrap().to_string(), "-123");
400        assert_eq!(Decimal::new(5, 2, -12345).unwrap().to_string(), "-123.45");
401        assert_eq!(Decimal::new(2, 2, 99).unwrap().to_string(), "0.99");
402        assert_eq!(Decimal::new(1, 0, 0).unwrap().to_string(), "0");
403        assert_eq!(format!("{:>10}", Decimal::new(9, 2, 12345).unwrap()), "    123.45");
404        assert_eq!(format!("{:+}", Decimal::new(9, 2, 12345).unwrap()), "+123.45");
405        assert_eq!(format!("{:08}", Decimal::new(9, 2, 12345).unwrap()), "00123.45");
406    }
407
408    #[test]
409    fn decimal_new_rejects_invalid_values() {
410        assert!(Decimal::new(3, 3, 1).is_ok());
411        assert_eq!(
412            Decimal::new(0, 0, 0).unwrap_err(),
413            DecimalError::InvalidWidth {
414                width: 0,
415                max: Decimal::MAX_WIDTH,
416            }
417        );
418        assert_eq!(
419            Decimal::new(39, 0, 0).unwrap_err(),
420            DecimalError::InvalidWidth {
421                width: 39,
422                max: Decimal::MAX_WIDTH,
423            }
424        );
425        assert_eq!(
426            Decimal::new(9, 10, 0).unwrap_err(),
427            DecimalError::ScaleExceedsWidth { scale: 10, width: 9 }
428        );
429        assert_eq!(
430            Decimal::new(2, 1, 100).unwrap_err(),
431            DecimalError::ValueExceedsWidth { digits: 3, width: 2 }
432        );
433        assert_eq!(
434            Decimal::new(38, 0, i128::MAX).unwrap_err(),
435            DecimalError::ValueExceedsWidth { digits: 39, width: 38 }
436        );
437    }
438
439    #[test]
440    fn decimal_signed_scale_helper_validates_arrow_metadata() {
441        assert_eq!(Decimal::validate_signed_scale(9, 2).unwrap(), 2);
442        assert_eq!(
443            Decimal::validate_signed_scale(9, -1).unwrap_err(),
444            "negative decimal scale is not supported: -1"
445        );
446        assert_eq!(
447            Decimal::validate_signed_scale(9, 10).unwrap_err(),
448            "decimal scale 10 exceeds width 9"
449        );
450    }
451
452    #[test]
453    fn test_to_duckdb_decimal_large_negative_upper_bits() -> Result<()> {
454        let decimal = Decimal::new(28, 10, -7922816251426433759354395033_i128)?;
455        let duckdb_decimal = to_duckdb_decimal(decimal);
456
457        assert_eq!(duckdb_decimal.width, 28);
458        assert_eq!(duckdb_decimal.scale, 10);
459        assert_eq!(duckdb_decimal.value.lower, 7_378_697_629_483_820_647);
460        assert_eq!(duckdb_decimal.value.upper, -429_496_730);
461        Ok(())
462    }
463
464    #[test]
465    fn test_to_duckdb_decimal_zero_and_boundaries() -> Result<()> {
466        let zero = to_duckdb_decimal(Decimal::new(1, 0, 0)?);
467        assert_eq!(zero.width, 1);
468        assert_eq!(zero.scale, 0);
469        assert_eq!(zero.value.lower, 0);
470        assert_eq!(zero.value.upper, 0);
471
472        let max = to_duckdb_decimal(Decimal::new(
473            38,
474            0,
475            99_999_999_999_999_999_999_999_999_999_999_999_999_i128,
476        )?);
477        assert_eq!(max.width, 38);
478        assert_eq!(max.scale, 0);
479        assert_eq!(max.value.lower, 687_399_551_400_673_279);
480        assert_eq!(max.value.upper, 5_421_010_862_427_522_170);
481        Ok(())
482    }
483
484    #[test]
485    fn test_from_sql_uhugeint_overflow_is_out_of_range() {
486        let err = Decimal::column_result(ValueRef::UHugeInt(u128::MAX)).unwrap_err();
487        match err {
488            FromSqlError::OutOfRangeUnsigned(value) => assert_eq!(value, u128::MAX),
489            _ => panic!("expected OutOfRangeUnsigned, got {err}"),
490        }
491    }
492
493    #[test]
494    fn test_from_sql_integer_decimal_overflow_is_out_of_range() {
495        let err = Decimal::column_result(ValueRef::HugeInt(i128::MAX)).unwrap_err();
496        match err {
497            FromSqlError::OutOfRange(value) => assert_eq!(value, i128::MAX),
498            _ => panic!("expected OutOfRange, got {err}"),
499        }
500
501        let value = 10_u128.pow(38);
502        let err = Decimal::column_result(ValueRef::UHugeInt(value)).unwrap_err();
503        match err {
504            FromSqlError::OutOfRangeUnsigned(err_value) => assert_eq!(err_value, value),
505            _ => panic!("expected OutOfRangeUnsigned, got {err}"),
506        }
507    }
508
509    #[test]
510    fn test_from_sql_time_values_as_decimal() -> Result<()> {
511        assert_eq!(
512            Decimal::column_result(ValueRef::Timestamp(TimeUnit::Second, -123))?,
513            Decimal::new(3, 0, -123)?
514        );
515        assert_eq!(Decimal::column_result(ValueRef::Date32(42))?, Decimal::new(2, 0, 42)?);
516        assert_eq!(
517            Decimal::column_result(ValueRef::Time64(TimeUnit::Microsecond, 1_234_567))?,
518            Decimal::new(7, 0, 1_234_567)?
519        );
520        Ok(())
521    }
522
523    #[test]
524    fn test_native_decimal_rejects_float_double_and_text() {
525        assert_eq!(
526            Decimal::column_result(ValueRef::Float(1.25)),
527            Err(FromSqlError::InvalidType)
528        );
529        assert_eq!(
530            Decimal::column_result(ValueRef::Double(1.25)),
531            Err(FromSqlError::InvalidType)
532        );
533        assert_eq!(
534            Decimal::column_result(ValueRef::Text(b"123.456")),
535            Err(FromSqlError::InvalidType)
536        );
537    }
538
539    #[test]
540    fn test_read_uhugeint_as_decimal() -> Result<()> {
541        const U128_MAX_SQL: &str = "340282366920938463463374607431768211455";
542
543        let db = Connection::open_in_memory()?;
544
545        let value: Decimal = db.query_row("SELECT (5)::UHUGEINT", [], |row| row.get(0))?;
546        assert_eq!(value, Decimal::new(1, 0, 5)?);
547
548        let err = db
549            .query_row(&format!("SELECT ({U128_MAX_SQL})::UHUGEINT"), [], |row| {
550                row.get::<_, Decimal>(0)
551            })
552            .unwrap_err();
553
554        match err {
555            Error::UnsignedIntegralValueOutOfRange(0, value) => assert_eq!(value, u128::MAX),
556            other => panic!("expected unsigned out-of-range error, got {other:?}"),
557        }
558
559        Ok(())
560    }
561
562    #[cfg(feature = "rust_decimal")]
563    #[test]
564    fn test_rust_decimal_converts_to_native_decimal() {
565        let decimal = rust_decimal::Decimal::from_i128_with_scale(12345, 2);
566        let native = Decimal::from(decimal);
567
568        assert_eq!(native, Decimal::new(5, 2, 12345).unwrap());
569
570        let decimal = rust_decimal::Decimal::from_i128_with_scale(1, 3);
571        let native = Decimal::from(decimal);
572
573        assert_eq!(native, Decimal::new(3, 3, 1).unwrap());
574    }
575
576    #[cfg(feature = "rust_decimal")]
577    #[test]
578    fn test_rust_decimal_round_trips_through_decimal_column() -> Result<()> {
579        let db = Connection::open_in_memory()?;
580        let value = rust_decimal::Decimal::from_i128_with_scale(12345, 2);
581
582        let read: rust_decimal::Decimal = db.query_row("SELECT ?::DECIMAL(5,2)", [value], |row| row.get(0))?;
583
584        assert_eq!(read, value);
585        Ok(())
586    }
587
588    #[cfg(feature = "rust_decimal")]
589    #[test]
590    fn test_rust_decimal_reads_representable_wide_duckdb_decimal() -> Result<()> {
591        let db = Connection::open_in_memory()?;
592
593        let read: rust_decimal::Decimal = db.query_row("SELECT 1.23::DECIMAL(38,2)", [], |row| row.get(0))?;
594
595        assert_eq!(read, rust_decimal::Decimal::from_i128_with_scale(123, 2));
596        Ok(())
597    }
598
599    #[cfg(feature = "rust_decimal")]
600    #[test]
601    fn test_rust_decimal_compat_reads_float_double_and_text() {
602        let float = rust_decimal::Decimal::column_result(ValueRef::Float(1.25)).unwrap();
603        assert_eq!(float, rust_decimal::Decimal::from_f32(1.25).unwrap());
604
605        let double = rust_decimal::Decimal::column_result(ValueRef::Double(1.25)).unwrap();
606        assert_eq!(double, rust_decimal::Decimal::from_f64(1.25).unwrap());
607
608        let text = rust_decimal::Decimal::column_result(ValueRef::Text(b"123.456")).unwrap();
609        assert_eq!(text, "123.456".parse::<rust_decimal::Decimal>().unwrap());
610    }
611
612    #[cfg(feature = "rust_decimal")]
613    #[test]
614    fn test_rust_decimal_errors_on_oversized_duckdb_decimal() -> Result<()> {
615        let db = Connection::open_in_memory()?;
616
617        let err = db
618            .query_row("SELECT 123456789012345678901234567890.12::DECIMAL(38,2)", [], |row| {
619                row.get::<_, rust_decimal::Decimal>(0)
620            })
621            .unwrap_err();
622
623        match err {
624            Error::FromSqlConversionFailure(0, crate::types::Type::Decimal, _) => {}
625            other => panic!("expected Decimal conversion failure, got {other:?}"),
626        }
627
628        Ok(())
629    }
630
631    #[cfg(feature = "rust_decimal")]
632    #[test]
633    fn test_rust_decimal_unrepresentable_float_errors_are_descriptive() {
634        let err = rust_decimal::Decimal::column_result(ValueRef::Float(f32::NAN)).unwrap_err();
635        assert_eq!(err.to_string(), "FLOAT value NaN cannot be represented as Decimal");
636
637        let err = rust_decimal::Decimal::column_result(ValueRef::Double(f64::INFINITY)).unwrap_err();
638        assert_eq!(err.to_string(), "DOUBLE value inf cannot be represented as Decimal");
639    }
640}