mirror of
https://github.com/diesel-rs/diesel.git
synced 2024-10-04 01:28:13 +03:00
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:
parent
e979eb35b3
commit
b38b9625ec
@ -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
|
||||
|
@ -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>)
|
||||
}
|
||||
}
|
135
diesel/src/types/impls/floats/mod.rs
Normal file
135
diesel/src/types/impls/floats/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
65
diesel/src/types/impls/floats/quickcheck_impls.rs
Normal file
65
diesel/src/types/impls/floats/quickcheck_impls.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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");
|
||||
|
Loading…
Reference in New Issue
Block a user