diff --git a/postgres-protocol/Cargo.toml b/postgres-protocol/Cargo.toml index 9351ea14f..cfaea35e0 100644 --- a/postgres-protocol/Cargo.toml +++ b/postgres-protocol/Cargo.toml @@ -24,3 +24,6 @@ rand = "0.9" sha2 = "0.10" stringprep = "0.1" getrandom = { version = "0.3", optional = true } + +[dev-dependencies] +proptest = "1.6.0" diff --git a/postgres-protocol/src/types/mod.rs b/postgres-protocol/src/types/mod.rs index 37dc793b1..b01079287 100644 --- a/postgres-protocol/src/types/mod.rs +++ b/postgres-protocol/src/types/mod.rs @@ -10,6 +10,9 @@ use std::str; use crate::{write_nullable, FromUsize, IsNull, Lsn, Oid}; +pub use numeric::{numeric_from_sql, numeric_to_sql, Numeric, NumericSign}; + +mod numeric; #[cfg(test)] mod test; diff --git a/postgres-protocol/src/types/numeric.rs b/postgres-protocol/src/types/numeric.rs new file mode 100644 index 000000000..ba371d623 --- /dev/null +++ b/postgres-protocol/src/types/numeric.rs @@ -0,0 +1,866 @@ +//! Conversions to and from Postgres's binary format for the numeric type. +use byteorder::{BigEndian, ReadBytesExt}; +use bytes::{BufMut, BytesMut}; +use std::boxed::Box as StdBox; +use std::collections::VecDeque; +use std::error::Error; +use std::str::{self, FromStr}; + +/// Serializes a `NUMERIC` value. +#[inline] +pub fn numeric_to_sql(v: Numeric, buf: &mut BytesMut) { + let num_digits = v.digits.len() as u16; + buf.put_u16(num_digits); + buf.put_i16(v.weight); + buf.put_u16(v.sign.into_u16()); + buf.put_u16(v.scale); + + for digit in v.digits { + buf.put_i16(digit); + } +} + +/// Deserializes a `NUMERIC` value. +#[inline] +pub fn numeric_from_sql(mut buf: &[u8]) -> Result> { + let num_digits = buf.read_u16::()?; + let mut digits = Vec::with_capacity(num_digits.into()); + + let weight = buf.read_i16::()?; + let sign = NumericSign::try_from_u16(buf.read_u16::()?)?; + + let scale = buf.read_u16::()?; + + for _ in 0..num_digits { + digits.push(buf.read_i16::()?); + } + + Ok(Numeric { + sign, + scale, + weight, + digits, + }) +} + +/// A Posgres numeric +#[derive(Debug, PartialEq, Eq)] +pub struct Numeric { + sign: NumericSign, + scale: u16, + weight: i16, + digits: Vec, +} + +impl Numeric { + /// Returns the number of digits. + #[inline] + pub fn num_digits(&self) -> usize { + self.digits.len() + } + + /// Returns the weight of the numeric value. + #[inline] + pub fn weight(&self) -> i16 { + self.weight + } + + /// Returns the scale of the numeric value. + #[inline] + pub fn scale(&self) -> u16 { + self.scale + } + + /// Returns the sign of the numeric value. + #[inline] + pub fn sign(&self) -> NumericSign { + self.sign + } + + fn nan() -> Self { + Self { + sign: NumericSign::NaN, + scale: 0, + weight: 0, + digits: vec![], + } + } + + fn infinity() -> Self { + Self { + sign: NumericSign::PositiveInfinity, + scale: 0, + weight: 0, + digits: vec![], + } + } + + fn negative_infinity() -> Self { + Self { + sign: NumericSign::NegativeInfinity, + scale: 0, + weight: 0, + digits: vec![], + } + } +} + +impl std::fmt::Display for Numeric { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.sign { + NumericSign::Positive | NumericSign::Negative => { + if self.sign == NumericSign::Negative { + write!(f, "-")?; + } + if self.weight >= 0 { + for i in 0..self.weight + 1 { + let digit = self.digits.get(i as usize).unwrap_or(&0); + if i == 0 { + write!(f, "{digit}")?; + } else { + write!(f, "{digit:0>4}")?; + } + } + } + + let mut weight = self.weight; + let mut scale = self.scale; + if weight < 0 && scale > 0 { + write!(f, "0.")?; + + while weight < -1 { + write!(f, "0000")?; + weight += 1; + scale -= 4; + } + } else if scale > 0 { + write!(f, ".")?; + } + if scale > 0 { + let first_decimal_index = if weight < 0 { 0 } else { weight as usize + 1 }; + + let mut decimals = scale; + for i in first_decimal_index..self.digits.len() { + let digit = self.digits[i]; + if decimals > 4 { + write!(f, "{digit:0>4}")?; + decimals -= 4; + } else { + let digit = digit / 10_i16.pow(4 - decimals as u32); + write!(f, "{digit:0>w$}", w = decimals as usize)?; + decimals = 0; + } + } + + if decimals > 0 { + write!(f, "{:0>width$}", 0, width = decimals as usize)?; + } + } + + Ok(()) + } + NumericSign::NaN => write!(f, "NaN"), + NumericSign::PositiveInfinity => write!(f, "Infinity"), + NumericSign::NegativeInfinity => write!(f, "-Infinity"), + } + } +} + +fn split_e(s: &[u8]) -> (&[u8], Option<&[u8]>) { + let mut s = s.splitn(2, |&b| b == b'e' || b == b'E'); + let first = s.next().unwrap(); + let second = s.next(); + (first, second) +} + +fn split_decimal(s: &[u8]) -> (&[u8], Option<&[u8]>) { + let mut s = s.splitn(2, |&b| b == b'.'); + let first = s.next().unwrap(); + let second = s.next(); + (first, second) +} + +impl FromStr for Numeric { + type Err = StdBox; + + fn from_str(value: &str) -> Result { + let mut digits = VecDeque::new(); + let mut scale = 0; + let mut sign = NumericSign::Positive; + + if value.eq_ignore_ascii_case("NaN") { + return Ok(Numeric::nan()); + } + if value.eq_ignore_ascii_case("Infinity") || value.eq_ignore_ascii_case("Inf") { + return Ok(Numeric::infinity()); + } + if value.eq_ignore_ascii_case("-Infinity") || value.eq_ignore_ascii_case("-Inf") { + return Ok(Numeric::negative_infinity()); + } + + let mut s = value.as_bytes(); + if let Some(&b'-') = s.first() { + sign = NumericSign::Negative; + s = &s[1..]; + }; + + if s.is_empty() { + return Err("empty string".into()); + } + + let (s, e) = split_e(s); + let (s, decimal) = split_decimal(s); + let mut decimal: VecDeque = decimal.unwrap_or(b"").to_vec().into(); + let mut integer: VecDeque = s.to_vec().into(); + + // parse scientific notation + if let Some(mut e) = e { + if e.is_empty() { + return Err("empty scientific notation string".into()); + } + + let mut positive = true; + let mut exp = 0; + + if let Some(&b'-') = e.first() { + positive = false; + e = &e[1..]; + } else if let Some(&b'+') = e.first() { + e = &e[1..]; + } + + for &b in e { + if !b.is_ascii_digit() { + return Err("scientific notation string contain non-digit character".into()); + } + exp = exp * 10 + (b - b'0') as u16; + } + + if positive { + while !decimal.is_empty() && exp > 0 { + integer.push_back(decimal[0]); + decimal.pop_front(); + exp -= 1; + } + for _ in 0..exp { + integer.push_back(b'0'); + } + } else { + while !(integer.is_empty() || integer == b"0") && exp > 0 { + decimal.push_front(integer[integer.len() - 1]); + integer.pop_back(); + exp -= 1; + } + for _ in 0..exp { + decimal.push_front(b'0'); + } + } + } + + // remove leading zeros from integer + while integer.len() > 1 && integer[0] == b'0' { + integer.pop_front(); + } + + let mut weight = if integer.is_empty() { + -1 + } else { + integer.len().div_ceil(4) as i16 - 1 + }; + + // parse the integer part + if weight >= 0 { + let integer: Vec = integer.into(); + for chunk in integer.rchunks(4) { + let mut digit = 0; + for &b in chunk { + if !b.is_ascii_digit() { + return Err("integer part string contain non-digit character".into()); + } + digit = digit * 10 + (b - b'0') as i16; + } + digits.push_front(digit); + } + } + + // parse the decimal part + if !decimal.is_empty() { + scale = decimal.len() as u16; + + let decimal: Vec = decimal.into(); + for chunk in decimal.chunks(4) { + let mut digit = 0; + for i in 0..4 { + let b = chunk.get(i).unwrap_or(&b'0'); + if !b.is_ascii_digit() { + return Err("decimal part string contain non-digit character".into()); + } + digit = digit * 10 + (b - b'0') as i16; + } + digits.push_back(digit); + } + } + + // drop trailing zeros + while digits.back() == Some(&0) { + digits.pop_back(); + } + // drop leading zeros + while digits.front() == Some(&0) { + weight -= 1; + digits.pop_front(); + } + + Ok(Numeric { + sign, + scale, + weight, + digits: digits.into(), + }) + } +} + +/// Numeric sign +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NumericSign { + /// Positive number + Positive, + /// Negative number + Negative, + /// Not a number + NaN, + /// Positive infinity + PositiveInfinity, + /// Negative infinity + NegativeInfinity, +} + +impl NumericSign { + #[inline] + fn try_from_u16(sign: u16) -> Result> { + match sign { + 0x0000 => Ok(NumericSign::Positive), + 0x4000 => Ok(NumericSign::Negative), + 0xC000 => Ok(NumericSign::NaN), + 0xD000 => Ok(NumericSign::PositiveInfinity), + 0xF000 => Ok(NumericSign::NegativeInfinity), + _ => Err("invalid sign in numeric value".into()), + } + } + + #[inline] + fn into_u16(self) -> u16 { + match self { + NumericSign::Positive => 0x0000, + NumericSign::Negative => 0x4000, + NumericSign::NaN => 0xC000, + NumericSign::PositiveInfinity => 0xD000, + NumericSign::NegativeInfinity => 0xF000, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_string_deserialization_and_serialization() { + let cases = &[ + ( + "0", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 0, + digits: vec![], + }, + ), + ( + "1", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 0, + digits: vec![1], + }, + ), + ( + "-1", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 0, + digits: vec![1], + }, + ), + ( + "10", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 0, + digits: vec![10], + }, + ), + ( + "-10", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 0, + digits: vec![10], + }, + ), + ( + "20000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![2], + }, + ), + ( + "-20000", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 1, + digits: vec![2], + }, + ), + ( + "20001", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![2, 1], + }, + ), + ( + "-20001", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 1, + digits: vec![2, 1], + }, + ), + ( + "200000000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 2, + digits: vec![2], + }, + ), + ( + "2.0", + Numeric { + sign: NumericSign::Positive, + scale: 1, + weight: 0, + digits: vec![2], + }, + ), + ( + "2.1", + Numeric { + sign: NumericSign::Positive, + scale: 1, + weight: 0, + digits: vec![2, 1000], + }, + ), + ( + "2.10", + Numeric { + sign: NumericSign::Positive, + scale: 2, + weight: 0, + digits: vec![2, 1000], + }, + ), + ( + "200000000.0001", + Numeric { + sign: NumericSign::Positive, + scale: 4, + weight: 2, + digits: vec![2, 0, 0, 1], + }, + ), + ( + "-200000000.0001", + Numeric { + sign: NumericSign::Negative, + scale: 4, + weight: 2, + digits: vec![2, 0, 0, 1], + }, + ), + ( + "0.1", + Numeric { + sign: NumericSign::Positive, + scale: 1, + weight: -1, + digits: vec![1000], + }, + ), + ( + "-0.1", + Numeric { + sign: NumericSign::Negative, + scale: 1, + weight: -1, + digits: vec![1000], + }, + ), + ( + "123.456", + Numeric { + sign: NumericSign::Positive, + scale: 3, + weight: 0, + digits: vec![123, 4560], + }, + ), + ( + "-123.456", + Numeric { + sign: NumericSign::Negative, + scale: 3, + weight: 0, + digits: vec![123, 4560], + }, + ), + ( + "-123.0456", + Numeric { + sign: NumericSign::Negative, + scale: 4, + weight: 0, + digits: vec![123, 456], + }, + ), + ( + "0.1000000000000000", + Numeric { + sign: NumericSign::Positive, + scale: 16, + weight: -1, + digits: vec![1000], + }, + ), + ( + "-0.1000000000000000", + Numeric { + sign: NumericSign::Negative, + scale: 16, + weight: -1, + digits: vec![1000], + }, + ), + ( + "0.003159370000000000", + Numeric { + sign: NumericSign::Positive, + scale: 18, + weight: -1, + digits: vec![31, 5937], + }, + ), + ( + "-0.003159370000000000", + Numeric { + sign: NumericSign::Negative, + scale: 18, + weight: -1, + digits: vec![31, 5937], + }, + ), + ( + "0.0000000000000002", + Numeric { + sign: NumericSign::Positive, + scale: 16, + weight: -4, + digits: vec![2], + }, + ), + ( + "-0.0000000000000002", + Numeric { + sign: NumericSign::Negative, + scale: 16, + weight: -4, + digits: vec![2], + }, + ), + ( + "100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 35, + digits: vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + ], + }, + ), + ( + "-100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 35, + digits: vec![ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, + ], + }, + ), + ]; + + for (str, n) in cases { + assert_eq!(*str, n.to_string(), "numeric to string"); + let num = str.parse::().expect("parse numeric"); + assert_eq!(num, *n, "numeric from string"); + } + } + + #[test] + fn test_from_scientific_notation() { + let cases = &[ + ( + "2e4", + "20000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![2], + }, + ), + ( + "2e+4", + "20000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![2], + }, + ), + ( + "-2e4", + "-20000", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 1, + digits: vec![2], + }, + ), + ( + "-2e-4", + "-0.0002", + Numeric { + sign: NumericSign::Negative, + scale: 4, + weight: -1, + digits: vec![2], + }, + ), + ( + "1.234e4", + "12340", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![1, 2340], + }, + ), + ( + "-1.234e4", + "-12340", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 1, + digits: vec![1, 2340], + }, + ), + ( + "1.234e5", + "123400", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 1, + digits: vec![12, 3400], + }, + ), + ( + "-1.234e5", + "-123400", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 1, + digits: vec![12, 3400], + }, + ), + ( + "1.234e8", + "123400000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 2, + digits: vec![1, 2340], + }, + ), + ( + "-1.234e8", + "-123400000", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 2, + digits: vec![1, 2340], + }, + ), + ( + "0.0001e4", + "1", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 0, + digits: vec![1], + }, + ), + ( + "-0.0001e4", + "-1", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 0, + digits: vec![1], + }, + ), + ( + "0.0001e5", + "10", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 0, + digits: vec![10], + }, + ), + ( + "-0.0001e5", + "-10", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 0, + digits: vec![10], + }, + ), + ( + "2e16", + "20000000000000000", + Numeric { + sign: NumericSign::Positive, + scale: 0, + weight: 4, + digits: vec![2], + }, + ), + ( + "-2e16", + "-20000000000000000", + Numeric { + sign: NumericSign::Negative, + scale: 0, + weight: 4, + digits: vec![2], + }, + ), + ( + "2e-16", + "0.0000000000000002", + Numeric { + sign: NumericSign::Positive, + scale: 16, + weight: -4, + digits: vec![2], + }, + ), + ( + "-2e-16", + "-0.0000000000000002", + Numeric { + sign: NumericSign::Negative, + scale: 16, + weight: -4, + digits: vec![2], + }, + ), + ( + "2e-17", + "0.00000000000000002", + Numeric { + sign: NumericSign::Positive, + scale: 17, + weight: -5, + digits: vec![2000], + }, + ), + ( + "-2e-17", + "-0.00000000000000002", + Numeric { + sign: NumericSign::Negative, + scale: 17, + weight: -5, + digits: vec![2000], + }, + ), + ]; + + for (e, str, n) in cases { + let num = e.parse::().expect("parse numeric"); + assert_eq!(num, *n, "{e} to numeric"); + assert_eq!(num.to_string(), *str, "{e} back to string"); + } + } + + use proptest::prelude::*; + proptest! { + #[test] + fn test_arbitrary_f64_from_string_and_back(value in any::()) { + let prop_val = value.to_string(); + let numeric = Numeric::from_str(&prop_val).expect("parse numeric"); + let str_val = numeric.to_string(); + assert_eq!(prop_val, str_val, "proprty test value {value}"); + } + #[test] + fn test_arbitrary_i64_from_string_and_back(value in any::()) { + let prop_val = value.to_string(); + let numeric = Numeric::from_str(&prop_val).expect("parse numeric"); + let str_val = numeric.to_string(); + assert_eq!(prop_val, str_val, "proprty test value {value}"); + } + } +}