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#[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 pub const MAX_WIDTH: u8 = 38;
86
87 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 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 #[inline]
132 pub const fn width(self) -> u8 {
133 self.width
134 }
135
136 #[inline]
138 pub const fn scale(self) -> u8 {
139 self.scale
140 }
141
142 #[inline]
144 pub const fn value(self) -> i128 {
145 self.value
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum DecimalError {
152 InvalidWidth {
154 width: u8,
156 max: u8,
158 },
159 ScaleExceedsWidth {
161 scale: u8,
163 width: u8,
165 },
166 ValueExceedsWidth {
168 digits: u8,
170 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 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}