Add support for the Postgres numeric type

This doesn't target any specific big decimal crate at the moment, it's
just minimal support for columns of that type. This was pretty
straightforward, the hardest part was really just getting an
implementation of `Arbitrary` which matches up with the behavior of PG's
reading.

The `max_digits = min(weight, scale)` is incorrect, but is a rough
approximation that will definitely get us below the length where it does
truncation. I don't want to duplicate that logic here, as it's complex.
This commit is contained in:
Sean Griffin 2015-12-02 08:31:28 -07:00
parent e979eb35b3
commit b38b9625ec
10 changed files with 237 additions and 42 deletions

View File

@ -17,6 +17,10 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
* Add `min` function that mirrors SQLs MIN.
* Added support for the `Numeric` data type. Since there is no Big Decimal type
in the standard library, a dumb struct has been provided which mirrors what
Postgres provides, which can be converted into whatever crate you are using.
## [0.2.0] - 2015-11-30
### Added

View File

@ -1,38 +0,0 @@
extern crate byteorder;
use self::byteorder::{ReadBytesExt, WriteBytesExt, BigEndian};
use super::option::UnexpectedNullError;
use types::{FromSql, ToSql, IsNull};
use types;
use std::error::Error;
use std::io::Write;
impl FromSql<types::Float> for f32 {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
let mut bytes = not_none!(bytes);
bytes.read_f32::<BigEndian>().map_err(|e| Box::new(e) as Box<Error>)
}
}
impl ToSql<types::Float> for f32 {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
out.write_f32::<BigEndian>(*self)
.map(|_| IsNull::No)
.map_err(|e| Box::new(e) as Box<Error>)
}
}
impl FromSql<types::Double> for f64 {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
let mut bytes = not_none!(bytes);
bytes.read_f64::<BigEndian>().map_err(|e| Box::new(e) as Box<Error>)
}
}
impl ToSql<types::Double> for f64 {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
out.write_f64::<BigEndian>(*self)
.map(|_| IsNull::No)
.map_err(|e| Box::new(e) as Box<Error>)
}
}

View File

@ -0,0 +1,135 @@
extern crate byteorder;
use self::byteorder::{ReadBytesExt, WriteBytesExt, BigEndian};
use super::option::UnexpectedNullError;
use types::{FromSql, ToSql, IsNull};
use types;
use std::error::Error;
use std::io::Write;
#[cfg(feature = "quickcheck")]
mod quickcheck_impls;
impl FromSql<types::Float> for f32 {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
let mut bytes = not_none!(bytes);
bytes.read_f32::<BigEndian>().map_err(|e| Box::new(e) as Box<Error>)
}
}
impl ToSql<types::Float> for f32 {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
out.write_f32::<BigEndian>(*self)
.map(|_| IsNull::No)
.map_err(|e| Box::new(e) as Box<Error>)
}
}
impl FromSql<types::Double> for f64 {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
let mut bytes = not_none!(bytes);
bytes.read_f64::<BigEndian>().map_err(|e| Box::new(e) as Box<Error>)
}
}
impl ToSql<types::Double> for f64 {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
out.write_f64::<BigEndian>(*self)
.map(|_| IsNull::No)
.map_err(|e| Box::new(e) as Box<Error>)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PgNumeric {
Positive {
weight: i16,
scale: u16,
digits: Vec<i16>,
},
Negative {
weight: i16,
scale: u16,
digits: Vec<i16>,
},
NaN,
}
#[derive(Debug, Clone, Copy)]
struct InvalidNumericSign(u16);
impl ::std::fmt::Display for InvalidNumericSign {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
write!(f, "InvalidNumericSign({0:x})", self.0)
}
}
impl Error for InvalidNumericSign {
fn description(&self) -> &str {
"sign for numeric field was not one of 0, 0x4000, 0xC000"
}
}
impl FromSql<types::Numeric> for PgNumeric {
fn from_sql(bytes: Option<&[u8]>) -> Result<Self, Box<Error>> {
let mut bytes = not_none!(bytes);
let ndigits = try!(bytes.read_u16::<BigEndian>());
let mut digits = Vec::with_capacity(ndigits as usize);
let weight = try!(bytes.read_i16::<BigEndian>());
let sign = try!(bytes.read_u16::<BigEndian>());
let scale = try!(bytes.read_u16::<BigEndian>());
for _ in 0..ndigits {
digits.push(try!(bytes.read_i16::<BigEndian>()));
}
match sign {
0 => Ok(PgNumeric::Positive {
weight: weight,
scale: scale,
digits: digits,
}),
0x4000 => Ok(PgNumeric::Negative {
weight: weight,
scale: scale,
digits: digits,
}),
0xC000 => Ok(PgNumeric::NaN),
invalid => Err(Box::new(InvalidNumericSign(invalid))),
}
}
}
impl ToSql<types::Numeric> for PgNumeric {
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error>> {
let sign = match self {
&PgNumeric::Positive { .. } => 0,
&PgNumeric::Negative { .. } => 0x4000,
&PgNumeric::NaN => 0xC000,
};
let empty_vec = Vec::new();
let digits = match self {
&PgNumeric::Positive { ref digits, .. } => digits,
&PgNumeric::Negative { ref digits, .. } => digits,
&PgNumeric::NaN => &empty_vec,
};
let weight = match self {
&PgNumeric::Positive { weight, .. } => weight,
&PgNumeric::Negative { weight, .. } => weight,
&PgNumeric::NaN => 0,
};
let scale = match self {
&PgNumeric::Positive { scale, .. } => scale,
&PgNumeric::Negative { scale, .. } => scale,
&PgNumeric::NaN => 0,
};
try!(out.write_u16::<BigEndian>(digits.len() as u16));
try!(out.write_i16::<BigEndian>(weight));
try!(out.write_u16::<BigEndian>(sign));
try!(out.write_u16::<BigEndian>(scale));
for digit in digits.iter() {
try!(out.write_i16::<BigEndian>(*digit));
}
Ok(IsNull::No)
}
}

View File

@ -0,0 +1,65 @@
extern crate quickcheck;
use self::quickcheck::{Arbitrary, Gen};
use super::PgNumeric;
const SCALE_MASK: u16 = 0x3FFF;
impl Arbitrary for PgNumeric {
fn arbitrary<G: Gen>(g: &mut G) -> Self {
let mut variant = Option::<bool>::arbitrary(g);
let mut weight = -1;
while weight < 0 {
// Oh postgres... Don't ever change. http://bit.ly/lol-code-comments
weight = i16::arbitrary(g);
}
let scale = u16::arbitrary(g) & SCALE_MASK;
let digits = gen_vec_of_appropriate_length_valid_digits(g, weight as u16, scale);
if digits.len() == 0 {
weight = 0;
variant = Some(true);
}
match variant {
Some(true) => PgNumeric::Positive {
digits: digits,
weight: weight,
scale: scale,
},
Some(false) => PgNumeric::Negative {
digits: digits,
weight: weight,
scale: scale,
},
None => PgNumeric::NaN,
}
}
}
fn gen_vec_of_appropriate_length_valid_digits
<G: Gen>(g: &mut G, weight: u16, scale: u16) -> Vec<i16> {
let max_digits = ::std::cmp::min(weight, scale);
let mut digits = Vec::<Digit>::arbitrary(g).into_iter()
.map(|d| d.0)
.skip_while(|d| d == &0) // drop leading zeros
.take(max_digits as usize)
.collect::<Vec<_>>();
while digits.last() == Some(&0) {
digits.pop(); // drop trailing zeros
}
digits
}
#[derive(Debug, Clone, Copy)]
struct Digit(i16);
impl Arbitrary for Digit {
fn arbitrary<G: Gen>(g: &mut G) -> Self {
let mut n = -1;
while n < 0 || n >= 10000 {
n = i16::arbitrary(g);
}
Digit(n)
}
}

View File

@ -80,7 +80,7 @@ macro_rules! primitive_impls {
mod array;
pub mod date_and_time;
mod floats;
pub mod floats;
mod integers;
mod option;
mod primitives;

View File

@ -1,7 +1,9 @@
use expression::{Expression, AsExpression};
use expression::bound::Bound;
use std::error::Error;
use std::io::Write;
use data_types::PgNumeric;
use expression::bound::Bound;
use expression::{Expression, AsExpression};
use super::option::UnexpectedNullError;
use types::{NativeSqlType, FromSql, ToSql, IsNull};
use {Queriable, types};
@ -15,6 +17,7 @@ primitive_impls! {
Float -> (f32, 700),
Double -> (f64, 701),
Numeric -> (PgNumeric, 1700),
VarChar -> (String, 1043),
Text -> (String, 25),

View File

@ -11,6 +11,7 @@ pub mod structs {
//! there is no existing Rust primitive, or where using it would be
//! confusing (such as date and time types)
pub use super::super::impls::date_and_time::{PgTimestamp, PgDate, PgTime, PgInterval};
pub use super::super::impls::floats::PgNumeric;
}
}
@ -33,6 +34,7 @@ pub type BigSerial = BigInt;
#[derive(Clone, Copy, Default)] pub struct Float;
#[derive(Clone, Copy, Default)] pub struct Double;
#[derive(Clone, Copy, Default)] pub struct Numeric;
#[derive(Clone, Copy, Default)] pub struct VarChar;
#[derive(Clone, Copy, Default)] pub struct Text;

View File

@ -50,7 +50,7 @@ macro_rules! numeric_type {
}
}
numeric_type!(SmallInt, Integer, BigInt, Float, Double);
numeric_type!(SmallInt, Integer, BigInt, Float, Double, Numeric);
impl Add for super::Timestamp {
type Rhs = super::Interval;

View File

@ -263,6 +263,29 @@ fn pg_timestamp_to_sql_timestamp() {
assert!(!query_to_sql_equality::<Timestamp, PgTimestamp>(expected_non_equal_value, value));
}
#[test]
fn pg_numeric_from_sql() {
use diesel::data_types::PgNumeric;
let query = "SELECT 1.0::numeric";
let expected_value = PgNumeric::Positive {
digits: vec![1],
weight: 0,
scale: 1,
};
assert_eq!(expected_value, query_single_value::<Numeric, PgNumeric>(query));
let query = "SELECT -31.0::numeric";
let expected_value = PgNumeric::Negative {
digits: vec![31],
weight: 0,
scale: 1,
};
assert_eq!(expected_value, query_single_value::<Numeric, PgNumeric>(query));
let query = "SELECT 'NaN'::numeric";
let expected_value = PgNumeric::NaN;
assert_eq!(expected_value, query_single_value::<Numeric, PgNumeric>(query));
}
fn query_single_value<T: NativeSqlType, U: Queriable<T>>(sql: &str) -> U {
let connection = connection();
let mut cursor = connection.query_sql::<T, U>(sql)

View File

@ -57,3 +57,4 @@ test_round_trip!(date_roundtrips, Date, PgDate, "date");
test_round_trip!(time_roundtrips, Time, PgTime, "time");
test_round_trip!(timestamp_roundtrips, Timestamp, PgTimestamp, "timestamp");
test_round_trip!(interval_roundtrips, Interval, PgInterval, "interval");
test_round_trip!(numeric_roundtrips, Numeric, PgNumeric, "numeric");