spg_sqlx/types/
decimal.rs1use spg_embedded::Value as EngineValue;
16
17#[cfg(feature = "bigdecimal")]
18use sqlx_core::decode::Decode;
19#[cfg(feature = "bigdecimal")]
20use sqlx_core::error::BoxDynError;
21#[cfg(feature = "bigdecimal")]
22use sqlx_core::types::Type;
23
24#[cfg(feature = "bigdecimal")]
25use crate::database::Spg;
26#[cfg(feature = "bigdecimal")]
27use crate::type_info::{Kind, SpgTypeInfo};
28#[cfg(feature = "bigdecimal")]
29use crate::value::SpgValueRef;
30
31pub(crate) fn numeric_to_text(scaled: i128, scale: u8) -> String {
38 if scale == 0 {
39 return format!("{scaled}");
40 }
41 let negative = scaled < 0;
42 let mag_str = scaled.unsigned_abs().to_string();
43 let mag_bytes = mag_str.as_bytes();
44 let scale_u = scale as usize;
45 let mut out = String::with_capacity(mag_str.len() + 3);
46 if negative {
47 out.push('-');
48 }
49 if mag_bytes.len() <= scale_u {
50 out.push('0');
51 out.push('.');
52 for _ in mag_bytes.len()..scale_u {
53 out.push('0');
54 }
55 out.push_str(&mag_str);
56 } else {
57 let split = mag_bytes.len() - scale_u;
58 out.push_str(&mag_str[..split]);
59 out.push('.');
60 out.push_str(&mag_str[split..]);
61 }
62 out
63}
64
65pub(crate) fn try_numeric_as_string(value: &EngineValue) -> Option<String> {
68 match value {
69 EngineValue::Numeric { scaled, scale } => Some(numeric_to_text(*scaled, *scale)),
70 _ => None,
71 }
72}
73
74#[cfg(feature = "bigdecimal")]
77mod bd {
78 use super::*;
79 use bigdecimal::BigDecimal;
80 use num_bigint::{BigInt, Sign};
81 use num_traits::ToPrimitive;
82 use sqlx_core::encode::{Encode, IsNull};
83
84 use crate::arguments::SpgArgumentValue;
85
86 impl Type<Spg> for BigDecimal {
87 fn type_info() -> SpgTypeInfo {
88 SpgTypeInfo::of(Kind::Numeric)
89 }
90
91 fn compatible(ty: &SpgTypeInfo) -> bool {
92 matches!(
96 ty.kind(),
97 Kind::Numeric | Kind::Int | Kind::BigInt | Kind::SmallInt
98 )
99 }
100 }
101
102 impl<'q> Encode<'q, Spg> for BigDecimal {
103 fn encode_by_ref(
104 &self,
105 buf: &mut Vec<SpgArgumentValue<'q>>,
106 ) -> Result<IsNull, BoxDynError> {
107 let (scaled, scale) = bigdecimal_to_scaled(self)?;
108 buf.push(SpgArgumentValue {
109 value: EngineValue::Numeric { scaled, scale },
110 type_info: Some(SpgTypeInfo::of(Kind::Numeric)),
111 _phantom: core::marker::PhantomData,
112 });
113 Ok(IsNull::No)
114 }
115 }
116
117 impl<'r> Decode<'r, Spg> for BigDecimal {
118 fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
119 match value.engine() {
120 EngineValue::Numeric { scaled, scale } => Ok(scaled_to_bigdecimal(*scaled, *scale)),
121 EngineValue::Int(n) => Ok(BigDecimal::from(*n)),
125 EngineValue::BigInt(n) => Ok(BigDecimal::from(*n)),
126 EngineValue::SmallInt(n) => Ok(BigDecimal::from(i32::from(*n))),
127 other => Err(format!("cannot decode {other:?} as BigDecimal / NUMERIC").into()),
128 }
129 }
130 }
131
132 fn bigdecimal_to_scaled(d: &BigDecimal) -> Result<(i128, u8), BoxDynError> {
137 let (mantissa, exp) = d.as_bigint_and_exponent();
143 let (scaled, scale) = if exp < 0 {
144 let factor = BigInt::from(10u8).pow((-exp) as u32);
145 let folded = mantissa * factor;
146 (folded, 0u8)
147 } else if exp > i64::from(u8::MAX) {
148 return Err(format!(
149 "BigDecimal scale {exp} exceeds SPG NUMERIC ceiling ({})",
150 u8::MAX
151 )
152 .into());
153 } else {
154 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
155 let s = exp as u8;
156 (mantissa, s)
157 };
158 let scaled_i128 = scaled.to_i128().ok_or_else(|| {
159 format!(
160 "BigDecimal mantissa {scaled} overflows i128 — SPG NUMERIC tops out at precision 38"
161 )
162 })?;
163 Ok((scaled_i128, scale))
164 }
165
166 fn scaled_to_bigdecimal(scaled: i128, scale: u8) -> BigDecimal {
167 let sign = if scaled < 0 { Sign::Minus } else { Sign::Plus };
168 let magnitude: u128 = scaled.unsigned_abs();
169 let mantissa = BigInt::from_biguint(sign, num_bigint::BigUint::from(magnitude));
170 BigDecimal::new(mantissa, i64::from(scale))
171 }
172}