use spg_embedded::Value as EngineValue;
#[cfg(feature = "bigdecimal")]
use sqlx_core::decode::Decode;
#[cfg(feature = "bigdecimal")]
use sqlx_core::error::BoxDynError;
#[cfg(feature = "bigdecimal")]
use sqlx_core::types::Type;
#[cfg(feature = "bigdecimal")]
use crate::database::Spg;
#[cfg(feature = "bigdecimal")]
use crate::type_info::{Kind, SpgTypeInfo};
#[cfg(feature = "bigdecimal")]
use crate::value::SpgValueRef;
pub(crate) fn numeric_to_text(scaled: i128, scale: u8) -> String {
if scale == 0 {
return format!("{scaled}");
}
let negative = scaled < 0;
let mag_str = scaled.unsigned_abs().to_string();
let mag_bytes = mag_str.as_bytes();
let scale_u = scale as usize;
let mut out = String::with_capacity(mag_str.len() + 3);
if negative {
out.push('-');
}
if mag_bytes.len() <= scale_u {
out.push('0');
out.push('.');
for _ in mag_bytes.len()..scale_u {
out.push('0');
}
out.push_str(&mag_str);
} else {
let split = mag_bytes.len() - scale_u;
out.push_str(&mag_str[..split]);
out.push('.');
out.push_str(&mag_str[split..]);
}
out
}
pub(crate) fn try_numeric_as_string(value: &EngineValue) -> Option<String> {
match value {
EngineValue::Numeric { scaled, scale } => Some(numeric_to_text(*scaled, *scale)),
_ => None,
}
}
#[cfg(feature = "bigdecimal")]
mod bd {
use super::*;
use bigdecimal::BigDecimal;
use num_bigint::{BigInt, Sign};
use num_traits::ToPrimitive;
use sqlx_core::encode::{Encode, IsNull};
use crate::arguments::SpgArgumentValue;
impl Type<Spg> for BigDecimal {
fn type_info() -> SpgTypeInfo {
SpgTypeInfo::of(Kind::Numeric)
}
fn compatible(ty: &SpgTypeInfo) -> bool {
matches!(
ty.kind(),
Kind::Numeric | Kind::Int | Kind::BigInt | Kind::SmallInt
)
}
}
impl<'q> Encode<'q, Spg> for BigDecimal {
fn encode_by_ref(
&self,
buf: &mut Vec<SpgArgumentValue<'q>>,
) -> Result<IsNull, BoxDynError> {
let (scaled, scale) = bigdecimal_to_scaled(self)?;
buf.push(SpgArgumentValue {
value: EngineValue::Numeric { scaled, scale },
type_info: Some(SpgTypeInfo::of(Kind::Numeric)),
_phantom: core::marker::PhantomData,
});
Ok(IsNull::No)
}
}
impl<'r> Decode<'r, Spg> for BigDecimal {
fn decode(value: SpgValueRef<'r>) -> Result<Self, BoxDynError> {
match value.engine() {
EngineValue::Numeric { scaled, scale } => Ok(scaled_to_bigdecimal(*scaled, *scale)),
EngineValue::Int(n) => Ok(BigDecimal::from(*n)),
EngineValue::BigInt(n) => Ok(BigDecimal::from(*n)),
EngineValue::SmallInt(n) => Ok(BigDecimal::from(i32::from(*n))),
other => Err(format!("cannot decode {other:?} as BigDecimal / NUMERIC").into()),
}
}
}
fn bigdecimal_to_scaled(d: &BigDecimal) -> Result<(i128, u8), BoxDynError> {
let (mantissa, exp) = d.as_bigint_and_exponent();
let (scaled, scale) = if exp < 0 {
let factor = BigInt::from(10u8).pow((-exp) as u32);
let folded = mantissa * factor;
(folded, 0u8)
} else if exp > i64::from(u8::MAX) {
return Err(format!(
"BigDecimal scale {exp} exceeds SPG NUMERIC ceiling ({})",
u8::MAX
)
.into());
} else {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let s = exp as u8;
(mantissa, s)
};
let scaled_i128 = scaled.to_i128().ok_or_else(|| {
format!(
"BigDecimal mantissa {scaled} overflows i128 — SPG NUMERIC tops out at precision 38"
)
})?;
Ok((scaled_i128, scale))
}
fn scaled_to_bigdecimal(scaled: i128, scale: u8) -> BigDecimal {
let sign = if scaled < 0 { Sign::Minus } else { Sign::Plus };
let magnitude: u128 = scaled.unsigned_abs();
let mantissa = BigInt::from_biguint(sign, num_bigint::BigUint::from(magnitude));
BigDecimal::new(mantissa, i64::from(scale))
}
}