Skip to content

Commit

Permalink
Add MySQL support for Diesel (#446)
Browse files Browse the repository at this point in the history
* Add MySQL support for Diesel
* Add MySQL service within Github workflow
  • Loading branch information
paupino committed Nov 26, 2021
1 parent 2f2d0fb commit b4e72bc
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 14 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ jobs:
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: ''
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
ports:
- 3306:3306

steps:
- uses: actions/checkout@v2
Expand Down
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ arbitrary = { default-features = false, optional = true, version = "1.0" }
arrayvec = { default-features = false, version = "0.5" }
byteorder = { default-features = false, optional = true, version = "1.3" }
bytes = { default-features = false, optional = true, version = "1.0" }
diesel = { default-features = false, features = ["postgres"], optional = true, version = "1.4" }
diesel = { default-features = false, optional = true, version = "1.4" }
num-traits = { default-features = false, features = ["i128"], version = "0.2" }
postgres = { default-features = false, optional = true, version = "0.19" }
rocket = { default-features = false, optional = true, version = "0.5.0-rc.1" }
Expand All @@ -45,7 +45,8 @@ tokio = { features = ["rt-multi-thread", "test-util", "macros"], version = "1.0"

[features]
c-repr = [] # Force Decimal to be repr(C)
db-diesel-postgres = ["diesel", "std"]
db-diesel-mysql = ["diesel/mysql", "std"]
db-diesel-postgres = ["diesel/postgres", "std"]
db-postgres = ["byteorder", "bytes", "postgres", "std"]
db-tokio-postgres = ["byteorder", "bytes", "postgres", "std", "tokio-postgres"]
default = ["serde", "std"]
Expand Down
21 changes: 18 additions & 3 deletions Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,8 @@ dependencies = [

[tasks.test-db]
dependencies = [
"test-db-postgres",
"test-db-tokio-postgres",
"test-db-diesel-postgres"
"test-db-mysql-all",
"test-db-postgres-all"
]

[tasks.test-serde]
Expand Down Expand Up @@ -175,6 +174,22 @@ args = ["test", "--workspace", "--no-default-features", "--features=maths-nopani
command = "cargo"
args = ["test", "--workspace", "--no-default-features", "--features=rust-fuzz", "rust_fuzz", "--", "--skip", "generated"]

[tasks.test-db-mysql-all]
dependencies = [
"test-db-diesel-mysql"
]

[tasks.test-db-diesel-mysql]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=db-diesel-mysql", "db", "--", "--skip", "generated"]

[tasks.test-db-postgres-all]
dependencies = [
"test-db-postgres",
"test-db-tokio-postgres",
"test-db-diesel-postgres"
]

[tasks.test-db-postgres]
command = "cargo"
args = ["test", "--workspace", "--tests", "--features=db-postgres", "db", "--", "--skip", "generated"]
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ assert_eq!(total.to_string(), "27.26");
* [db-postgres](#db-postgres)
* [db-tokio-postgres](#db-tokio-postgres)
* [db-diesel-postgres](#db-diesel-postgres)
* [db-diesel-mysql](#db-diesel-mysql)
* [legacy-ops](#legacy-ops)
* [maths](#maths)
* [rocket-traits](#rocket-traits)
Expand All @@ -103,6 +104,10 @@ Enables the tokio postgres module allowing for async communication with PostgreS

Enable `diesel` PostgreSQL support.

### `db-diesel-mysql`

Enable `diesel` MySQL support.

### `legacy-ops`

As of `1.10` the algorithms used to perform basic operations have changed which has benefits of significant speed improvements.
Expand Down
10 changes: 8 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,18 @@ mod error;
mod ops;
mod str;

#[cfg(any(feature = "postgres", feature = "diesel"))]
mod db;
#[cfg(feature = "rust-fuzz")]
mod fuzz;
#[cfg(feature = "maths")]
mod maths;
#[cfg(any(feature = "db-diesel-mysql"))]
mod mysql;
#[cfg(any(
feature = "db-tokio-postgres",
feature = "db-postgres",
feature = "db-diesel-postgres",
))]
mod postgres;
#[cfg(feature = "rocket-traits")]
mod rocket;
#[cfg(feature = "serde")]
Expand Down
150 changes: 150 additions & 0 deletions src/mysql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use crate::prelude::*;

#[cfg(feature = "db-diesel-mysql")]
mod diesel_mysql {
use super::*;
use ::diesel::{
deserialize::{self, FromSql},
mysql::Mysql,
serialize::{self, IsNull, Output, ToSql},
sql_types::Numeric,
};
use std::io::Write;
use std::str::FromStr;

impl ToSql<Numeric, Mysql> for Decimal {
fn to_sql<W: Write>(&self, out: &mut Output<W, Mysql>) -> serialize::Result {
// From what I can ascertain, MySQL simply writes to a string format for the Decimal type.
write!(out, "{}", *self).map(|_| IsNull::No).map_err(|e| e.into())
}
}

impl FromSql<Numeric, Mysql> for Decimal {
fn from_sql(numeric: Option<&[u8]>) -> deserialize::Result<Self> {
// From what I can ascertain, MySQL simply reads from a string format for the Decimal type.
// Explicitly, it looks like it is length followed by the string. Regardless, we can leverage
// internal types.
let bytes = numeric.ok_or("Invalid decimal")?;
let s = std::str::from_utf8(bytes)?;
Decimal::from_str(&s).map_err(|e| e.into())
}
}

#[cfg(test)]
mod tests {
use super::*;
use diesel::deserialize::QueryableByName;
use diesel::prelude::*;
use diesel::row::NamedRow;
use diesel::sql_query;
use diesel::sql_types::Text;

struct Test {
value: Decimal,
}

struct NullableTest {
value: Option<Decimal>,
}

impl QueryableByName<Mysql> for Test {
fn build<R: NamedRow<Mysql>>(row: &R) -> deserialize::Result<Self> {
let value = row.get("value")?;
Ok(Test { value })
}
}

impl QueryableByName<Mysql> for NullableTest {
fn build<R: NamedRow<Mysql>>(row: &R) -> deserialize::Result<Self> {
let value = row.get("value")?;
Ok(NullableTest { value })
}
}

pub static TEST_DECIMALS: &[(u32, u32, &str, &str)] = &[
// precision, scale, sent, expected
(1, 0, "1", "1"),
(6, 2, "1", "1.00"),
(6, 2, "9999.99", "9999.99"),
(35, 6, "3950.123456", "3950.123456"),
(10, 2, "3950.123456", "3950.12"),
(35, 6, "3950", "3950.000000"),
(4, 0, "3950", "3950"),
(35, 6, "0.1", "0.100000"),
(35, 6, "0.01", "0.010000"),
(35, 6, "0.001", "0.001000"),
(35, 6, "0.0001", "0.000100"),
(35, 6, "0.00001", "0.000010"),
(35, 6, "0.000001", "0.000001"),
(35, 6, "1", "1.000000"),
(35, 6, "-100", "-100.000000"),
(35, 6, "-123.456", "-123.456000"),
(35, 6, "119996.25", "119996.250000"),
(35, 6, "1000000", "1000000.000000"),
(35, 6, "9999999.99999", "9999999.999990"),
(35, 6, "12340.56789", "12340.567890"),
];

/// Gets the URL for connecting to MySQL for testing. Set the MYSQL_URL
/// environment variable to change from the default of "mysql://root@localhost/mysql".
fn get_mysql_url() -> String {
if let Ok(url) = std::env::var("MYSQL_URL") {
return url;
}
"mysql://root@127.0.0.1/mysql".to_string()
}

#[test]
fn test_null() {
let connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");

// Test NULL
let items: Vec<NullableTest> = sql_query("SELECT CAST(NULL AS DECIMAL) AS value")
.load(&connection)
.expect("Unable to query value");
let result = items.first().unwrap().value;
assert_eq!(None, result);
}

#[test]
fn read_numeric_type() {
let connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");
for &(precision, scale, sent, expected) in TEST_DECIMALS.iter() {
let items: Vec<Test> = sql_query(format!(
"SELECT CAST('{}' AS DECIMAL({}, {})) AS value",
sent, precision, scale
))
.load(&connection)
.expect("Unable to query value");
assert_eq!(
expected,
items.first().unwrap().value.to_string(),
"DECIMAL({}, {}) sent: {}",
precision,
scale,
sent
);
}
}

#[test]
fn write_numeric_type() {
let connection = diesel::MysqlConnection::establish(&get_mysql_url()).expect("Establish connection");
for &(precision, scale, sent, expected) in TEST_DECIMALS.iter() {
let items: Vec<Test> =
sql_query(format!("SELECT CAST($1 AS DECIMAL({}, {})) AS value", precision, scale))
.bind::<Text, _>(sent)
.load(&connection)
.expect("Unable to query value");
assert_eq!(
expected,
items.first().unwrap().value.to_string(),
"DECIMAL({}, {}) sent: {}",
precision,
scale,
sent
);
}
}
}
}
13 changes: 6 additions & 7 deletions src/db.rs → src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ impl Decimal {
}
}

#[cfg(feature = "diesel")]
mod diesel {
#[cfg(feature = "db-diesel-postgres")]
mod diesel_postgres {
use super::*;
use ::diesel::{
deserialize::{self, FromSql},
Expand Down Expand Up @@ -205,8 +205,8 @@ mod diesel {
}

impl From<Decimal> for PgNumeric {
fn from(bigdecimal: Decimal) -> Self {
(&bigdecimal).into()
fn from(decimal: Decimal) -> Self {
(&decimal).into()
}
}

Expand All @@ -224,7 +224,7 @@ mod diesel {
}

#[cfg(test)]
mod pg_tests {
mod tests {
use super::*;
use core::str::FromStr;

Expand Down Expand Up @@ -349,7 +349,6 @@ mod diesel {
}

#[test]
#[cfg(feature = "unstable")]
fn decimal_to_pg_numeric_retains_sign() {
let decimal = Decimal::from_str("123.456").unwrap();
let expected = PgNumeric::Positive {
Expand Down Expand Up @@ -460,7 +459,7 @@ mod diesel {
}
}

#[cfg(feature = "postgres")]
#[cfg(any(feature = "db-postgres", feature = "db-tokio-postgres"))]
mod postgres {
use super::*;
use ::postgres::types::{to_sql_checked, FromSql, IsNull, ToSql, Type};
Expand Down

0 comments on commit b4e72bc

Please sign in to comment.