Skip to content

Commit f7bb506

Browse files
authored
Merge pull request diesel-rs#837 from rubdos/bigdecimal
numeric/BigDecimal
2 parents e0861bb + 42e8e76 commit f7bb506

10 files changed

Lines changed: 215 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
1010

1111
* Added support for the [PostgreSQL network types][pg-network-0.14.0] `MACADDR`.
1212

13+
* Added support for the Numeric datatypes, using the [BigDecimal crate][bigdecimal-0.14.0].
14+
1315
* Added a function which maps to SQL `NOT`. See [the docs][not-0.14.0] for more
1416
details.
1517

@@ -18,6 +20,7 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/
1820
[pg-network-0.14.0]: https://www.postgresql.org/docs/9.6/static/datatype-net-types.html
1921
[not-0.14.0]: http://docs.diesel.rs/diesel/expression/dsl/fn.not.html
2022
[insert-default-0.14.0]: http://docs.diesel.rs/disel/fn.insert_default_values.html
23+
[bigdecimal-0.14-0]: https://crates.io/crates/bigdecimal
2124

2225
* Added `diesel_prefix_operator!` which behaves identically to
2326
`diesel_postfix_operator!` (previously `postfix_predicate!`), but for

diesel/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ time = { version = "0.1", optional = true }
2525
url = { version = "1.4.0", optional = true }
2626
uuid = { version = ">=0.2.0, <0.6.0", optional = true, features = ["use_std"] }
2727
ipnetwork = { version = "0.12.2", optional = true }
28+
num-bigint = { version = "0.1.37", optional = true }
29+
num-traits = { version = "0.1.35", optional = true }
30+
num-integer = { version = "0.1.32", optional = true }
31+
bigdecimal = { version = "0.0.7", optional = true }
2832

2933
[dev-dependencies]
3034
cfg-if = "0.1.0"
@@ -35,7 +39,7 @@ tempdir = "^0.3.4"
3539

3640
[features]
3741
default = ["with-deprecated"]
38-
extras = ["chrono", "serde_json", "uuid", "deprecated-time", "network-address"]
42+
extras = ["chrono", "serde_json", "uuid", "deprecated-time", "network-address", "numeric"]
3943
unstable = []
4044
lint = ["clippy"]
4145
large-tables = []
@@ -46,6 +50,7 @@ mysql = ["mysqlclient-sys", "url"]
4650
with-deprecated = []
4751
deprecated-time = ["time"]
4852
network-address = ["ipnetwork", "libc"]
53+
numeric = ["num-bigint", "bigdecimal", "num-traits", "num-integer"]
4954

5055
[badges]
5156
travis-ci = { repository = "diesel-rs/diesel" }

diesel/src/pg/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod floats;
44
#[cfg(feature = "network-address")]
55
mod network_address;
66
mod integers;
7+
mod numeric;
78
mod primitives;
89
#[cfg(feature = "uuid")]
910
mod uuid;

diesel/src/pg/types/numeric.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#[cfg(feature="bigdecimal")]
2+
mod bigdecimal {
3+
extern crate num_traits;
4+
extern crate num_bigint;
5+
extern crate num_integer;
6+
extern crate bigdecimal;
7+
8+
use std::error::Error;
9+
use std::io::prelude::*;
10+
11+
use pg::Pg;
12+
13+
use self::num_traits::{Signed, Zero, ToPrimitive};
14+
use self::num_bigint::{Sign, BigInt, BigUint, ToBigInt};
15+
use self::num_integer::Integer;
16+
use self::bigdecimal::BigDecimal;
17+
18+
use pg::data_types::PgNumeric;
19+
use types::{self, FromSql, ToSql, IsNull};
20+
21+
type Digits = Vec<i16>;
22+
23+
fn bigdec_add_integer_part(digits: &mut Digits, absolute: &BigDecimal) -> i16 {
24+
let mut weight = 0;
25+
let ten_k = BigInt::from(10000);
26+
27+
let mut integer_part = absolute.to_bigint().expect("Can always take integer part of BigDecimal");
28+
29+
while ten_k < integer_part {
30+
weight += 1;
31+
// digit is integer_part REM 10_000
32+
let (div, digit) = integer_part.div_rem(&ten_k);
33+
digits.push(digit.to_u16().expect("digit < 10000, but cannot fit in i16") as i16);
34+
integer_part = div;
35+
}
36+
digits.push(integer_part.to_string().parse::<i16>().expect("digit < 10000, but cannot fit in i16"));
37+
38+
digits.reverse();
39+
40+
weight
41+
}
42+
43+
fn bigdec_add_decimal_part(digits: &mut Digits, absolute: &BigDecimal) -> u16 {
44+
use std::str::FromStr;
45+
46+
let ten_k = BigDecimal::from_str("10000").expect("Could not parse into BigDecimal");
47+
48+
let decimal_part = absolute;
49+
let mut decimal_part = decimal_part - absolute.with_scale(0);
50+
// scale is the amount of digits to print. to_string() includes a "0.",
51+
// that's why the -2 is there.
52+
let scale = if decimal_part == Zero::zero() {
53+
0
54+
} else {
55+
decimal_part.to_string().len() as u16 - 2
56+
};
57+
58+
while decimal_part != BigDecimal::zero() {
59+
decimal_part *= &ten_k;
60+
let digit = decimal_part.to_bigint().expect("Can always take integer part of BigDecimal");
61+
62+
// This can be simplified when github.com/akubera/bigdecimal-rs/issues/13 gets
63+
// solved; decimal_part -= &digit; should suffice by then.
64+
decimal_part -= BigDecimal::new(digit.clone(), 0);
65+
digits.push(digit.to_u16().expect("digit < 10000, but cannot fit in i16") as i16);
66+
}
67+
68+
scale
69+
}
70+
71+
impl ToSql<types::Numeric, Pg> for BigDecimal {
72+
fn to_sql<W: Write>(&self, out: &mut W) -> Result<IsNull, Box<Error + Send + Sync>> {
73+
// The encoding of the BigDecimal type for PostgreSQL is a bit complicated:
74+
// PostgreSQL expects the data in base-10000 (so two bytes per 10k),
75+
// and the decimal point should lie on a boundary (as per definition of "base-10000").
76+
77+
// BigDecimal, internally, holds an int vector (base-256, one byte per byte),
78+
// and a base (u64, base-10) shift.
79+
80+
// Therefore, we split up the encoding in three parts:
81+
// the sign, the (integer) part before the decimal, and the part after the decimal.
82+
83+
let absolute = self.abs();
84+
let mut digits = vec![];
85+
86+
// Encode the integer part
87+
let weight = bigdec_add_integer_part(&mut digits, &absolute);
88+
89+
// Encode the decimal part
90+
let scale = bigdec_add_decimal_part(&mut digits, &absolute);
91+
92+
let numeric = match self.sign() {
93+
Sign::Plus => PgNumeric::Positive {
94+
digits, scale, weight
95+
},
96+
Sign::Minus => PgNumeric::Negative {
97+
digits, scale, weight
98+
},
99+
Sign::NoSign => PgNumeric::Positive {
100+
digits: vec![0],
101+
scale: 0,
102+
weight: 0,
103+
},
104+
};
105+
ToSql::<types::Numeric, Pg>::to_sql(&numeric, out)
106+
}
107+
}
108+
109+
impl FromSql<types::Numeric, Pg> for BigDecimal {
110+
fn from_sql(numeric: Option<&[u8]>) -> Result<Self, Box<Error+Send+Sync>> {
111+
let (sign, weight, _, digits) = match PgNumeric::from_sql(numeric)? {
112+
PgNumeric::Positive { weight, scale, digits } => (Sign::Plus, weight, scale, digits),
113+
PgNumeric::Negative { weight, scale, digits } => (Sign::Minus, weight, scale, digits),
114+
PgNumeric::NaN => return Err(Box::from("NaN is not (yet) supported in BigDecimal")),
115+
};
116+
let mut result = BigUint::default();
117+
let count = digits.len() as i64;
118+
for digit in digits {
119+
result = result * BigUint::from(10_000u64);
120+
result = result + BigUint::from(digit as u64);
121+
}
122+
// First digit got factor 10_000^(digits.len() - 1), but should get 10_000^weight
123+
let correction_exp = 4 * ( (weight as i64) - count + 1);
124+
// FIXME: `scale` allows to drop some insignificant figures, which is currently unimplemented.
125+
// This means that e.g. PostgreSQL 0.01 will be interpreted as 0.0100
126+
let result = BigDecimal::new(BigInt::from_biguint(sign, result), -correction_exp);
127+
Ok(result)
128+
}
129+
}
130+
}

diesel/src/types/impls/decimal.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[cfg(feature="bigdecimal")]
2+
mod bigdecimal {
3+
extern crate bigdecimal;
4+
expression_impls! {
5+
Numeric -> bigdecimal::BigDecimal,
6+
}
7+
8+
queryable_impls! {
9+
Numeric -> bigdecimal::BigDecimal,
10+
}
11+
}

diesel/src/types/impls/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,4 @@ mod integers;
177177
pub mod option;
178178
mod primitives;
179179
mod tuples;
180+
mod decimal;

diesel/src/types/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,22 @@ use std::io::Write;
132132

133133
/// The numeric SQL type.
134134
///
135-
/// This type does not currently have any corresponding Rust types. On SQLite,
136-
/// [`Double`](struct.Double.html) should be used instead.
135+
/// ### [`ToSql`](/diesel/types/trait.ToSql.html) impls
136+
///
137+
/// - [`bigdecimal::BigDecimal`][bigdecimal] (currenty PostgreSQL only, requires the `numeric`
138+
/// feature, which depends on the
139+
/// [`bigdecimal`][bigdecimal] crate)
140+
///
141+
/// ### [`FromSql`](/diesel/types/trait.FromSql.html) impls
142+
///
143+
/// - [`bigdecimal::BigDecimal`][BigDecimal] (currenty PostgreSQL only, requires the `numeric`
144+
/// feature, which depends on the
145+
/// [`bigdecimal`][bigdecimal] crate)
146+
///
147+
/// On SQLite, [`Double`](struct.Double.html) should be used instead.
148+
///
149+
/// [BigDecimal]: /bigdecimal/struct.BigDecimal.html
150+
/// [bigdecimal]: /bigdecimal/index.html
137151
#[derive(Debug, Clone, Copy, Default)] pub struct Numeric;
138152

139153
#[cfg(not(feature="postgres"))]

diesel_tests/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ dotenv = ">=0.8, <0.11"
1212
[dependencies]
1313
assert_matches = "1.0.1"
1414
chrono = { version = "0.4" }
15-
diesel = { path = "../diesel", default-features = false, features = ["quickcheck", "chrono", "uuid", "serde_json", "network-address"] }
15+
diesel = { path = "../diesel", default-features = false, features = ["quickcheck", "chrono", "uuid", "serde_json", "network-address", "numeric"] }
1616
diesel_codegen = { path = "../diesel_codegen" }
1717
dotenv = ">=0.8, <0.11"
1818
quickcheck = { version = "0.3.1", features = ["unstable"] }
1919
uuid = { version = ">=0.2.0, <0.6.0" }
2020
serde_json = { version=">=0.9, <2.0" }
2121
ipnetwork = "0.12.2"
22+
bigdecimal = "0.0.7"
2223

2324
[features]
2425
default = []

diesel_tests/tests/types.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
// FIXME: Review this module to see if we can do these casts in a more backend agnostic way
22
extern crate chrono;
3+
#[cfg(feature="postgres")]
4+
extern crate bigdecimal;
35

46
use schema::*;
57
use diesel::*;
68
#[cfg(feature="postgres")]
79
use diesel::pg::Pg;
810
use diesel::types::*;
911

12+
use quickcheck::quickcheck;
13+
1014
table! {
1115
has_timestamps {
1216
id -> Integer,
@@ -425,6 +429,39 @@ fn pg_numeric_from_sql() {
425429
assert_eq!(expected_value, query_single_value::<Numeric, PgNumeric>(query));
426430
}
427431

432+
#[test]
433+
#[cfg(feature = "postgres")]
434+
fn pg_numeric_bigdecimal_to_sql() {
435+
use self::bigdecimal::BigDecimal;
436+
437+
fn correct_rep(integer: u64, decimal: u64) -> bool {
438+
let expected = format!("{}.{}", integer, decimal);
439+
let value: BigDecimal = expected.parse().expect("Could not parse to a BigDecimal");
440+
query_to_sql_equality::<Numeric, BigDecimal>(&expected, value)
441+
}
442+
443+
quickcheck(correct_rep as fn(u64, u64) -> bool);
444+
}
445+
446+
#[test]
447+
#[cfg(feature = "postgres")]
448+
fn pg_numeric_bigdecimal_from_sql() {
449+
use self::bigdecimal::BigDecimal;
450+
451+
let query = "1.0::numeric";
452+
let expected_value: BigDecimal = "1.0".parse().expect("Could not parse to a BigDecimal");
453+
assert_eq!(expected_value, query_single_value::<Numeric, BigDecimal>(query));
454+
455+
let query = "141.00::numeric";
456+
let expected_value: BigDecimal = "141.00".parse().expect("Could not parse to a BigDecimal");
457+
assert_eq!(expected_value, query_single_value::<Numeric, BigDecimal>(query));
458+
459+
// Some non standard values:
460+
let query = "18446744073709551616::numeric"; // 2^64; doesn't fit in u64
461+
let expected_value: BigDecimal = "18446744073709551616.00".parse().expect("Could not parse to a BigDecimal");
462+
assert_eq!(expected_value, query_single_value::<Numeric, BigDecimal>(query));
463+
}
464+
428465
#[test]
429466
#[cfg(feature = "postgres")]
430467
fn pg_uuid_from_sql() {

diesel_tests/tests/types_roundtrip.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ mod sqlite_types {
9090
mod pg_types {
9191
extern crate uuid;
9292
extern crate ipnetwork;
93+
extern crate bigdecimal;
94+
9395
use super::*;
9496

9597
test_round_trip!(date_roundtrips, Date, PgDate);
@@ -114,6 +116,12 @@ mod pg_types {
114116
test_round_trip!(inet_v4_roundtrips, Inet, (u8, u8, u8, u8), mk_ipv4);
115117
test_round_trip!(inet_v6_roundtrips, Inet, (u16, u16, u16, u16, u16, u16, u16, u16), mk_ipv6);
116118

119+
test_round_trip!(bigdecimal_roundtrips, Numeric, (i64, u64), mk_bigdecimal);
120+
121+
fn mk_bigdecimal(data: (i64, u64)) -> self::bigdecimal::BigDecimal {
122+
format!("{}.{}", data.0, data.1).parse().expect("Could not interpret as bigdecimal")
123+
}
124+
117125
fn mk_uuid(data: (u32, u16, u16, (u8, u8, u8, u8, u8, u8, u8, u8))) -> self::uuid::Uuid {
118126
let a = data.3;
119127
let b = [a.0, a.1, a.2, a.3, a.4, a.5, a.6, a.7];

0 commit comments

Comments
 (0)