Skip to main content

rust_decimal/
mysql.rs

1use crate::Decimal;
2use diesel::{
3    deserialize::{self, FromSql},
4    mysql::Mysql,
5    serialize::{self, IsNull, Output, ToSql},
6    sql_types::Numeric,
7};
8use std::io::Write;
9use std::str::FromStr;
10
11#[cfg(feature = "diesel")]
12impl ToSql<Numeric, Mysql> for Decimal {
13    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
14        write!(out, "{}", *self).map(|_| IsNull::No).map_err(|e| e.into())
15    }
16}
17
18#[cfg(feature = "diesel")]
19impl FromSql<Numeric, Mysql> for Decimal {
20    fn from_sql(numeric: diesel::mysql::MysqlValue) -> deserialize::Result<Self> {
21        // From what I can ascertain, MySQL simply reads from a string format for the Decimal type.
22        // Explicitly, it looks like it is length followed by the string. Regardless, we can leverage
23        // internal types.
24        let s = std::str::from_utf8(numeric.as_bytes())?;
25        Decimal::from_str(s).map_err(|e| e.into())
26    }
27}
28
29#[cfg(test)]
30mod tests {
31    use super::*;
32
33    struct Test {
34        value: Decimal,
35    }
36
37    struct NullableTest {
38        value: Option<Decimal>,
39    }
40
41    pub static TEST_DECIMALS: &[(u32, u32, &str, &str)] = &[
42        // precision, scale, sent, expected
43        (1, 0, "1", "1"),
44        (6, 2, "1", "1.00"),
45        (6, 2, "9999.99", "9999.99"),
46        (35, 6, "3950.123456", "3950.123456"),
47        (10, 2, "3950.123456", "3950.12"),
48        (35, 6, "3950", "3950.000000"),
49        (4, 0, "3950", "3950"),
50        (35, 6, "0.1", "0.100000"),
51        (35, 6, "0.01", "0.010000"),
52        (35, 6, "0.001", "0.001000"),
53        (35, 6, "0.0001", "0.000100"),
54        (35, 6, "0.00001", "0.000010"),
55        (35, 6, "0.000001", "0.000001"),
56        (35, 6, "1", "1.000000"),
57        (35, 6, "-100", "-100.000000"),
58        (35, 6, "-123.456", "-123.456000"),
59        (35, 6, "119996.25", "119996.250000"),
60        (35, 6, "1000000", "1000000.000000"),
61        (35, 6, "9999999.99999", "9999999.999990"),
62        (35, 6, "12340.56789", "12340.567890"),
63    ];
64
65    /// Gets the URL for connecting to MySQL for testing. Set the MYSQL_URL
66    /// environment variable to change from the default of "mysql://root@localhost/mysql".
67    fn get_mysql_url() -> String {
68        if let Ok(url) = std::env::var("MYSQL_URL") {
69            return url;
70        }
71        "mysql://root@127.0.0.1/mysql".to_string()
72    }
73
74    #[cfg(feature = "diesel")]
75    mod diesel_tests {
76        use super::*;
77        use diesel::deserialize::QueryableByName;
78        use diesel::prelude::*;
79        use diesel::row::NamedRow;
80        use diesel::sql_query;
81        use diesel::sql_types::Text;
82
83        impl QueryableByName<Mysql> for Test {
84            fn build<'a>(row: &impl NamedRow<'a, Mysql>) -> deserialize::Result<Self> {
85                let value = NamedRow::get(row, "value")?;
86                Ok(Test { value })
87            }
88        }
89
90        impl QueryableByName<Mysql> for NullableTest {
91            fn build<'a>(row: &impl NamedRow<'a, Mysql>) -> deserialize::Result<Self> {
92                let value = NamedRow::get(row, "value")?;
93                Ok(NullableTest { value })
94            }
95        }
96
97        #[test]
98        fn test_null() {
99            let mut connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");
100
101            // Test NULL
102            let items: Vec<NullableTest> = sql_query("SELECT CAST(NULL AS DECIMAL) AS value")
103                .load(&mut connection)
104                .expect("Unable to query value");
105            let result = items.first().unwrap().value;
106            assert_eq!(None, result);
107        }
108
109        #[test]
110        fn read_numeric_type() {
111            let mut connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");
112            for &(precision, scale, sent, expected) in TEST_DECIMALS.iter() {
113                let items: Vec<Test> = sql_query(format!(
114                    "SELECT CAST('{}' AS DECIMAL({}, {})) AS value",
115                    sent, precision, scale
116                ))
117                .load(&mut connection)
118                .expect("Unable to query value");
119                assert_eq!(
120                    expected,
121                    items.first().unwrap().value.to_string(),
122                    "DECIMAL({}, {}) sent: {}",
123                    precision,
124                    scale,
125                    sent
126                );
127            }
128        }
129
130        #[test]
131        fn write_numeric_type() {
132            let mut connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");
133            for &(precision, scale, sent, expected) in TEST_DECIMALS.iter() {
134                let items: Vec<Test> =
135                    sql_query(format!("SELECT CAST(? AS DECIMAL({}, {})) AS value", precision, scale))
136                        .bind::<Text, _>(sent)
137                        .load(&mut connection)
138                        .expect("Unable to query value");
139                assert_eq!(
140                    expected,
141                    items.first().unwrap().value.to_string(),
142                    "DECIMAL({}, {}) sent: {}",
143                    precision,
144                    scale,
145                    sent
146                );
147            }
148        }
149    }
150}