From 217dd1a44c53835c78ab27e77be2600dbae14bd7 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Sun, 7 Jan 2018 08:18:46 -0700 Subject: [PATCH 01/25] Change MySQL's number handling to be more permissive MySQL is a bit more... lax with types than other backends. It's very easy to accidentally get a 64 bit integer or decimal when other backends would continue to give you a 32 bit integer. Right now we're relying on conversion happening in `libmysqlclient` for `query_by_index`. If we ever want to dump `libmysqlclient`, we will need to move those conversions into Diesel. However, I've opted to do this in such a way that it affects `sql_query` as well. Right now there are many differences between what our query builder says and what MySQL does. (32 bit addition/subtraction returns 64 bit, 32 bit multiplication/division/sum return decimal). Ideally you should be able to have a struct that derives both `Queryable` and `QueryableByName`, and have that work with the same query built using the query builder or `sql_query`. In order for that to happen, we can't do the conversions until we hit `FromSql`, since for `sql_query` that is the first and only time we learn what the expected type is. --- diesel/src/mysql/backend.rs | 2 + diesel/src/mysql/connection/bind.rs | 104 +++++++++++++------ diesel/src/mysql/mod.rs | 2 +- diesel/src/mysql/types/date_and_time.rs | 70 +++++++------ diesel/src/mysql/types/mod.rs | 1 + diesel/src/mysql/types/numeric.rs | 23 +++-- diesel/src/mysql/types/primitives.rs | 116 ++++++++++++++++++++++ diesel/src/mysql/value.rs | 70 +++++++++++-- diesel/src/sql_types/mod.rs | 2 +- diesel_derives/tests/queryable_by_name.rs | 41 +++----- 10 files changed, 324 insertions(+), 107 deletions(-) create mode 100644 diesel/src/mysql/types/primitives.rs diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index a8ceb3351e46..59a5efddb14a 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -50,6 +50,8 @@ pub enum MysqlType { Float, /// Sets `buffer_type` to `MYSQL_TYPE_DOUBLE` Double, + /// Sets `buffer_type` to `MYSQL_TYPE_NEWDECIMAL` + Numeric, /// Sets `buffer_type` to `MYSQL_TYPE_TIME` Time, /// Sets `buffer_type` to `MYSQL_TYPE_DATE` diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index abf9f12d732e..99d757b38ce8 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -29,12 +29,7 @@ impl Binds { pub fn from_output_types(types: Vec) -> Self { let data = types .into_iter() - .map(|metadata| { - ( - mysql_type_to_ffi_type(metadata.data_type), - metadata.is_unsigned as _, - ) - }) + .map(|metadata| (metadata.data_type.into(), metadata.is_unsigned as _)) .map(BindData::for_output) .collect(); @@ -87,7 +82,17 @@ impl Binds { } pub fn field_data(&self, idx: usize) -> Option> { - self.data[idx].bytes().map(MysqlValue::new) + let data = &self.data[idx]; + let tpe = data.tpe.into(); + self.data[idx].bytes().map(|bytes| { + MysqlValue::new( + bytes, + MysqlTypeMetadata { + data_type: tpe, + is_unsigned: data.is_unsigned != 0, + }, + ) + }) } } @@ -107,10 +112,10 @@ impl BindData { let length = bytes.len() as libc::c_ulong; BindData { - tpe: mysql_type_to_ffi_type(tpe), - bytes: bytes, - length: length, - is_null: is_null, + tpe: tpe.into(), + bytes, + length, + is_null, is_truncated: None, is_unsigned, } @@ -123,9 +128,9 @@ impl BindData { let length = bytes.len() as libc::c_ulong; BindData { - tpe: tpe, - bytes: bytes, - length: length, + tpe, + bytes, + length, is_null: 0, is_truncated: Some(0), is_unsigned, @@ -213,22 +218,63 @@ impl BindData { } } -fn mysql_type_to_ffi_type(tpe: MysqlType) -> ffi::enum_field_types { - use self::ffi::enum_field_types::*; +impl From for ffi::enum_field_types { + fn from(tpe: MysqlType) -> Self { + use self::ffi::enum_field_types::*; + + match tpe { + MysqlType::Tiny => MYSQL_TYPE_TINY, + MysqlType::Short => MYSQL_TYPE_SHORT, + MysqlType::Long => MYSQL_TYPE_LONG, + MysqlType::LongLong => MYSQL_TYPE_LONGLONG, + MysqlType::Float => MYSQL_TYPE_FLOAT, + MysqlType::Double => MYSQL_TYPE_DOUBLE, + MysqlType::Time => MYSQL_TYPE_TIME, + MysqlType::Date => MYSQL_TYPE_DATE, + MysqlType::DateTime => MYSQL_TYPE_DATETIME, + MysqlType::Timestamp => MYSQL_TYPE_TIMESTAMP, + MysqlType::String => MYSQL_TYPE_STRING, + MysqlType::Blob => MYSQL_TYPE_BLOB, + MysqlType::Numeric => MYSQL_TYPE_NEWDECIMAL, + } + } +} - match tpe { - MysqlType::Tiny => MYSQL_TYPE_TINY, - MysqlType::Short => MYSQL_TYPE_SHORT, - MysqlType::Long => MYSQL_TYPE_LONG, - MysqlType::LongLong => MYSQL_TYPE_LONGLONG, - MysqlType::Float => MYSQL_TYPE_FLOAT, - MysqlType::Double => MYSQL_TYPE_DOUBLE, - MysqlType::Time => MYSQL_TYPE_TIME, - MysqlType::Date => MYSQL_TYPE_DATE, - MysqlType::DateTime => MYSQL_TYPE_DATETIME, - MysqlType::Timestamp => MYSQL_TYPE_TIMESTAMP, - MysqlType::String => MYSQL_TYPE_STRING, - MysqlType::Blob => MYSQL_TYPE_BLOB, +impl From for MysqlType { + fn from(tpe: ffi::enum_field_types) -> Self { + use self::ffi::enum_field_types::*; + + match tpe { + MYSQL_TYPE_TINY => MysqlType::Tiny, + MYSQL_TYPE_SHORT => MysqlType::Short, + MYSQL_TYPE_INT24 | MYSQL_TYPE_LONG => MysqlType::Long, + MYSQL_TYPE_LONGLONG => MysqlType::LongLong, + MYSQL_TYPE_FLOAT => MysqlType::Float, + MYSQL_TYPE_DOUBLE => MysqlType::Double, + MYSQL_TYPE_TIME => MysqlType::Time, + MYSQL_TYPE_DATE => MysqlType::Date, + MYSQL_TYPE_DATETIME => MysqlType::DateTime, + MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, + MYSQL_TYPE_STRING => MysqlType::String, + MYSQL_TYPE_BLOB => MysqlType::Blob, + MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, + MYSQL_TYPE_NULL + | MYSQL_TYPE_YEAR + | MYSQL_TYPE_NEWDATE + | MYSQL_TYPE_VARCHAR + | MYSQL_TYPE_BIT + | MYSQL_TYPE_TIMESTAMP2 + | MYSQL_TYPE_DATETIME2 + | MYSQL_TYPE_TIME2 + | MYSQL_TYPE_JSON + | MYSQL_TYPE_ENUM + | MYSQL_TYPE_SET + | MYSQL_TYPE_TINY_BLOB + | MYSQL_TYPE_MEDIUM_BLOB + | MYSQL_TYPE_LONG_BLOB + | MYSQL_TYPE_VAR_STRING + | MYSQL_TYPE_GEOMETRY => unimplemented!(), + } } } diff --git a/diesel/src/mysql/mod.rs b/diesel/src/mysql/mod.rs index 6fcf29a466aa..a8d46fd0ec2a 100644 --- a/diesel/src/mysql/mod.rs +++ b/diesel/src/mysql/mod.rs @@ -14,4 +14,4 @@ pub mod types; pub use self::backend::{Mysql, MysqlType, MysqlTypeMetadata}; pub use self::connection::MysqlConnection; pub use self::query_builder::MysqlQueryBuilder; -pub use self::value::MysqlValue; +pub use self::value::{MysqlValue, NumericRepresentation}; diff --git a/diesel/src/mysql/types/date_and_time.rs b/diesel/src/mysql/types/date_and_time.rs index 162645a414a2..c345b342d7cf 100644 --- a/diesel/src/mysql/types/date_and_time.rs +++ b/diesel/src/mysql/types/date_and_time.rs @@ -4,7 +4,7 @@ extern crate mysqlclient_sys as ffi; use self::chrono::*; use std::io::Write; use std::os::raw as libc; -use std::{mem, ptr, slice}; +use std::{mem, slice}; use crate::deserialize::{self, FromSql}; use crate::mysql::{Mysql, MysqlValue}; @@ -26,18 +26,8 @@ macro_rules! mysql_time_impls { impl FromSql<$ty, Mysql> for ffi::MYSQL_TIME { fn from_sql(value: Option>) -> deserialize::Result { - let value = not_none!(value); - let bytes_ptr = value.as_bytes().as_ptr() as *const ffi::MYSQL_TIME; - unsafe { - let mut result = mem::MaybeUninit::uninit(); - ptr::copy_nonoverlapping(bytes_ptr, result.as_mut_ptr(), 1); - let result = result.assume_init(); - if result.neg == 0 { - Ok(result) - } else { - Err("Negative dates/times are not yet supported".into()) - } - } + let data = not_none!(value); + data.time_value() } } }; @@ -62,15 +52,17 @@ impl FromSql for NaiveDateTime { impl ToSql for NaiveDateTime { fn to_sql(&self, out: &mut Output) -> serialize::Result { - let mut mysql_time: ffi::MYSQL_TIME = unsafe { mem::zeroed() }; - - mysql_time.year = self.year() as libc::c_uint; - mysql_time.month = self.month() as libc::c_uint; - mysql_time.day = self.day() as libc::c_uint; - mysql_time.hour = self.hour() as libc::c_uint; - mysql_time.minute = self.minute() as libc::c_uint; - mysql_time.second = self.second() as libc::c_uint; - mysql_time.second_part = libc::c_ulong::from(self.timestamp_subsec_micros()); + let mysql_time = ffi::MYSQL_TIME { + year: self.year() as libc::c_uint, + month: self.month() as libc::c_uint, + day: self.day() as libc::c_uint, + hour: self.hour() as libc::c_uint, + minute: self.minute() as libc::c_uint, + second: self.second() as libc::c_uint, + second_part: libc::c_ulong::from(self.timestamp_subsec_micros()), + neg: 0, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATETIME, + }; >::to_sql(&mysql_time, out) } @@ -98,12 +90,18 @@ impl FromSql for NaiveDateTime { } impl ToSql for NaiveTime { - fn to_sql(&self, out: &mut Output) -> serialize::Result { - let mut mysql_time: ffi::MYSQL_TIME = unsafe { mem::zeroed() }; - - mysql_time.hour = self.hour() as libc::c_uint; - mysql_time.minute = self.minute() as libc::c_uint; - mysql_time.second = self.second() as libc::c_uint; + fn to_sql(&self, out: &mut serialize::Output) -> serialize::Result { + let mysql_time = ffi::MYSQL_TIME { + hour: self.hour() as libc::c_uint, + minute: self.minute() as libc::c_uint, + second: self.second() as libc::c_uint, + day: 0, + month: 0, + second_part: 0, + year: 0, + neg: 0, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_TIME, + }; >::to_sql(&mysql_time, out) } @@ -123,11 +121,17 @@ impl FromSql for NaiveTime { impl ToSql for NaiveDate { fn to_sql(&self, out: &mut Output) -> serialize::Result { - let mut mysql_time: ffi::MYSQL_TIME = unsafe { mem::zeroed() }; - - mysql_time.year = self.year() as libc::c_uint; - mysql_time.month = self.month() as libc::c_uint; - mysql_time.day = self.day() as libc::c_uint; + let mysql_time = ffi::MYSQL_TIME { + year: self.year() as libc::c_uint, + month: self.month() as libc::c_uint, + day: self.day() as libc::c_uint, + hour: 0, + minute: 0, + second: 0, + second_part: 0, + neg: 0, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATE, + }; >::to_sql(&mysql_time, out) } diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index de593a142198..6208e625080d 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -3,6 +3,7 @@ #[cfg(feature = "chrono")] mod date_and_time; mod numeric; +mod primitives; use byteorder::WriteBytesExt; use std::io::Write; diff --git a/diesel/src/mysql/types/numeric.rs b/diesel/src/mysql/types/numeric.rs index 2a99bf70e791..38b6f23d9315 100644 --- a/diesel/src/mysql/types/numeric.rs +++ b/diesel/src/mysql/types/numeric.rs @@ -5,11 +5,10 @@ pub mod bigdecimal { use self::bigdecimal::BigDecimal; use std::io::prelude::*; - use crate::backend; use crate::deserialize::{self, FromSql}; - use crate::mysql::Mysql; + use crate::mysql::{Mysql, MysqlValue}; use crate::serialize::{self, IsNull, Output, ToSql}; - use crate::sql_types::{Binary, Numeric}; + use crate::sql_types::Numeric; impl ToSql for BigDecimal { fn to_sql(&self, out: &mut Output) -> serialize::Result { @@ -20,11 +19,19 @@ pub mod bigdecimal { } impl FromSql for BigDecimal { - fn from_sql(bytes: Option>) -> deserialize::Result { - let bytes_ptr = <*const [u8] as FromSql>::from_sql(bytes)?; - let bytes = unsafe { &*bytes_ptr }; - BigDecimal::parse_bytes(bytes, 10) - .ok_or_else(|| Box::from(format!("{:?} is not valid decimal number ", bytes))) + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x.into()), + Medium(x) => Ok(x.into()), + Big(x) => Ok(x.into()), + Float(x) => Ok(x.into()), + Double(x) => Ok(x.into()), + Decimal(bytes) => BigDecimal::parse_bytes(bytes, 10) + .ok_or_else(|| format!("{:?} is not valid decimal number ", bytes).into()), + } } } } diff --git a/diesel/src/mysql/types/primitives.rs b/diesel/src/mysql/types/primitives.rs new file mode 100644 index 000000000000..645ff3e47bbe --- /dev/null +++ b/diesel/src/mysql/types/primitives.rs @@ -0,0 +1,116 @@ +use std::str; + +use crate::deserialize::{self, FromSql}; +use crate::mysql::{Mysql, MysqlValue}; +use crate::sql_types::{BigInt, Binary, Double, Float, Integer, SmallInt, Text}; + +impl FromSql for i16 { + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x), + Medium(x) => Ok(x as Self), + Big(x) => Ok(x as Self), + Float(x) => Ok(x as Self), + Double(x) => Ok(x as Self), + Decimal(bytes) => { + let string = str::from_utf8(bytes)?; + let integer_portion = string.split('.').nth(0).unwrap_or_default(); + Ok(integer_portion.parse()?) + } + } + } +} + +impl FromSql for i32 { + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x.into()), + Medium(x) => Ok(x), + Big(x) => Ok(x as Self), + Float(x) => Ok(x as Self), + Double(x) => Ok(x as Self), + Decimal(bytes) => { + let string = str::from_utf8(bytes)?; + let integer_portion = string.split('.').nth(0).unwrap_or_default(); + Ok(integer_portion.parse()?) + } + } + } +} + +impl FromSql for i64 { + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x.into()), + Medium(x) => Ok(x.into()), + Big(x) => Ok(x), + Float(x) => Ok(x as Self), + Double(x) => Ok(x as Self), + Decimal(bytes) => { + let string = str::from_utf8(bytes)?; + let integer_portion = string.split('.').nth(0).unwrap_or_default(); + Ok(integer_portion.parse()?) + } + } + } +} + +impl FromSql for f32 { + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x.into()), + Medium(x) => Ok(x as Self), + Big(x) => Ok(x as Self), + Float(x) => Ok(x), + Double(x) => Ok(x as Self), + Decimal(bytes) => Ok(str::from_utf8(bytes)?.parse()?), + } + } +} + +impl FromSql for f64 { + fn from_sql(value: Option>) -> deserialize::Result { + use crate::mysql::NumericRepresentation::*; + + let data = not_none!(value); + match data.numeric_value()? { + Tiny(x) => Ok(x.into()), + Small(x) => Ok(x.into()), + Medium(x) => Ok(x.into()), + Big(x) => Ok(x as Self), + Float(x) => Ok(x.into()), + Double(x) => Ok(x), + Decimal(bytes) => Ok(str::from_utf8(bytes)?.parse()?), + } + } +} + +impl FromSql for String { + fn from_sql(value: Option>) -> deserialize::Result { + let value = not_none!(value); + String::from_utf8(value.as_bytes().into()).map_err(Into::into) + } +} + +impl FromSql for Vec { + fn from_sql(value: Option>) -> deserialize::Result { + let value = not_none!(value); + Ok(value.as_bytes().into()) + } +} diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index 6df6dc3f13ff..1db5df735623 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -1,25 +1,79 @@ -use super::Mysql; -use crate::backend::BinaryRawValue; +use super::{MysqlType, MysqlTypeMetadata}; +use crate::deserialize; +use mysqlclient_sys as ffi; +use std::error::Error; /// Raw mysql value as received from the database #[derive(Copy, Clone, Debug)] pub struct MysqlValue<'a> { raw: &'a [u8], + tpe: MysqlTypeMetadata, } impl<'a> MysqlValue<'a> { - pub(crate) fn new(raw: &'a [u8]) -> Self { - Self { raw } + pub(crate) fn new(raw: &'a [u8], tpe: MysqlTypeMetadata) -> Self { + Self { raw, tpe } } /// Get the underlying raw byte representation pub fn as_bytes(&self) -> &[u8] { self.raw } -} -impl<'a> BinaryRawValue<'a> for Mysql { - fn as_bytes(value: Self::RawValue) -> &'a [u8] { - value.raw + /// Checks that the type code is valid, and interprets the data as a + /// `MYSQL_TIME` pointer + pub(crate) fn time_value(&self) -> deserialize::Result { + match self.tpe.data_type { + MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { + Ok(*unsafe { &*(self.raw as *const _ as *const ffi::MYSQL_TIME) }) + } + _ => Err(self.invalid_type_code("timestamp")), + } + } + + /// Returns the numeric representation of this value, based on the type code. + /// Returns an error if the type code is not numeric. + pub(crate) fn numeric_value(&self) -> deserialize::Result { + use self::NumericRepresentation::*; + use std::convert::TryInto; + + Ok(match self.tpe.data_type { + MysqlType::Tiny => Tiny(self.raw[0] as i8), + MysqlType::Short => Small(i16::from_ne_bytes(self.raw.try_into()?)), + MysqlType::Long => Medium(i32::from_ne_bytes(self.raw.try_into()?)), + MysqlType::LongLong => Big(i64::from_ne_bytes(self.raw.try_into()?)), + MysqlType::Float => Float(f32::from_ne_bytes(self.raw.try_into()?)), + MysqlType::Double => Double(f64::from_ne_bytes(self.raw.try_into()?)), + + MysqlType::Numeric => Decimal(self.raw), + _ => return Err(self.invalid_type_code("number")), + }) } + + fn invalid_type_code(&self, expected: &str) -> Box { + format!( + "Invalid representation received for {}: {:?}", + expected, self.tpe + ) + .into() + } +} + +/// Represents all possible forms MySQL transmits integers +#[derive(Debug, Clone, Copy)] +pub enum NumericRepresentation<'a> { + /// Correponds to `MYSQL_TYPE_TINY` + Tiny(i8), + /// Correponds to `MYSQL_TYPE_SHORT` + Small(i16), + /// Correponds to `MYSQL_TYPE_INT24` and `MYSQL_TYPE_LONG` + Medium(i32), + /// Correponds to `MYSQL_TYPE_LONGLONG` + Big(i64), + /// Correponds to `MYSQL_TYPE_FLOAT` + Float(f32), + /// Correponds to `MYSQL_TYPE_DOUBLE` + Double(f64), + /// Correponds to `MYSQL_TYPE_DECIMAL` and `MYSQL_TYPE_NEWDECIMAL` + Decimal(&'a [u8]), } diff --git a/diesel/src/sql_types/mod.rs b/diesel/src/sql_types/mod.rs index 9cd8e981461d..a5bb16171f67 100644 --- a/diesel/src/sql_types/mod.rs +++ b/diesel/src/sql_types/mod.rs @@ -178,7 +178,7 @@ pub type Float8 = Double; /// [`bigdecimal::BigDecimal`]: /bigdecimal/struct.BigDecimal.html #[derive(Debug, Clone, Copy, Default, QueryId, SqlType)] #[postgres(oid = "1700", array_oid = "1231")] -#[mysql_type = "String"] +#[mysql_type = "Numeric"] #[sqlite_type = "Double"] pub struct Numeric; diff --git a/diesel_derives/tests/queryable_by_name.rs b/diesel_derives/tests/queryable_by_name.rs index 0b53e5188ade..acddaf4a9653 100644 --- a/diesel_derives/tests/queryable_by_name.rs +++ b/diesel_derives/tests/queryable_by_name.rs @@ -1,22 +1,12 @@ +use diesel::sql_types::Integer; use diesel::*; use helpers::connection; -#[cfg(feature = "mysql")] -type IntSql = ::diesel::sql_types::BigInt; -#[cfg(feature = "mysql")] -type IntRust = i64; - -#[cfg(not(feature = "mysql"))] -type IntSql = ::diesel::sql_types::Integer; -#[cfg(not(feature = "mysql"))] -type IntRust = i32; - table! { - use super::IntSql; my_structs (foo) { - foo -> IntSql, - bar -> IntSql, + foo -> Integer, + bar -> Integer, } } @@ -25,8 +15,8 @@ fn named_struct_definition() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] struct MyStruct { - foo: IntRust, - bar: IntRust, + foo: i32, + bar: i32, } let conn = connection(); @@ -38,10 +28,7 @@ fn named_struct_definition() { fn tuple_struct() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] - struct MyStruct( - #[column_name = "foo"] IntRust, - #[column_name = "bar"] IntRust, - ); + struct MyStruct(#[column_name = "foo"] i32, #[column_name = "bar"] i32); let conn = connection(); let data = sql_query("SELECT 1 AS foo, 2 AS bar").get_result(&conn); @@ -54,10 +41,10 @@ fn tuple_struct() { fn struct_with_no_table() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] struct MyStructNamedSoYouCantInferIt { - #[sql_type = "IntSql"] - foo: IntRust, - #[sql_type = "IntSql"] - bar: IntRust, + #[sql_type = "Integer"] + foo: i32, + #[sql_type = "Integer"] + bar: i32, } let conn = connection(); @@ -70,7 +57,7 @@ fn embedded_struct() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] struct A { - foo: IntRust, + foo: i32, #[diesel(embed)] b: B, } @@ -78,7 +65,7 @@ fn embedded_struct() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] struct B { - bar: IntRust, + bar: i32, } let conn = connection(); @@ -97,7 +84,7 @@ fn embedded_option() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] struct A { - foo: IntRust, + foo: i32, #[diesel(embed)] b: Option, } @@ -105,7 +92,7 @@ fn embedded_option() { #[derive(Debug, Clone, Copy, PartialEq, Eq, QueryableByName)] #[table_name = "my_structs"] struct B { - bar: IntRust, + bar: i32, } let conn = connection(); From a16c5765770ba8d7b955c173531cbe64ba0471f2 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Mon, 17 Feb 2020 23:02:04 +0100 Subject: [PATCH 02/25] Allow dead code in mysql if chrono feature is disabled --- diesel/src/mysql/value.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index 1db5df735623..0172fd14acae 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -22,6 +22,7 @@ impl<'a> MysqlValue<'a> { /// Checks that the type code is valid, and interprets the data as a /// `MYSQL_TIME` pointer + #[allow(dead_code)] pub(crate) fn time_value(&self) -> deserialize::Result { match self.tpe.data_type { MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { From 5b80a181b786808961869d4090c24ba1cfe67294 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Mon, 17 Feb 2020 23:02:31 +0100 Subject: [PATCH 03/25] Improve type mapping in mysql backend Also add some links to relevant mysql/mariadb documentation there --- diesel/src/mysql/connection/bind.rs | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 99d757b38ce8..326953e1a3c4 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -244,9 +244,13 @@ impl From for MysqlType { fn from(tpe: ffi::enum_field_types) -> Self { use self::ffi::enum_field_types::*; + // https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/c-api-data-structures.html + // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/binary__log__types_8h.html + // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html + // https://mariadb.com/kb/en/packet_bindata/ match tpe { MYSQL_TYPE_TINY => MysqlType::Tiny, - MYSQL_TYPE_SHORT => MysqlType::Short, + MYSQL_TYPE_YEAR | MYSQL_TYPE_SHORT => MysqlType::Short, MYSQL_TYPE_INT24 | MYSQL_TYPE_LONG => MysqlType::Long, MYSQL_TYPE_LONGLONG => MysqlType::LongLong, MYSQL_TYPE_FLOAT => MysqlType::Float, @@ -255,25 +259,42 @@ impl From for MysqlType { MYSQL_TYPE_DATE => MysqlType::Date, MYSQL_TYPE_DATETIME => MysqlType::DateTime, MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, - MYSQL_TYPE_STRING => MysqlType::String, - MYSQL_TYPE_BLOB => MysqlType::Blob, - MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, - MYSQL_TYPE_NULL - | MYSQL_TYPE_YEAR - | MYSQL_TYPE_NEWDATE - | MYSQL_TYPE_VARCHAR - | MYSQL_TYPE_BIT - | MYSQL_TYPE_TIMESTAMP2 - | MYSQL_TYPE_DATETIME2 - | MYSQL_TYPE_TIME2 - | MYSQL_TYPE_JSON - | MYSQL_TYPE_ENUM - | MYSQL_TYPE_SET + MYSQL_TYPE_VAR_STRING | MYSQL_TYPE_VARCHAR | MYSQL_TYPE_STRING => MysqlType::String, + MYSQL_TYPE_BLOB | MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB - | MYSQL_TYPE_LONG_BLOB - | MYSQL_TYPE_VAR_STRING - | MYSQL_TYPE_GEOMETRY => unimplemented!(), + | MYSQL_TYPE_LONG_BLOB => MysqlType::Blob, + MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, + // Null value + MYSQL_TYPE_NULL | + // bit type + // same encoding as string + MYSQL_TYPE_BIT | + // enum type + // same encoding as string + MYSQL_TYPE_ENUM | + // set type + // same encoding as string + MYSQL_TYPE_SET | + // spatial type + // same encoding as string + MYSQL_TYPE_GEOMETRY | + // json type, only available on newer mysql versions + // same encoding as string + MYSQL_TYPE_JSON => unimplemented!( + "Hit currently unsupported type, those variants should \ + probably be variants of MysqlType or at least be mapped \ + to one of the existing types" + ), + MYSQL_TYPE_NEWDATE + | MYSQL_TYPE_TIME2 + | MYSQL_TYPE_DATETIME2 + | MYSQL_TYPE_TIMESTAMP2 => panic!( + "The mysql documentation states that this types are \ + only used on server side, so if you see this error \ + something has gone wrong. Please open a issue at \ + the diesel github repo." + ), } } } From 3cc4192dcecc67c342bf18b6f796cf93e3f874d1 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 19 Feb 2020 00:18:09 +0100 Subject: [PATCH 04/25] Only convert the mysql ffi type if it's not null --- diesel/src/mysql/connection/bind.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 326953e1a3c4..ee0bcfadd740 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -83,8 +83,8 @@ impl Binds { pub fn field_data(&self, idx: usize) -> Option> { let data = &self.data[idx]; - let tpe = data.tpe.into(); self.data[idx].bytes().map(|bytes| { + let tpe = data.tpe.into(); MysqlValue::new( bytes, MysqlTypeMetadata { @@ -266,7 +266,11 @@ impl From for MysqlType { | MYSQL_TYPE_LONG_BLOB => MysqlType::Blob, MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, // Null value - MYSQL_TYPE_NULL | + MYSQL_TYPE_NULL => unreachable!( + "We ensure at the call side that we do not hit this type here. \ + If you ever see this error, something has gone very wrong. \ + Please open a issue at the diesel github repo in this case" + ), // bit type // same encoding as string MYSQL_TYPE_BIT | @@ -289,7 +293,7 @@ impl From for MysqlType { MYSQL_TYPE_NEWDATE | MYSQL_TYPE_TIME2 | MYSQL_TYPE_DATETIME2 - | MYSQL_TYPE_TIMESTAMP2 => panic!( + | MYSQL_TYPE_TIMESTAMP2 => unreachable!( "The mysql documentation states that this types are \ only used on server side, so if you see this error \ something has gone wrong. Please open a issue at \ From b285e5c2f35003fe72ff53a81d0310084b38be00 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 19 Feb 2020 23:29:24 +0100 Subject: [PATCH 05/25] Improve conversion of the received mysql value bytes to MYSQL_TIME Clippy (rightful) complained that we read from a potential unaligned pointer, which would be undefined behaviour. The problem is: Clippy continues to complain about this, even if we use `read_unaligned()` here. This seems to be a clippy bug, see https://github.com/rust-lang/rust-clippy/issues/2881 --- diesel/src/mysql/value.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index 0172fd14acae..df63e4a38a79 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -22,11 +22,20 @@ impl<'a> MysqlValue<'a> { /// Checks that the type code is valid, and interprets the data as a /// `MYSQL_TIME` pointer - #[allow(dead_code)] + // We use `ptr.read_unaligned()` to read the potential unaligned ptr, + // so clippy is clearly wrong here + // https://github.com/rust-lang/rust-clippy/issues/2881 + #[allow(dead_code, clippy::cast_ptr_alignment)] pub(crate) fn time_value(&self) -> deserialize::Result { match self.tpe.data_type { MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { - Ok(*unsafe { &*(self.raw as *const _ as *const ffi::MYSQL_TIME) }) + let ptr = self.raw.as_ptr() as *const ffi::MYSQL_TIME; + let result = unsafe { ptr.read_unaligned() }; + if result.neg == 0 { + Ok(result) + } else { + Err("Negative dates/times are not yet supported".into()) + } } _ => Err(self.invalid_type_code("timestamp")), } From 4cd1db8c334c7ac1b0d8003b1c5441a7fa7771ab Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Thu, 20 Feb 2020 16:23:30 +0100 Subject: [PATCH 06/25] Improve the conversion from decimal to various integer types in the mysql backend --- diesel/src/mysql/types/primitives.rs | 44 ++++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/diesel/src/mysql/types/primitives.rs b/diesel/src/mysql/types/primitives.rs index 645ff3e47bbe..6e6d9c0f5733 100644 --- a/diesel/src/mysql/types/primitives.rs +++ b/diesel/src/mysql/types/primitives.rs @@ -1,9 +1,33 @@ -use std::str; +use std::error::Error; +use std::str::{self, FromStr}; use crate::deserialize::{self, FromSql}; use crate::mysql::{Mysql, MysqlValue}; use crate::sql_types::{BigInt, Binary, Double, Float, Integer, SmallInt, Text}; +fn decimal_to_integer(bytes: &[u8]) -> deserialize::Result +where + T: FromStr, + T::Err: Error + Send + Sync + 'static, +{ + let string = str::from_utf8(bytes)?; + let mut splited = string.split('.'); + let integer_portion = splited.next().unwrap_or_default(); + let decimal_portion = splited.next().unwrap_or_default(); + if splited.next().is_some() { + Err(format!("Invalid decimal format: {:?}", string).into()) + } else if decimal_portion.chars().any(|c| c != '0') { + Err(format!( + "Tried to convert a decimal to an integer that contained / + a non null decimal portion: {:?}", + string + ) + .into()) + } else { + Ok(integer_portion.parse()?) + } +} + impl FromSql for i16 { fn from_sql(value: Option>) -> deserialize::Result { use crate::mysql::NumericRepresentation::*; @@ -16,11 +40,7 @@ impl FromSql for i16 { Big(x) => Ok(x as Self), Float(x) => Ok(x as Self), Double(x) => Ok(x as Self), - Decimal(bytes) => { - let string = str::from_utf8(bytes)?; - let integer_portion = string.split('.').nth(0).unwrap_or_default(); - Ok(integer_portion.parse()?) - } + Decimal(bytes) => decimal_to_integer(bytes), } } } @@ -37,11 +57,7 @@ impl FromSql for i32 { Big(x) => Ok(x as Self), Float(x) => Ok(x as Self), Double(x) => Ok(x as Self), - Decimal(bytes) => { - let string = str::from_utf8(bytes)?; - let integer_portion = string.split('.').nth(0).unwrap_or_default(); - Ok(integer_portion.parse()?) - } + Decimal(bytes) => decimal_to_integer(bytes), } } } @@ -58,11 +74,7 @@ impl FromSql for i64 { Big(x) => Ok(x), Float(x) => Ok(x as Self), Double(x) => Ok(x as Self), - Decimal(bytes) => { - let string = str::from_utf8(bytes)?; - let integer_portion = string.split('.').nth(0).unwrap_or_default(); - Ok(integer_portion.parse()?) - } + Decimal(bytes) => decimal_to_integer(bytes), } } } From de0b452a2e3b66960429c2e27258cf5a9a5f7154 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Mon, 11 May 2020 18:43:50 +0200 Subject: [PATCH 07/25] Write some bind tests for mysql --- diesel/Cargo.toml | 4 +- diesel/src/mysql/backend.rs | 4 + diesel/src/mysql/connection/bind.rs | 1046 +++++++++++++++++- diesel/src/mysql/connection/stmt/iterator.rs | 10 +- diesel/src/mysql/connection/stmt/mod.rs | 6 +- diesel/src/mysql/value.rs | 3 +- 6 files changed, 1035 insertions(+), 38 deletions(-) diff --git a/diesel/Cargo.toml b/diesel/Cargo.toml index bc86a64065a2..f4a9103b88e4 100644 --- a/diesel/Cargo.toml +++ b/diesel/Cargo.toml @@ -43,7 +43,7 @@ dotenv = "0.15" quickcheck = "0.9" [features] -default = ["with-deprecated", "32-column-tables"] +default = ["mysql", "extras"] extras = ["chrono", "serde_json", "uuid", "deprecated-time", "network-address", "numeric", "r2d2"] unstable = ["diesel_derives/nightly"] large-tables = ["32-column-tables"] @@ -53,7 +53,7 @@ huge-tables = ["64-column-tables"] 128-column-tables = ["64-column-tables"] postgres = ["pq-sys", "bitflags", "diesel_derives/postgres"] sqlite = ["libsqlite3-sys", "diesel_derives/sqlite"] -mysql = ["mysqlclient-sys", "url", "percent-encoding", "diesel_derives/mysql"] +mysql = ["mysqlclient-sys", "url", "percent-encoding", "diesel_derives/mysql", "bitflags"] with-deprecated = [] deprecated-time = ["time"] network-address = ["ipnetwork", "libc"] diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index 59a5efddb14a..6f6964a69cd2 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -42,6 +42,8 @@ pub enum MysqlType { Tiny, /// Sets `buffer_type` to `MYSQL_TYPE_SHORT` Short, + /// Sets `buffer_type` to `MYSQL_TYPE_INT24` + Medium, /// Sets `buffer_type` to `MYSQL_TYPE_LONG` Long, /// Sets `buffer_type` to `MYSQL_TYPE_LONGLONG` @@ -64,6 +66,8 @@ pub enum MysqlType { String, /// Sets `buffer_type` to `MYSQL_TYPE_BLOB` Blob, + /// Sets `buffer_type` to `MYSQL_TYPE_BIT` + Bit, } impl Backend for Mysql { diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index ee0bcfadd740..2a2b9704779e 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -7,6 +7,7 @@ use super::stmt::Statement; use crate::mysql::{MysqlType, MysqlTypeMetadata, MysqlValue}; use crate::result::QueryResult; +#[derive(Debug)] pub struct Binds { data: Vec, } @@ -29,7 +30,16 @@ impl Binds { pub fn from_output_types(types: Vec) -> Self { let data = types .into_iter() - .map(|metadata| (metadata.data_type.into(), metadata.is_unsigned as _)) + .map(|metadata| { + ( + metadata.data_type.into(), + if metadata.is_unsigned { + Flags::UNSIGNED_FLAG + } else { + Flags::empty() + }, + ) + }) .map(BindData::for_output) .collect(); @@ -39,7 +49,12 @@ impl Binds { pub fn from_result_metadata(fields: &[ffi::MYSQL_FIELD]) -> Self { let data = fields .iter() - .map(|field| (field.type_, is_field_unsigned(field))) + .map(|field| { + ( + field.type_, + Flags::from_bits(field.flags).expect("No unknown flags"), + ) + }) .map(BindData::for_output) .collect(); @@ -89,24 +104,51 @@ impl Binds { bytes, MysqlTypeMetadata { data_type: tpe, - is_unsigned: data.is_unsigned != 0, + is_unsigned: data.flags.contains(Flags::UNSIGNED_FLAG), }, ) }) } } +bitflags::bitflags! { + struct Flags: u32 { + const NOT_NULL_FLAG = 1; + const PRI_KEY_FAG = 2; + const UNIQUE_KEY_FLAG = 4; + const MULTIPLE_KEY_FLAG = 8; + const BLOB_FLAG = 16; + const UNSIGNED_FLAG = 32; + const ZEROFILL_FLAG = 64; + const BINARY_FLAG = 128; + const ENUM_FLAG = 256; + const AUTO_INCREMENT_FLAG = 512; + const TIMESTAMP_FLAG = 1024; + const SET_FLAG = 2048; + const NO_DEFAULT_VALUE_FLAG = 4096; + const ON_UPDATE_NOW_FLAG = 8192; + const NUM_FLAG = 32768; + const PART_KEY_FLAG = 16384; + const GROUP_FLAG = 32768; + const UNIQUE_FLAG = 65536; + const BINCMP_FLAG = 130172; + const GET_FIXED_FIELDS_FLAG = (1<<18); + const FIELD_IN_PART_FUNC_FLAG = (1 << 19); + } +} + +#[derive(Debug)] struct BindData { tpe: ffi::enum_field_types, bytes: Vec, length: libc::c_ulong, + flags: Flags, is_null: ffi::my_bool, is_truncated: Option, - is_unsigned: ffi::my_bool, } impl BindData { - fn for_input(tpe: MysqlType, is_unsigned: ffi::my_bool, data: Option>) -> Self { + fn for_input(tpe: MysqlType, is_unsigned: bool, data: Option>) -> Self { let is_null = if data.is_none() { 1 } else { 0 }; let bytes = data.unwrap_or_default(); let length = bytes.len() as libc::c_ulong; @@ -117,11 +159,15 @@ impl BindData { length, is_null, is_truncated: None, - is_unsigned, + flags: if is_unsigned { + Flags::UNSIGNED_FLAG + } else { + Flags::empty() + }, } } - fn for_output((tpe, is_unsigned): (ffi::enum_field_types, ffi::my_bool)) -> Self { + fn for_output((tpe, flags): (ffi::enum_field_types, Flags)) -> Self { let bytes = known_buffer_size_for_ffi_type(tpe) .map(|len| vec![0; len]) .unwrap_or_default(); @@ -133,7 +179,7 @@ impl BindData { length, is_null: 0, is_truncated: Some(0), - is_unsigned, + flags, } } @@ -167,7 +213,7 @@ impl BindData { bind.buffer_length = self.bytes.capacity() as libc::c_ulong; bind.length = &mut self.length; bind.is_null = &mut self.is_null; - bind.is_unsigned = self.is_unsigned; + bind.is_unsigned = self.flags.contains(Flags::UNSIGNED_FLAG) as ffi::my_bool; if let Some(ref mut is_truncated) = self.is_truncated { bind.error = is_truncated; @@ -225,6 +271,7 @@ impl From for ffi::enum_field_types { match tpe { MysqlType::Tiny => MYSQL_TYPE_TINY, MysqlType::Short => MYSQL_TYPE_SHORT, + MysqlType::Medium => MYSQL_TYPE_INT24, MysqlType::Long => MYSQL_TYPE_LONG, MysqlType::LongLong => MYSQL_TYPE_LONGLONG, MysqlType::Float => MYSQL_TYPE_FLOAT, @@ -236,6 +283,7 @@ impl From for ffi::enum_field_types { MysqlType::String => MYSQL_TYPE_STRING, MysqlType::Blob => MYSQL_TYPE_BLOB, MysqlType::Numeric => MYSQL_TYPE_NEWDECIMAL, + MysqlType::Bit => MYSQL_TYPE_BIT, } } } @@ -251,7 +299,8 @@ impl From for MysqlType { match tpe { MYSQL_TYPE_TINY => MysqlType::Tiny, MYSQL_TYPE_YEAR | MYSQL_TYPE_SHORT => MysqlType::Short, - MYSQL_TYPE_INT24 | MYSQL_TYPE_LONG => MysqlType::Long, + MYSQL_TYPE_INT24 => MysqlType::Medium, + MYSQL_TYPE_LONG => MysqlType::Long, MYSQL_TYPE_LONGLONG => MysqlType::LongLong, MYSQL_TYPE_FLOAT => MysqlType::Float, MYSQL_TYPE_DOUBLE => MysqlType::Double, @@ -259,7 +308,13 @@ impl From for MysqlType { MYSQL_TYPE_DATE => MysqlType::Date, MYSQL_TYPE_DATETIME => MysqlType::DateTime, MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, - MYSQL_TYPE_VAR_STRING | MYSQL_TYPE_VARCHAR | MYSQL_TYPE_STRING => MysqlType::String, + MYSQL_TYPE_BIT => MysqlType::Bit, + MYSQL_TYPE_ENUM + | MYSQL_TYPE_SET + | MYSQL_TYPE_JSON + | MYSQL_TYPE_VAR_STRING + | MYSQL_TYPE_VARCHAR + | MYSQL_TYPE_STRING => MysqlType::String, MYSQL_TYPE_BLOB | MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB @@ -271,25 +326,14 @@ impl From for MysqlType { If you ever see this error, something has gone very wrong. \ Please open a issue at the diesel github repo in this case" ), - // bit type - // same encoding as string - MYSQL_TYPE_BIT | - // enum type - // same encoding as string - MYSQL_TYPE_ENUM | - // set type - // same encoding as string - MYSQL_TYPE_SET | // spatial type // same encoding as string - MYSQL_TYPE_GEOMETRY | - // json type, only available on newer mysql versions - // same encoding as string - MYSQL_TYPE_JSON => unimplemented!( + MYSQL_TYPE_GEOMETRY => unimplemented!( "Hit currently unsupported type, those variants should \ probably be variants of MysqlType or at least be mapped \ to one of the existing types" ), + MYSQL_TYPE_NEWDATE | MYSQL_TYPE_TIME2 | MYSQL_TYPE_DATETIME2 @@ -320,7 +364,955 @@ fn known_buffer_size_for_ffi_type(tpe: ffi::enum_field_types) -> Option { } } -fn is_field_unsigned(field: &ffi::MYSQL_FIELD) -> ffi::my_bool { - const UNSIGNED_FLAG: libc::c_uint = 32; - (field.flags & UNSIGNED_FLAG > 0) as _ +#[allow(warnings)] +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + use super::{MysqlTypeMetadata, MysqlValue}; + use crate::deserialize::FromSql; + use crate::mysql::connection::stmt::iterator::NamedStatementIterator; + use crate::sql_types::*; + + fn to_value( + bind: &BindData, + ) -> Result> + where + T: FromSql + std::fmt::Debug, + { + let meta = MysqlTypeMetadata { + data_type: bind.tpe.into(), + is_unsigned: bind.flags.contains(Flags::UNSIGNED_FLAG), + }; + + let value = MysqlValue::new(&bind.bytes, meta); + dbg!(T::from_sql(Some(value))) + } + + #[test] + fn check_all_the_types() { + let conn = crate::test_helpers::connection(); + + conn.execute("DROP TABLE IF EXISTS all_mysql_types CASCADE") + .unwrap(); + conn.execute( + "CREATE TABLE all_mysql_types ( + tiny_int TINYINT NOT NULL, + small_int SMALLINT NOT NULL, + medium_int MEDIUMINT NOT NULL, + int_col INTEGER NOT NULL, + big_int BIGINT NOT NULL, + unsigned_int INTEGER UNSIGNED NOT NULL, + zero_fill_int INTEGER ZEROFILL NOT NULL, + numeric_col NUMERIC(20,5) NOT NULL, + decimal_col DECIMAL(20,5) NOT NULL, + float_col FLOAT NOT NULL, + double_col DOUBLE NOT NULL, + bit_col BIT(8) NOT NULL, + date_col DATE NOT NULL, + date_time DATETIME NOT NULL, + timestamp_col TIMESTAMP NOT NULL, + time_col TIME NOT NULL, + year_col YEAR NOT NULL, + char_col CHAR(30) NOT NULL, + varchar_col VARCHAR(30) NOT NULL, + binary_col BINARY(30) NOT NULL, + varbinary_col VARBINARY(30) NOT NULL, + blob_col BLOB NOT NULL, + text_col TEXT NOT NULL, + enum_col ENUM('red', 'green', 'blue') NOT NULL, + set_col SET('one', 'two') NOT NULL, + geom GEOMETRY NOT NULL, + point_col POINT NOT NULL, + linestring_col LINESTRING NOT NULL, + polygon_col POLYGON NOT NULL, + multipoint_col MULTIPOINT NOT NULL, + multilinestring_col MULTILINESTRING NOT NULL, + multipolygon_col MULTIPOLYGON NOT NULL, + geometry_collection GEOMETRYCOLLECTION NOT NULL, + json_col JSON NOT NULL + )", + ) + .unwrap(); + conn + .execute( + "INSERT INTO all_mysql_types VALUES ( + 0, -- tiny_int + 1, -- small_int + 2, -- medium_int + 3, -- int_col + -5, -- big_int + 42, -- unsigned_int + 1, -- zero_fill_int + -999.999, -- numeric_col, + 3.14, -- decimal_col, + 1.23, -- float_col + 4.5678, -- double_col + b'10101010', -- bit_col + '1000-01-01', -- date_col + '9999-12-31 12:34:45.012345', -- date_time + '2020-01-01 10:10:10', -- timestamp_col + '23:01:01', -- time_col + 2020, -- year_col + 'abc', -- char_col + 'foo', -- varchar_col + 'a ', -- binary_col + 'a ', -- varbinary_col + 'binary', -- blob_col + 'some text whatever', -- text_col + 'red', -- enum_col + 'one', -- set_col + ST_GeomFromText('POINT(1 1)'), -- geom + ST_PointFromText('POINT(1 1)'), -- point_col + ST_LineStringFromText('LINESTRING(0 0,1 1,2 2)'), -- linestring_col + ST_PolygonFromText('POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7, 5 5))'), -- polygon_col + ST_MultiPointFromText('MULTIPOINT(0 0,10 10,10 20,20 20)'), -- multipoint_col + ST_MultiLineStringFromText('MULTILINESTRING((10 48,10 21,10 0),(16 0,16 23,16 48))'), -- multilinestring_col + ST_MultiPolygonFromText('MULTIPOLYGON(((28 26,28 0,84 0,84 42,28 26),(52 18,66 23,73 9,48 6,52 18)),((59 18,67 18,67 13,59 13,59 18)))'), -- multipolygon_col + ST_GeomCollFromText('GEOMETRYCOLLECTION(POINT(1 1),LINESTRING(0 0,1 1,2 2,3 3,4 4))'), -- geometry_collection + '{\"key1\": \"value1\", \"key2\": \"value2\"}' -- json_col +)", + ) + .unwrap(); + + let mut stmt = conn + .prepare_query(&crate::sql_query( + "SELECT + tiny_int, small_int, medium_int, int_col, + big_int, unsigned_int, zero_fill_int, + numeric_col, decimal_col, float_col, double_col, bit_col, + date_col, date_time, timestamp_col, time_col, year_col, + char_col, varchar_col, binary_col, varbinary_col, blob_col, + text_col, enum_col, set_col, ST_AsText(geom), ST_AsText(point_col), ST_AsText(linestring_col), + ST_AsText(polygon_col), ST_AsText(multipoint_col), ST_AsText(multilinestring_col), + ST_AsText(multipolygon_col), ST_AsText(geometry_collection), json_col + FROM all_mysql_types", + )) + .unwrap(); + + let results = unsafe { stmt.named_results().unwrap() }; + + let NamedStatementIterator { + stmt, + mut output_binds, + metadata, + } = results; + + crate::mysql::connection::stmt::iterator::populate_row_buffers(stmt, &mut output_binds) + .unwrap(); + + let results: Vec<(BindData, &ffi::st_mysql_field)> = output_binds + .data + .into_iter() + .zip(metadata.fields()) + .collect::>(); + + macro_rules! matches { + ($expression:expr, $( $pattern:pat )|+ $( if $guard: expr )?) => { + match $expression { + $( $pattern )|+ $( if $guard )? => true, + _ => false + } + } + } + + let tiny_int_col = &results[0].0; + assert_eq!(tiny_int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_TINY); + assert!(tiny_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(!tiny_int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(tiny_int_col), Ok(0))); + + let small_int_col = &results[1].0; + assert_eq!(small_int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_SHORT); + assert!(small_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(!small_int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(small_int_col), Ok(1))); + + let medium_int_col = &results[2].0; + assert_eq!(medium_int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_INT24); + assert!(medium_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(!medium_int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(medium_int_col), Ok(2))); + + let int_col = &results[3].0; + assert_eq!(int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG); + assert!(int_col.flags.contains(Flags::NUM_FLAG)); + assert!(!int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(int_col), Ok(3))); + + let big_int_col = &results[4].0; + assert_eq!(big_int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONGLONG); + assert!(big_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(!big_int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(big_int_col), Ok(-5))); + + let unsigned_int_col = &results[5].0; + assert_eq!(unsigned_int_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG); + assert!(unsigned_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(unsigned_int_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!( + to_value::, u32>(unsigned_int_col), + Ok(42) + )); + + let zero_fill_int_col = &results[6].0; + assert_eq!( + zero_fill_int_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_LONG + ); + assert!(zero_fill_int_col.flags.contains(Flags::NUM_FLAG)); + assert!(zero_fill_int_col.flags.contains(Flags::ZEROFILL_FLAG)); + assert!(matches!(to_value::(zero_fill_int_col), Ok(1))); + + let numeric_col = &results[7].0; + assert_eq!( + numeric_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_NEWDECIMAL + ); + assert!(numeric_col.flags.contains(Flags::NUM_FLAG)); + assert!(!numeric_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert_eq!( + to_value::(numeric_col).unwrap(), + bigdecimal::BigDecimal::from(-999.999) + ); + + let decimal_col = &results[8].0; + assert_eq!( + decimal_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_NEWDECIMAL + ); + assert!(decimal_col.flags.contains(Flags::NUM_FLAG)); + assert!(!decimal_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert_eq!( + to_value::(decimal_col).unwrap(), + bigdecimal::BigDecimal::from(3.14) + ); + + let float_col = &results[9].0; + assert_eq!(float_col.tpe, ffi::enum_field_types::MYSQL_TYPE_FLOAT); + assert!(float_col.flags.contains(Flags::NUM_FLAG)); + assert!(!float_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(float_col), Ok(1.23))); + + let double_col = &results[10].0; + assert_eq!(double_col.tpe, ffi::enum_field_types::MYSQL_TYPE_DOUBLE); + assert!(double_col.flags.contains(Flags::NUM_FLAG)); + assert!(!double_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(double_col), Ok(4.5678))); + + let bit_col = &results[11].0; + assert_eq!(bit_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BIT); + assert!(!bit_col.flags.contains(Flags::NUM_FLAG)); + assert!(bit_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(!bit_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::>(bit_col).unwrap(), vec![170]); + + let date_col = &results[12].0; + assert_eq!(date_col.tpe, ffi::enum_field_types::MYSQL_TYPE_DATE); + assert!(!date_col.flags.contains(Flags::TIMESTAMP_FLAG)); + assert!(!date_col.flags.contains(Flags::NUM_FLAG)); + assert_eq!( + to_value::(date_col).unwrap(), + chrono::NaiveDate::from_ymd_opt(1000, 1, 1).unwrap(), + ); + + let date_time_col = &results[13].0; + assert_eq!( + date_time_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_DATETIME + ); + assert!(!date_time_col.flags.contains(Flags::TIMESTAMP_FLAG)); + assert!(!date_time_col.flags.contains(Flags::NUM_FLAG)); + assert_eq!( + to_value::(date_time_col).unwrap(), + chrono::NaiveDateTime::parse_from_str("9999-12-31 12:34:45", "%Y-%m-%d %H:%M:%S") + .unwrap() + ); + + let timestamp_col = &results[14].0; + assert_eq!( + timestamp_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_TIMESTAMP + ); + assert!(timestamp_col.flags.contains(Flags::TIMESTAMP_FLAG)); + assert!(!timestamp_col.flags.contains(Flags::NUM_FLAG)); + assert_eq!( + to_value::(timestamp_col).unwrap(), + chrono::NaiveDateTime::parse_from_str("2020-01-01 10:10:10", "%Y-%m-%d %H:%M:%S") + .unwrap() + ); + + let time_col = &results[15].0; + assert_eq!(time_col.tpe, ffi::enum_field_types::MYSQL_TYPE_TIME); + assert!(!time_col.flags.contains(Flags::TIMESTAMP_FLAG)); + assert!(!time_col.flags.contains(Flags::NUM_FLAG)); + assert_eq!( + to_value::(time_col).unwrap(), + chrono::NaiveTime::from_hms(23, 01, 01) + ); + + let year_col = &results[16].0; + assert_eq!(year_col.tpe, ffi::enum_field_types::MYSQL_TYPE_YEAR); + assert!(!year_col.flags.contains(Flags::TIMESTAMP_FLAG)); + assert!(year_col.flags.contains(Flags::NUM_FLAG)); + assert!(year_col.flags.contains(Flags::UNSIGNED_FLAG)); + assert!(matches!(to_value::(year_col), Ok(2020))); + + let char_col = &results[17].0; + assert_eq!(char_col.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); + assert!(!char_col.flags.contains(Flags::NUM_FLAG)); + assert!(!char_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!char_col.flags.contains(Flags::SET_FLAG)); + assert!(!char_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!char_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(char_col).unwrap(), "abc"); + + let varchar_col = &results[18].0; + assert_eq!( + varchar_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_VAR_STRING + ); + assert!(!varchar_col.flags.contains(Flags::NUM_FLAG)); + assert!(!varchar_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!varchar_col.flags.contains(Flags::SET_FLAG)); + assert!(!varchar_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!varchar_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(varchar_col).unwrap(), "foo"); + + let binary_col = &results[19].0; + assert_eq!(binary_col.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); + assert!(!binary_col.flags.contains(Flags::NUM_FLAG)); + assert!(!binary_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!binary_col.flags.contains(Flags::SET_FLAG)); + assert!(!binary_col.flags.contains(Flags::ENUM_FLAG)); + assert!(binary_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::>(binary_col).unwrap(), + b"a \0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" + ); + + let varbinary_col = &results[20].0; + assert_eq!( + varbinary_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_VAR_STRING + ); + assert!(!varbinary_col.flags.contains(Flags::NUM_FLAG)); + assert!(!varbinary_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!varbinary_col.flags.contains(Flags::SET_FLAG)); + assert!(!varbinary_col.flags.contains(Flags::ENUM_FLAG)); + assert!(varbinary_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::>(varbinary_col).unwrap(), b"a "); + + let blob_col = &results[21].0; + assert_eq!(blob_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!blob_col.flags.contains(Flags::NUM_FLAG)); + assert!(blob_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!blob_col.flags.contains(Flags::SET_FLAG)); + assert!(!blob_col.flags.contains(Flags::ENUM_FLAG)); + assert!(blob_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::>(blob_col).unwrap(), b"binary"); + + let text_col = &results[22].0; + assert_eq!(text_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!text_col.flags.contains(Flags::NUM_FLAG)); + assert!(text_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!text_col.flags.contains(Flags::SET_FLAG)); + assert!(!text_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!text_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(text_col).unwrap(), + "some text whatever" + ); + + let enum_col = &results[23].0; + assert_eq!(enum_col.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); + assert!(!enum_col.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col.flags.contains(Flags::SET_FLAG)); + assert!(enum_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(enum_col).unwrap(), "red"); + + let set_col = &results[24].0; + assert_eq!(set_col.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); + assert!(!set_col.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col.flags.contains(Flags::SET_FLAG)); + assert!(!set_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(set_col).unwrap(), "one"); + + let geom = &results[25].0; + assert_eq!(geom.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB); + assert!(!geom.flags.contains(Flags::NUM_FLAG)); + assert!(!geom.flags.contains(Flags::BLOB_FLAG)); + assert!(!geom.flags.contains(Flags::SET_FLAG)); + assert!(!geom.flags.contains(Flags::ENUM_FLAG)); + assert!(!geom.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(geom).unwrap(), "POINT(1 1)"); + + let point_col = &results[26].0; + assert_eq!(point_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB); + assert!(!point_col.flags.contains(Flags::NUM_FLAG)); + assert!(!point_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!point_col.flags.contains(Flags::SET_FLAG)); + assert!(!point_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!point_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(point_col).unwrap(), "POINT(1 1)"); + + let linestring_col = &results[27].0; + assert_eq!( + linestring_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB + ); + assert!(!linestring_col.flags.contains(Flags::NUM_FLAG)); + assert!(!linestring_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!linestring_col.flags.contains(Flags::SET_FLAG)); + assert!(!linestring_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!linestring_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(linestring_col).unwrap(), + "LINESTRING(0 0,1 1,2 2)" + ); + + let polygon_col = &results[28].0; + assert_eq!(polygon_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB); + assert!(!polygon_col.flags.contains(Flags::NUM_FLAG)); + assert!(!polygon_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!polygon_col.flags.contains(Flags::SET_FLAG)); + assert!(!polygon_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!polygon_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(polygon_col).unwrap(), + "POLYGON((0 0,10 0,10 10,0 10,0 0),(5 5,7 5,7 7,5 7,5 5))" + ); + + let multipoint_col = &results[29].0; + assert_eq!( + multipoint_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB + ); + assert!(!multipoint_col.flags.contains(Flags::NUM_FLAG)); + assert!(!multipoint_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!multipoint_col.flags.contains(Flags::SET_FLAG)); + assert!(!multipoint_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!multipoint_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(multipoint_col).unwrap(), + "MULTIPOINT(0 0,10 10,10 20,20 20)" + ); + + let multilinestring_col = &results[30].0; + assert_eq!( + multilinestring_col.tpe, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB + ); + assert!(!multilinestring_col.flags.contains(Flags::NUM_FLAG)); + assert!(!multilinestring_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!multilinestring_col.flags.contains(Flags::SET_FLAG)); + assert!(!multilinestring_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!multilinestring_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(multilinestring_col).unwrap(), + "MULTILINESTRING((10 48,10 21,10 0),(16 0,16 23,16 48))" + ); + + let polygon_col = &results[31].0; + assert_eq!(polygon_col.tpe, ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB); + assert!(!polygon_col.flags.contains(Flags::NUM_FLAG)); + assert!(!polygon_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!polygon_col.flags.contains(Flags::SET_FLAG)); + assert!(!polygon_col.flags.contains(Flags::ENUM_FLAG)); + assert!(!polygon_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(polygon_col).unwrap(), + "MULTIPOLYGON(((28 26,28 0,84 0,84 42,28 26),(52 18,66 23,73 9,48 6,52 18)),((59 18,67 18,67 13,59 13,59 18)))" + ); + + let geometry_collection = &results[32].0; + assert_eq!( + geometry_collection.tpe, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB + ); + assert!(!geometry_collection.flags.contains(Flags::NUM_FLAG)); + assert!(!geometry_collection.flags.contains(Flags::BLOB_FLAG)); + assert!(!geometry_collection.flags.contains(Flags::SET_FLAG)); + assert!(!geometry_collection.flags.contains(Flags::ENUM_FLAG)); + assert!(!geometry_collection.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(geometry_collection).unwrap(), + "GEOMETRYCOLLECTION(POINT(1 1),LINESTRING(0 0,1 1,2 2,3 3,4 4))" + ); + + let json_col = &results[33].0; + assert_eq!(json_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!json_col.flags.contains(Flags::NUM_FLAG)); + assert!(json_col.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col.flags.contains(Flags::SET_FLAG)); + assert!(!json_col.flags.contains(Flags::ENUM_FLAG)); + assert!(json_col.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(json_col).unwrap(), + "{\"key1\": \"value1\", \"key2\": \"value2\"}" + ); + } + + fn query_single_table( + query: &'static str, + conn: &MysqlConnection, + bind_tpe: ffi::enum_field_types, + ) -> BindData { + let mut stmt: Statement = conn.raw_connection.prepare(query).unwrap(); + + let bind = BindData::for_output((bind_tpe, Flags::UNSIGNED_FLAG)); + + let mut binds = Binds { data: vec![bind] }; + + crate::mysql::connection::stmt::iterator::execute_statement(&mut stmt, &mut binds).unwrap(); + + crate::mysql::connection::stmt::iterator::populate_row_buffers(&stmt, &mut binds).unwrap(); + + binds.data.remove(0) + } + + fn input_bind( + query: &'static str, + conn: &MysqlConnection, + id: i32, + (field, tpe): (Vec, ffi::enum_field_types), + ) { + let mut stmt = conn.raw_connection.prepare(query).unwrap(); + let length = field.len() as _; + + let json_bind = BindData { + tpe, + bytes: field, + length, + flags: Flags::empty(), + is_null: 0, + is_truncated: None, + }; + + let bytes = id.to_be_bytes().to_vec(); + let length = bytes.len() as _; + + let id_bind = BindData { + tpe: ffi::enum_field_types::MYSQL_TYPE_LONG, + bytes, + length, + flags: Flags::empty(), + is_null: 0, + is_truncated: None, + }; + + let mut binds = Binds { + data: vec![id_bind, json_bind], + }; + + binds.with_mysql_binds(|bind_ptr| unsafe { + ffi::mysql_stmt_bind_param(stmt.stmt.as_ptr(), bind_ptr); + }); + + stmt.did_an_error_occur().unwrap(); + + let mut out_binds = Binds { data: vec![] }; + + unsafe { + stmt.execute().unwrap(); + } + } + + #[test] + fn check_json_bind() { + let conn: MysqlConnection = crate::test_helpers::connection(); + + table! { + json_test { + id -> Integer, + json_field -> Text, + } + } + + conn.execute("DROP TABLE IF EXISTS json_test CASCADE") + .unwrap(); + + conn.execute("CREATE TABLE json_test(id INTEGER PRIMARY KEY, json_field JSON NOT NULL)") + .unwrap(); + + conn.execute("INSERT INTO json_test(id, json_field) VALUES (1, '{\"key1\": \"value1\", \"key2\": \"value2\"}')").unwrap(); + + let json_col_as_json = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_JSON, + ); + + assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); + assert!(!json_col_as_json.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_json).unwrap(), + "{\"key1\": \"value1\", \"key2\": \"value2\"}" + ); + + let json_col_as_text = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!json_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_text).unwrap(), + "{\"key1\": \"value1\", \"key2\": \"value2\"}" + ); + assert_eq!(json_col_as_json.bytes, json_col_as_text.bytes); + + conn.execute("DELETE FROM json_test").unwrap(); + + input_bind( + "INSERT INTO json_test(id, json_field) VALUES (?, ?)", + &conn, + 41, + ( + b"{\"abc\": 42}".to_vec(), + ffi::enum_field_types::MYSQL_TYPE_JSON, + ), + ); + + let json_col_as_json = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_JSON, + ); + + assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); + assert!(!json_col_as_json.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_json).unwrap(), + "{\"abc\": 42}" + ); + + let json_col_as_text = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!json_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_text).unwrap(), + "{\"abc\": 42}" + ); + assert_eq!(json_col_as_json.bytes, json_col_as_text.bytes); + + conn.execute("DELETE FROM json_test").unwrap(); + + input_bind( + "INSERT INTO json_test(id, json_field) VALUES (?, ?)", + &conn, + 41, + ( + b"{\"abca\": 42}".to_vec(), + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ), + ); + + let json_col_as_json = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_JSON, + ); + + assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); + assert!(!json_col_as_json.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_json.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_json).unwrap(), + "{\"abca\": 42}" + ); + + let json_col_as_text = query_single_table( + "SELECT json_field FROM json_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!json_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!json_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&json_col_as_text).unwrap(), + "{\"abca\": 42}" + ); + assert_eq!(json_col_as_json.bytes, json_col_as_text.bytes); + } + + #[test] + fn check_enum_bind() { + let conn: MysqlConnection = crate::test_helpers::connection(); + + conn.execute("DROP TABLE IF EXISTS enum_test CASCADE") + .unwrap(); + + conn.execute("CREATE TABLE enum_test(id INTEGER PRIMARY KEY, enum_field ENUM('red', 'green', 'blue') NOT NULL)") + .unwrap(); + + conn.execute("INSERT INTO enum_test(id, enum_field) VALUES (1, 'green')") + .unwrap(); + + let enum_col_as_enum: BindData = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_ENUM, + ); + + assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_enum.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&enum_col_as_enum).unwrap(), + "green" + ); + + let enum_col_as_text = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&enum_col_as_text).unwrap(), + "green" + ); + assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); + + conn.execute("DELETE FROM enum_test").unwrap(); + + input_bind( + "INSERT INTO enum_test(id, enum_field) VALUES (?, ?)", + &conn, + 41, + (b"blue".to_vec(), ffi::enum_field_types::MYSQL_TYPE_ENUM), + ); + + let enum_col_as_enum = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_ENUM, + ); + + assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_enum.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&enum_col_as_enum).unwrap(), "blue"); + + let enum_col_as_text = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&enum_col_as_text).unwrap(), "blue"); + assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); + + conn.execute("DELETE FROM enum_test").unwrap(); + + input_bind( + "INSERT INTO enum_test(id, enum_field) VALUES (?, ?)", + &conn, + 41, + (b"red".to_vec(), ffi::enum_field_types::MYSQL_TYPE_BLOB), + ); + + let enum_col_as_enum = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_ENUM, + ); + + assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_enum.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_enum.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&enum_col_as_enum).unwrap(), "red"); + + let enum_col_as_text = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&enum_col_as_text).unwrap(), "red"); + assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); + } + + #[test] + fn check_set_bind() { + let conn: MysqlConnection = crate::test_helpers::connection(); + + conn.execute("DROP TABLE IF EXISTS set_test CASCADE") + .unwrap(); + + conn.execute("CREATE TABLE set_test(id INTEGER PRIMARY KEY, set_field SET('red', 'green', 'blue') NOT NULL)") + .unwrap(); + + conn.execute("INSERT INTO set_test(id, set_field) VALUES (1, 'green')") + .unwrap(); + + let set_col_as_set: BindData = query_single_table( + "SELECT set_field FROM set_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_SET, + ); + + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_set).unwrap(), "green"); + + let set_col_as_text = query_single_table( + "SELECT set_field FROM enum_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!set_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_text).unwrap(), "green"); + assert_eq!(set_col_as_set.bytes, set_col_as_text.bytes); + + conn.execute("DELETE FROM set_test").unwrap(); + + input_bind( + "INSERT INTO set_test(id, set_field) VALUES (?, ?)", + &conn, + 41, + (b"blue".to_vec(), ffi::enum_field_types::MYSQL_TYPE_SET), + ); + + let set_col_as_set = query_single_table( + "SELECT set_field FROM set_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_SET, + ); + + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_SET); + assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_set).unwrap(), "blue"); + + let set_col_as_text = query_single_table( + "SELECT set_field FROM set_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!set_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_text).unwrap(), "blue"); + assert_eq!(set_col_as_set.bytes, set_col_as_text.bytes); + + conn.execute("DELETE FROM set_test").unwrap(); + + input_bind( + "INSERT INTO set_test(id, set_field) VALUES (?, ?)", + &conn, + 41, + (b"red".to_vec(), ffi::enum_field_types::MYSQL_TYPE_BLOB), + ); + + let set_col_as_set = query_single_table( + "SELECT set_field FROM set_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_SET, + ); + + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_SET); + assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_set.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_set).unwrap(), "red"); + + let set_col_as_text = query_single_table( + "SELECT set_field FROM set_test", + &conn, + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ); + + assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!set_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_text).unwrap(), "red"); + assert_eq!(set_col_as_set.bytes, set_col_as_text.bytes); + } } diff --git a/diesel/src/mysql/connection/stmt/iterator.rs b/diesel/src/mysql/connection/stmt/iterator.rs index 4911fcf29ff7..91f367652258 100644 --- a/diesel/src/mysql/connection/stmt/iterator.rs +++ b/diesel/src/mysql/connection/stmt/iterator.rs @@ -62,9 +62,9 @@ impl<'a> Row for MysqlRow<'a> { } pub struct NamedStatementIterator<'a> { - stmt: &'a mut Statement, - output_binds: Binds, - metadata: StatementMetadata, + pub(crate) stmt: &'a mut Statement, + pub(crate) output_binds: Binds, + pub(crate) metadata: StatementMetadata, } #[allow(clippy::should_implement_trait)] // don't need `Iterator` here @@ -121,7 +121,7 @@ impl<'a> NamedRow for NamedMysqlRow<'a> { } } -fn execute_statement(stmt: &mut Statement, binds: &mut Binds) -> QueryResult<()> { +pub(in crate::mysql::connection) fn execute_statement(stmt: &mut Statement, binds: &mut Binds) -> QueryResult<()> { unsafe { binds.with_mysql_binds(|bind_ptr| stmt.bind_result(bind_ptr))?; stmt.execute()?; @@ -129,7 +129,7 @@ fn execute_statement(stmt: &mut Statement, binds: &mut Binds) -> QueryResult<()> Ok(()) } -fn populate_row_buffers(stmt: &Statement, binds: &mut Binds) -> QueryResult> { +pub(crate) fn populate_row_buffers(stmt: &Statement, binds: &mut Binds) -> QueryResult> { let next_row_result = unsafe { ffi::mysql_stmt_fetch(stmt.stmt.as_ptr()) }; match next_row_result as libc::c_uint { ffi::MYSQL_NO_DATA => Ok(None), diff --git a/diesel/src/mysql/connection/stmt/mod.rs b/diesel/src/mysql/connection/stmt/mod.rs index 6524a43c1a02..1a5b3306f527 100644 --- a/diesel/src/mysql/connection/stmt/mod.rs +++ b/diesel/src/mysql/connection/stmt/mod.rs @@ -1,6 +1,6 @@ extern crate mysqlclient_sys as ffi; -mod iterator; +pub(super) mod iterator; mod metadata; use std::ffi::CStr; @@ -14,7 +14,7 @@ use crate::mysql::MysqlTypeMetadata; use crate::result::{DatabaseErrorKind, QueryResult}; pub struct Statement { - stmt: NonNull, + pub(super) stmt: NonNull, input_binds: Option, } @@ -124,7 +124,7 @@ impl Statement { .ok_or_else(|| DeserializationError("No metadata exists".into())) } - fn did_an_error_occur(&self) -> QueryResult<()> { + pub(super) fn did_an_error_occur(&self) -> QueryResult<()> { use crate::result::Error::DatabaseError; let error_message = self.last_error_message(); diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index df63e4a38a79..809decede26a 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -50,7 +50,7 @@ impl<'a> MysqlValue<'a> { Ok(match self.tpe.data_type { MysqlType::Tiny => Tiny(self.raw[0] as i8), MysqlType::Short => Small(i16::from_ne_bytes(self.raw.try_into()?)), - MysqlType::Long => Medium(i32::from_ne_bytes(self.raw.try_into()?)), + MysqlType::Medium | MysqlType::Long => Medium(i32::from_ne_bytes(self.raw.try_into()?)), MysqlType::LongLong => Big(i64::from_ne_bytes(self.raw.try_into()?)), MysqlType::Float => Float(f32::from_ne_bytes(self.raw.try_into()?)), MysqlType::Double => Double(f64::from_ne_bytes(self.raw.try_into()?)), @@ -71,6 +71,7 @@ impl<'a> MysqlValue<'a> { /// Represents all possible forms MySQL transmits integers #[derive(Debug, Clone, Copy)] +#[non_exhaustive] pub enum NumericRepresentation<'a> { /// Correponds to `MYSQL_TYPE_TINY` Tiny(i8), From 2facb241832478feac605e52d8ba250257c736c8 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 15 May 2020 23:28:32 +0200 Subject: [PATCH 08/25] Try to cleanup mysql type metadata implementation This commit adds a fake type layer above the types exposed by the mysql wire protocol as some of them are unused, can be used in the same context and some additional information are required to be read from the provided flags integer. I do not want to expose such a mess to potential users, better expose an clear type enum with all currently supported variants in diesel. If we hit later something that isn't supported yet we can just add it with an additional entry, because this enum is `#[non_exhaustive]` --- diesel/src/mysql/backend.rs | 42 +-- diesel/src/mysql/connection/bind.rs | 374 ++++++++++++------- diesel/src/mysql/connection/stmt/iterator.rs | 4 +- diesel/src/mysql/connection/stmt/mod.rs | 6 +- diesel/src/mysql/mod.rs | 2 +- diesel/src/mysql/types/mod.rs | 32 +- diesel/src/mysql/value.rs | 24 +- diesel_derives/src/sql_type.rs | 7 +- 8 files changed, 304 insertions(+), 187 deletions(-) diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index 6f6964a69cd2..3212f9c7207b 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -12,42 +12,28 @@ use crate::sql_types::TypeMetadata; #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] pub struct Mysql; -/// The full type metadata for MySQL -/// -/// This includes the type of the value, and whether it is signed. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub struct MysqlTypeMetadata { - /// The underlying data type - /// - /// Affects the `buffer_type` sent to libmysqlclient - pub data_type: MysqlType, - - /// Is this type signed? - /// - /// Affects the `is_unsigned` flag sent to libmysqlclient - pub is_unsigned: bool, -} - #[allow(missing_debug_implementations)] -/// Represents the possible forms a bind parameter can be transmitted as. -/// Each variant represents one of the forms documented at -/// -/// -/// The null variant is omitted, as we will never prepare a statement in which -/// one of the bind parameters can always be NULL +/// Represents the possible types, that can be transmitted as via the +/// Mysql wire protocol #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum MysqlType { - /// Sets `buffer_type` to `MYSQL_TYPE_TINY` + /// Tiny, + /// Sets + UnsignedTiny, /// Sets `buffer_type` to `MYSQL_TYPE_SHORT` Short, - /// Sets `buffer_type` to `MYSQL_TYPE_INT24` - Medium, + /// + UnsignedShort, /// Sets `buffer_type` to `MYSQL_TYPE_LONG` Long, + /// + UnsignedLong, /// Sets `buffer_type` to `MYSQL_TYPE_LONGLONG` LongLong, + /// + UnsignedLongLong, /// Sets `buffer_type` to `MYSQL_TYPE_FLOAT` Float, /// Sets `buffer_type` to `MYSQL_TYPE_DOUBLE` @@ -68,6 +54,10 @@ pub enum MysqlType { Blob, /// Sets `buffer_type` to `MYSQL_TYPE_BIT` Bit, + /// + Set, + /// + Enum, } impl Backend for Mysql { @@ -81,7 +71,7 @@ impl<'a> HasRawValue<'a> for Mysql { } impl TypeMetadata for Mysql { - type TypeMetadata = MysqlTypeMetadata; + type TypeMetadata = MysqlType; type MetadataLookup = (); } diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 2a2b9704779e..70cb52464eee 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -4,7 +4,7 @@ use std::mem; use std::os::raw as libc; use super::stmt::Statement; -use crate::mysql::{MysqlType, MysqlTypeMetadata, MysqlValue}; +use crate::mysql::{MysqlType, MysqlValue}; use crate::result::QueryResult; #[derive(Debug)] @@ -15,31 +15,20 @@ pub struct Binds { impl Binds { pub fn from_input_data(input: Iter) -> Self where - Iter: IntoIterator>)>, + Iter: IntoIterator>)>, { let data = input .into_iter() - .map(|(metadata, bytes)| { - BindData::for_input(metadata.data_type, metadata.is_unsigned as _, bytes) - }) + .map(|(metadata, bytes)| BindData::for_input(metadata, bytes)) .collect(); Binds { data } } - pub fn from_output_types(types: Vec) -> Self { + pub fn from_output_types(types: Vec) -> Self { let data = types .into_iter() - .map(|metadata| { - ( - metadata.data_type.into(), - if metadata.is_unsigned { - Flags::UNSIGNED_FLAG - } else { - Flags::empty() - }, - ) - }) + .map(|metadata| metadata.into()) .map(BindData::for_output) .collect(); @@ -99,14 +88,8 @@ impl Binds { pub fn field_data(&self, idx: usize) -> Option> { let data = &self.data[idx]; self.data[idx].bytes().map(|bytes| { - let tpe = data.tpe.into(); - MysqlValue::new( - bytes, - MysqlTypeMetadata { - data_type: tpe, - is_unsigned: data.flags.contains(Flags::UNSIGNED_FLAG), - }, - ) + let tpe = (data.tpe, data.flags).into(); + MysqlValue::new(bytes, tpe) }) } } @@ -148,22 +131,18 @@ struct BindData { } impl BindData { - fn for_input(tpe: MysqlType, is_unsigned: bool, data: Option>) -> Self { + fn for_input(tpe: MysqlType, data: Option>) -> Self { let is_null = if data.is_none() { 1 } else { 0 }; let bytes = data.unwrap_or_default(); let length = bytes.len() as libc::c_ulong; - + let (tpe, flags) = tpe.into(); BindData { - tpe: tpe.into(), + tpe, bytes, length, is_null, is_truncated: None, - flags: if is_unsigned { - Flags::UNSIGNED_FLAG - } else { - Flags::empty() - }, + flags, } } @@ -264,14 +243,13 @@ impl BindData { } } -impl From for ffi::enum_field_types { +impl From for (ffi::enum_field_types, Flags) { fn from(tpe: MysqlType) -> Self { use self::ffi::enum_field_types::*; - - match tpe { + let mut flags = Flags::empty(); + let tpe = match tpe { MysqlType::Tiny => MYSQL_TYPE_TINY, MysqlType::Short => MYSQL_TYPE_SHORT, - MysqlType::Medium => MYSQL_TYPE_INT24, MysqlType::Long => MYSQL_TYPE_LONG, MysqlType::LongLong => MYSQL_TYPE_LONGLONG, MysqlType::Float => MYSQL_TYPE_FLOAT, @@ -284,55 +262,137 @@ impl From for ffi::enum_field_types { MysqlType::Blob => MYSQL_TYPE_BLOB, MysqlType::Numeric => MYSQL_TYPE_NEWDECIMAL, MysqlType::Bit => MYSQL_TYPE_BIT, - } + MysqlType::UnsignedTiny => { + flags = Flags::UNSIGNED_FLAG; + MYSQL_TYPE_TINY + } + MysqlType::UnsignedShort => { + flags = Flags::UNSIGNED_FLAG; + MYSQL_TYPE_SHORT + } + MysqlType::UnsignedLong => { + flags = Flags::UNSIGNED_FLAG; + MYSQL_TYPE_LONG + } + MysqlType::UnsignedLongLong => { + flags = Flags::UNSIGNED_FLAG; + MYSQL_TYPE_LONGLONG + } + MysqlType::Set => { + flags = Flags::SET_FLAG; + MYSQL_TYPE_STRING + } + MysqlType::Enum => { + flags = Flags::ENUM_FLAG; + MYSQL_TYPE_STRING + } + }; + (tpe, flags) } } -impl From for MysqlType { - fn from(tpe: ffi::enum_field_types) -> Self { +impl From<(ffi::enum_field_types, Flags)> for MysqlType { + fn from((tpe, flags): (ffi::enum_field_types, Flags)) -> Self { use self::ffi::enum_field_types::*; + let is_unsigned = flags.contains(Flags::UNSIGNED_FLAG); + // https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/c-api-data-structures.html // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/binary__log__types_8h.html // https://dev.mysql.com/doc/internals/en/binary-protocol-value.html // https://mariadb.com/kb/en/packet_bindata/ match tpe { + MYSQL_TYPE_TINY if is_unsigned => MysqlType::UnsignedTiny, + MYSQL_TYPE_YEAR | MYSQL_TYPE_SHORT if is_unsigned => MysqlType::UnsignedShort, + MYSQL_TYPE_INT24 | MYSQL_TYPE_LONG if is_unsigned => MysqlType::UnsignedLong, + MYSQL_TYPE_LONGLONG if is_unsigned => MysqlType::UnsignedLongLong, MYSQL_TYPE_TINY => MysqlType::Tiny, - MYSQL_TYPE_YEAR | MYSQL_TYPE_SHORT => MysqlType::Short, - MYSQL_TYPE_INT24 => MysqlType::Medium, - MYSQL_TYPE_LONG => MysqlType::Long, + MYSQL_TYPE_SHORT => MysqlType::Short, + MYSQL_TYPE_INT24 | MYSQL_TYPE_LONG => MysqlType::Long, MYSQL_TYPE_LONGLONG => MysqlType::LongLong, MYSQL_TYPE_FLOAT => MysqlType::Float, MYSQL_TYPE_DOUBLE => MysqlType::Double, + MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, + MYSQL_TYPE_BIT => MysqlType::Bit, + MYSQL_TYPE_TIME => MysqlType::Time, MYSQL_TYPE_DATE => MysqlType::Date, MYSQL_TYPE_DATETIME => MysqlType::DateTime, MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, - MYSQL_TYPE_BIT => MysqlType::Bit, - MYSQL_TYPE_ENUM - | MYSQL_TYPE_SET - | MYSQL_TYPE_JSON + + // The documentation states that + // MYSQL_TYPE_STRING is used for enums and sets + // but experimentation has shown that + // just any string like type works, so + // better be safe here + MYSQL_TYPE_BLOB + | MYSQL_TYPE_TINY_BLOB + | MYSQL_TYPE_MEDIUM_BLOB + | MYSQL_TYPE_LONG_BLOB | MYSQL_TYPE_VAR_STRING - | MYSQL_TYPE_VARCHAR - | MYSQL_TYPE_STRING => MysqlType::String, + | MYSQL_TYPE_STRING + if flags.contains(Flags::ENUM_FLAG) => + { + MysqlType::Enum + } MYSQL_TYPE_BLOB | MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB - | MYSQL_TYPE_LONG_BLOB => MysqlType::Blob, - MYSQL_TYPE_DECIMAL | MYSQL_TYPE_NEWDECIMAL => MysqlType::Numeric, + | MYSQL_TYPE_LONG_BLOB + | MYSQL_TYPE_VAR_STRING + | MYSQL_TYPE_STRING + if flags.contains(Flags::SET_FLAG) => + { + MysqlType::Set + } + + // "blobs" may contain binary data + // also "strings" can contain binary data + // but all only if the binary flag is set + // (see the check_all_the_types test case) + MYSQL_TYPE_BLOB + | MYSQL_TYPE_TINY_BLOB + | MYSQL_TYPE_MEDIUM_BLOB + | MYSQL_TYPE_LONG_BLOB + | MYSQL_TYPE_VAR_STRING + | MYSQL_TYPE_STRING + if flags.contains(Flags::BINARY_FLAG) => + { + MysqlType::Blob + } + + // If the binary flag is not set consider everything as string + MYSQL_TYPE_BLOB + | MYSQL_TYPE_TINY_BLOB + | MYSQL_TYPE_MEDIUM_BLOB + | MYSQL_TYPE_LONG_BLOB + | MYSQL_TYPE_JSON + | MYSQL_TYPE_VAR_STRING + | MYSQL_TYPE_STRING => MysqlType::String, + + // unsigned seems to be set for year in any case + MYSQL_TYPE_YEAR => unreachable!( + "The year type should have set the unsigned flag. If you ever \ + see this error message, something has gone very wrong. Please \ + open an issue at the diesel githup repo in this case" + ), // Null value MYSQL_TYPE_NULL => unreachable!( "We ensure at the call side that we do not hit this type here. \ If you ever see this error, something has gone very wrong. \ - Please open a issue at the diesel github repo in this case" - ), - // spatial type - // same encoding as string - MYSQL_TYPE_GEOMETRY => unimplemented!( - "Hit currently unsupported type, those variants should \ - probably be variants of MysqlType or at least be mapped \ - to one of the existing types" + Please open an issue at the diesel github repo in this case" ), + // Those exist in libmysqlclient + // but are just not supported + // + MYSQL_TYPE_VARCHAR | MYSQL_TYPE_ENUM | MYSQL_TYPE_SET | MYSQL_TYPE_GEOMETRY => { + unimplemented!( + "Hit a type that should be unsupported in libmysqlclient. If \ + you ever see this error, they probably have added support for \ + one of those types. Please open an issue at the diesel github \ + repo in this case." + ) + } MYSQL_TYPE_NEWDATE | MYSQL_TYPE_TIME2 @@ -370,7 +430,7 @@ mod tests { use super::*; use crate::prelude::*; - use super::{MysqlTypeMetadata, MysqlValue}; + use super::MysqlValue; use crate::deserialize::FromSql; use crate::mysql::connection::stmt::iterator::NamedStatementIterator; use crate::sql_types::*; @@ -381,11 +441,8 @@ mod tests { where T: FromSql + std::fmt::Debug, { - let meta = MysqlTypeMetadata { - data_type: bind.tpe.into(), - is_unsigned: bind.flags.contains(Flags::UNSIGNED_FLAG), - }; - + let meta = (bind.tpe, bind.flags).into(); + dbg!(meta); let value = MysqlValue::new(&bind.bytes, meta); dbg!(T::from_sql(Some(value))) } @@ -861,11 +918,11 @@ mod tests { fn query_single_table( query: &'static str, conn: &MysqlConnection, - bind_tpe: ffi::enum_field_types, + bind_tpe: impl Into<(ffi::enum_field_types, Flags)>, ) -> BindData { let mut stmt: Statement = conn.raw_connection.prepare(query).unwrap(); - let bind = BindData::for_output((bind_tpe, Flags::UNSIGNED_FLAG)); + let bind = BindData::for_output(bind_tpe.into()); let mut binds = Binds { data: vec![bind] }; @@ -880,16 +937,17 @@ mod tests { query: &'static str, conn: &MysqlConnection, id: i32, - (field, tpe): (Vec, ffi::enum_field_types), + (field, tpe): (Vec, impl Into<(ffi::enum_field_types, Flags)>), ) { let mut stmt = conn.raw_connection.prepare(query).unwrap(); let length = field.len() as _; + let (tpe, flags) = tpe.into(); - let json_bind = BindData { + let field_bind = BindData { tpe, bytes: field, length, - flags: Flags::empty(), + flags, is_null: 0, is_truncated: None, }; @@ -907,7 +965,7 @@ mod tests { }; let mut binds = Binds { - data: vec![id_bind, json_bind], + data: vec![id_bind, field_bind], }; binds.with_mysql_binds(|bind_ptr| unsafe { @@ -945,7 +1003,7 @@ mod tests { let json_col_as_json = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_JSON, + (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), ); assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); @@ -962,7 +1020,7 @@ mod tests { let json_col_as_text = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::empty()), ); assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); @@ -985,14 +1043,14 @@ mod tests { 41, ( b"{\"abc\": 42}".to_vec(), - ffi::enum_field_types::MYSQL_TYPE_JSON, + (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), ), ); let json_col_as_json = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_JSON, + (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), ); assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); @@ -1009,7 +1067,7 @@ mod tests { let json_col_as_text = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::empty()), ); assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); @@ -1030,16 +1088,13 @@ mod tests { "INSERT INTO json_test(id, json_field) VALUES (?, ?)", &conn, 41, - ( - b"{\"abca\": 42}".to_vec(), - ffi::enum_field_types::MYSQL_TYPE_BLOB, - ), + (b"{\"abca\": 42}".to_vec(), MysqlType::String), ); let json_col_as_json = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_JSON, + (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), ); assert_eq!(json_col_as_json.tpe, ffi::enum_field_types::MYSQL_TYPE_JSON); @@ -1056,7 +1111,7 @@ mod tests { let json_col_as_text = query_single_table( "SELECT json_field FROM json_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::empty()), ); assert_eq!(json_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); @@ -1085,13 +1140,13 @@ mod tests { conn.execute("INSERT INTO enum_test(id, enum_field) VALUES (1, 'green')") .unwrap(); - let enum_col_as_enum: BindData = query_single_table( - "SELECT enum_field FROM enum_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_ENUM, - ); + let enum_col_as_enum: BindData = + query_single_table("SELECT enum_field FROM enum_test", &conn, MysqlType::Enum); - assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert_eq!( + enum_col_as_enum.tpe, + ffi::enum_field_types::MYSQL_TYPE_STRING + ); assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); @@ -1102,17 +1157,43 @@ mod tests { "green" ); + for tpe in &[ + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ffi::enum_field_types::MYSQL_TYPE_VAR_STRING, + ffi::enum_field_types::MYSQL_TYPE_TINY_BLOB, + ffi::enum_field_types::MYSQL_TYPE_MEDIUM_BLOB, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB, + ] { + let enum_col_as_text = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + (*tpe, Flags::ENUM_FLAG), + ); + + assert_eq!(enum_col_as_text.tpe, *tpe); + assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!( + to_value::(&enum_col_as_text).unwrap(), + "green" + ); + assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); + } + let enum_col_as_text = query_single_table( "SELECT enum_field FROM enum_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::empty()), ); assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); - assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); assert_eq!( to_value::(&enum_col_as_text).unwrap(), @@ -1126,16 +1207,16 @@ mod tests { "INSERT INTO enum_test(id, enum_field) VALUES (?, ?)", &conn, 41, - (b"blue".to_vec(), ffi::enum_field_types::MYSQL_TYPE_ENUM), + (b"blue".to_vec(), MysqlType::Enum), ); - let enum_col_as_enum = query_single_table( - "SELECT enum_field FROM enum_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_ENUM, - ); + let enum_col_as_enum = + query_single_table("SELECT enum_field FROM enum_test", &conn, MysqlType::Enum); - assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert_eq!( + enum_col_as_enum.tpe, + ffi::enum_field_types::MYSQL_TYPE_STRING + ); assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); @@ -1146,7 +1227,22 @@ mod tests { let enum_col_as_text = query_single_table( "SELECT enum_field FROM enum_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::ENUM_FLAG), + ); + + assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&enum_col_as_text).unwrap(), "blue"); + assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); + + let enum_col_as_text = query_single_table( + "SELECT enum_field FROM enum_test", + &conn, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::ENUM_FLAG), ); assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); @@ -1164,16 +1260,19 @@ mod tests { "INSERT INTO enum_test(id, enum_field) VALUES (?, ?)", &conn, 41, - (b"red".to_vec(), ffi::enum_field_types::MYSQL_TYPE_BLOB), + ( + b"red".to_vec(), + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::ENUM_FLAG), + ), ); - let enum_col_as_enum = query_single_table( - "SELECT enum_field FROM enum_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_ENUM, - ); + let enum_col_as_enum = + query_single_table("SELECT enum_field FROM enum_test", &conn, MysqlType::Enum); - assert_eq!(enum_col_as_enum.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert_eq!( + enum_col_as_enum.tpe, + ffi::enum_field_types::MYSQL_TYPE_STRING + ); assert!(!enum_col_as_enum.flags.contains(Flags::NUM_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::BLOB_FLAG)); assert!(!enum_col_as_enum.flags.contains(Flags::SET_FLAG)); @@ -1184,14 +1283,14 @@ mod tests { let enum_col_as_text = query_single_table( "SELECT enum_field FROM enum_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::ENUM_FLAG), ); assert_eq!(enum_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); assert!(!enum_col_as_text.flags.contains(Flags::NUM_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::BLOB_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::SET_FLAG)); - assert!(!enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(enum_col_as_text.flags.contains(Flags::ENUM_FLAG)); assert!(!enum_col_as_text.flags.contains(Flags::BINARY_FLAG)); assert_eq!(to_value::(&enum_col_as_text).unwrap(), "red"); assert_eq!(enum_col_as_enum.bytes, enum_col_as_text.bytes); @@ -1210,13 +1309,10 @@ mod tests { conn.execute("INSERT INTO set_test(id, set_field) VALUES (1, 'green')") .unwrap(); - let set_col_as_set: BindData = query_single_table( - "SELECT set_field FROM set_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_SET, - ); + let set_col_as_set: BindData = + query_single_table("SELECT set_field FROM set_test", &conn, MysqlType::Set); - assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_ENUM); + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); @@ -1224,16 +1320,38 @@ mod tests { assert!(!set_col_as_set.flags.contains(Flags::BINARY_FLAG)); assert_eq!(to_value::(&set_col_as_set).unwrap(), "green"); + for tpe in &[ + ffi::enum_field_types::MYSQL_TYPE_BLOB, + ffi::enum_field_types::MYSQL_TYPE_VAR_STRING, + ffi::enum_field_types::MYSQL_TYPE_TINY_BLOB, + ffi::enum_field_types::MYSQL_TYPE_MEDIUM_BLOB, + ffi::enum_field_types::MYSQL_TYPE_LONG_BLOB, + ] { + let set_col_as_text = query_single_table( + "SELECT set_field FROM set_test", + &conn, + (*tpe, Flags::SET_FLAG), + ); + + assert_eq!(set_col_as_text.tpe, *tpe); + assert!(!set_col_as_text.flags.contains(Flags::NUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BLOB_FLAG)); + assert!(set_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::ENUM_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::BINARY_FLAG)); + assert_eq!(to_value::(&set_col_as_text).unwrap(), "green"); + assert_eq!(set_col_as_set.bytes, set_col_as_text.bytes); + } let set_col_as_text = query_single_table( - "SELECT set_field FROM enum_test", + "SELECT set_field FROM set_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::empty()), ); assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); assert!(!set_col_as_text.flags.contains(Flags::NUM_FLAG)); assert!(!set_col_as_text.flags.contains(Flags::BLOB_FLAG)); - assert!(set_col_as_text.flags.contains(Flags::SET_FLAG)); + assert!(!set_col_as_text.flags.contains(Flags::SET_FLAG)); assert!(!set_col_as_text.flags.contains(Flags::ENUM_FLAG)); assert!(!set_col_as_text.flags.contains(Flags::BINARY_FLAG)); assert_eq!(to_value::(&set_col_as_text).unwrap(), "green"); @@ -1245,16 +1363,13 @@ mod tests { "INSERT INTO set_test(id, set_field) VALUES (?, ?)", &conn, 41, - (b"blue".to_vec(), ffi::enum_field_types::MYSQL_TYPE_SET), + (b"blue".to_vec(), MysqlType::Set), ); - let set_col_as_set = query_single_table( - "SELECT set_field FROM set_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_SET, - ); + let set_col_as_set = + query_single_table("SELECT set_field FROM set_test", &conn, MysqlType::Set); - assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_SET); + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); @@ -1265,7 +1380,7 @@ mod tests { let set_col_as_text = query_single_table( "SELECT set_field FROM set_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::SET_FLAG), ); assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); @@ -1283,16 +1398,13 @@ mod tests { "INSERT INTO set_test(id, set_field) VALUES (?, ?)", &conn, 41, - (b"red".to_vec(), ffi::enum_field_types::MYSQL_TYPE_BLOB), + (b"red".to_vec(), MysqlType::String), ); - let set_col_as_set = query_single_table( - "SELECT set_field FROM set_test", - &conn, - ffi::enum_field_types::MYSQL_TYPE_SET, - ); + let set_col_as_set = + query_single_table("SELECT set_field FROM set_test", &conn, MysqlType::Set); - assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_SET); + assert_eq!(set_col_as_set.tpe, ffi::enum_field_types::MYSQL_TYPE_STRING); assert!(!set_col_as_set.flags.contains(Flags::NUM_FLAG)); assert!(!set_col_as_set.flags.contains(Flags::BLOB_FLAG)); assert!(set_col_as_set.flags.contains(Flags::SET_FLAG)); @@ -1303,7 +1415,7 @@ mod tests { let set_col_as_text = query_single_table( "SELECT set_field FROM set_test", &conn, - ffi::enum_field_types::MYSQL_TYPE_BLOB, + (ffi::enum_field_types::MYSQL_TYPE_BLOB, Flags::SET_FLAG), ); assert_eq!(set_col_as_text.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); diff --git a/diesel/src/mysql/connection/stmt/iterator.rs b/diesel/src/mysql/connection/stmt/iterator.rs index 91f367652258..ca018e3686c4 100644 --- a/diesel/src/mysql/connection/stmt/iterator.rs +++ b/diesel/src/mysql/connection/stmt/iterator.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use super::{ffi, libc, Binds, Statement, StatementMetadata}; -use crate::mysql::{Mysql, MysqlTypeMetadata, MysqlValue}; +use crate::mysql::{Mysql, MysqlType, MysqlValue}; use crate::result::QueryResult; use crate::row::*; @@ -13,7 +13,7 @@ pub struct StatementIterator<'a> { #[allow(clippy::should_implement_trait)] // don't neet `Iterator` here impl<'a> StatementIterator<'a> { #[allow(clippy::new_ret_no_self)] - pub fn new(stmt: &'a mut Statement, types: Vec) -> QueryResult { + pub fn new(stmt: &'a mut Statement, types: Vec) -> QueryResult { let mut output_binds = Binds::from_output_types(types); execute_statement(stmt, &mut output_binds)?; diff --git a/diesel/src/mysql/connection/stmt/mod.rs b/diesel/src/mysql/connection/stmt/mod.rs index 1a5b3306f527..edb0952a4883 100644 --- a/diesel/src/mysql/connection/stmt/mod.rs +++ b/diesel/src/mysql/connection/stmt/mod.rs @@ -10,7 +10,7 @@ use std::ptr::NonNull; use self::iterator::*; use self::metadata::*; use super::bind::Binds; -use crate::mysql::MysqlTypeMetadata; +use crate::mysql::MysqlType; use crate::result::{DatabaseErrorKind, QueryResult}; pub struct Statement { @@ -39,7 +39,7 @@ impl Statement { pub fn bind(&mut self, binds: Iter) -> QueryResult<()> where - Iter: IntoIterator>)>, + Iter: IntoIterator>)>, { let mut input_binds = Binds::from_input_data(binds); input_binds.with_mysql_binds(|bind_ptr| { @@ -74,7 +74,7 @@ impl Statement { /// be called on this statement. pub unsafe fn results( &mut self, - types: Vec, + types: Vec, ) -> QueryResult { StatementIterator::new(self, types) } diff --git a/diesel/src/mysql/mod.rs b/diesel/src/mysql/mod.rs index a8d46fd0ec2a..8f31612544fb 100644 --- a/diesel/src/mysql/mod.rs +++ b/diesel/src/mysql/mod.rs @@ -11,7 +11,7 @@ mod value; mod query_builder; pub mod types; -pub use self::backend::{Mysql, MysqlType, MysqlTypeMetadata}; +pub use self::backend::{Mysql, MysqlType}; pub use self::connection::MysqlConnection; pub use self::query_builder::MysqlQueryBuilder; pub use self::value::{MysqlValue, NumericRepresentation}; diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index 6208e625080d..a3606074ecb7 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -9,7 +9,7 @@ use byteorder::WriteBytesExt; use std::io::Write; use crate::deserialize::{self, FromSql}; -use crate::mysql::{Mysql, MysqlTypeMetadata, MysqlValue}; +use crate::mysql::{Mysql, MysqlType, MysqlValue}; use crate::query_builder::QueryId; use crate::serialize::{self, IsNull, Output, ToSql}; use crate::sql_types::ops::*; @@ -130,15 +130,27 @@ impl FromSql for bool { } } -impl HasSqlType> for Mysql -where - Mysql: HasSqlType, -{ - fn metadata(lookup: &()) -> MysqlTypeMetadata { - MysqlTypeMetadata { - is_unsigned: true, - ..>::metadata(lookup) - } +impl HasSqlType> for Mysql { + fn metadata(_lookup: &()) -> MysqlType { + MysqlType::UnsignedTiny + } +} + +impl HasSqlType> for Mysql { + fn metadata(_lookup: &()) -> MysqlType { + MysqlType::UnsignedShort + } +} + +impl HasSqlType> for Mysql { + fn metadata(_lookup: &()) -> MysqlType { + MysqlType::UnsignedLong + } +} + +impl HasSqlType> for Mysql { + fn metadata(_lookup: &()) -> MysqlType { + MysqlType::UnsignedLongLong } } diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index 809decede26a..a9e6e5ce1bb4 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -1,4 +1,4 @@ -use super::{MysqlType, MysqlTypeMetadata}; +use super::MysqlType; use crate::deserialize; use mysqlclient_sys as ffi; use std::error::Error; @@ -7,11 +7,11 @@ use std::error::Error; #[derive(Copy, Clone, Debug)] pub struct MysqlValue<'a> { raw: &'a [u8], - tpe: MysqlTypeMetadata, + tpe: MysqlType, } impl<'a> MysqlValue<'a> { - pub(crate) fn new(raw: &'a [u8], tpe: MysqlTypeMetadata) -> Self { + pub(crate) fn new(raw: &'a [u8], tpe: MysqlType) -> Self { Self { raw, tpe } } @@ -27,7 +27,7 @@ impl<'a> MysqlValue<'a> { // https://github.com/rust-lang/rust-clippy/issues/2881 #[allow(dead_code, clippy::cast_ptr_alignment)] pub(crate) fn time_value(&self) -> deserialize::Result { - match self.tpe.data_type { + match self.tpe { MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { let ptr = self.raw.as_ptr() as *const ffi::MYSQL_TIME; let result = unsafe { ptr.read_unaligned() }; @@ -47,11 +47,17 @@ impl<'a> MysqlValue<'a> { use self::NumericRepresentation::*; use std::convert::TryInto; - Ok(match self.tpe.data_type { - MysqlType::Tiny => Tiny(self.raw[0] as i8), - MysqlType::Short => Small(i16::from_ne_bytes(self.raw.try_into()?)), - MysqlType::Medium | MysqlType::Long => Medium(i32::from_ne_bytes(self.raw.try_into()?)), - MysqlType::LongLong => Big(i64::from_ne_bytes(self.raw.try_into()?)), + Ok(match self.tpe { + MysqlType::UnsignedTiny | MysqlType::Tiny => Tiny(self.raw[0] as i8), + MysqlType::UnsignedShort | MysqlType::Short => { + Small(i16::from_ne_bytes(self.raw.try_into()?)) + } + MysqlType::UnsignedLong | MysqlType::Long => { + Medium(i32::from_ne_bytes(self.raw.try_into()?)) + } + MysqlType::UnsignedLongLong | MysqlType::LongLong => { + Big(i64::from_ne_bytes(self.raw.try_into()?)) + } MysqlType::Float => Float(f32::from_ne_bytes(self.raw.try_into()?)), MysqlType::Double => Double(f64::from_ne_bytes(self.raw.try_into()?)), diff --git a/diesel_derives/src/sql_type.rs b/diesel_derives/src/sql_type.rs index ef3e11137d6d..5a488d4e1a18 100644 --- a/diesel_derives/src/sql_type.rs +++ b/diesel_derives/src/sql_type.rs @@ -71,11 +71,8 @@ fn mysql_tokens(item: &syn::DeriveInput) -> Option { for diesel::mysql::Mysql #where_clause { - fn metadata(_: &()) -> diesel::mysql::MysqlTypeMetadata { - diesel::mysql::MysqlTypeMetadata { - data_type: diesel::mysql::MysqlType::#ty, - is_unsigned: false, - } + fn metadata(_: &()) -> diesel::mysql::MysqlType { + diesel::mysql::MysqlType::#ty } } }) From d1eed7c05c8df04605dbaefbc0ca82f0f9fec75b Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Sat, 16 May 2020 10:56:55 +0200 Subject: [PATCH 09/25] More type fixes --- diesel/src/mysql/backend.rs | 2 ++ diesel/src/mysql/connection/bind.rs | 30 ++++++++++++++++++----------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index 3212f9c7207b..62c07b1ee85a 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -58,6 +58,8 @@ pub enum MysqlType { Set, /// Enum, + /// + Json, } impl Backend for Mysql { diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 70cb52464eee..d91691d4939b 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -262,6 +262,7 @@ impl From for (ffi::enum_field_types, Flags) { MysqlType::Blob => MYSQL_TYPE_BLOB, MysqlType::Numeric => MYSQL_TYPE_NEWDECIMAL, MysqlType::Bit => MYSQL_TYPE_BIT, + MysqlType::Json => MYSQL_TYPE_JSON, MysqlType::UnsignedTiny => { flags = Flags::UNSIGNED_FLAG; MYSQL_TYPE_TINY @@ -319,6 +320,7 @@ impl From<(ffi::enum_field_types, Flags)> for MysqlType { MYSQL_TYPE_DATE => MysqlType::Date, MYSQL_TYPE_DATETIME => MysqlType::DateTime, MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, + MYSQL_TYPE_JSON => MysqlType::Json, // The documentation states that // MYSQL_TYPE_STRING is used for enums and sets @@ -366,7 +368,6 @@ impl From<(ffi::enum_field_types, Flags)> for MysqlType { | MYSQL_TYPE_TINY_BLOB | MYSQL_TYPE_MEDIUM_BLOB | MYSQL_TYPE_LONG_BLOB - | MYSQL_TYPE_JSON | MYSQL_TYPE_VAR_STRING | MYSQL_TYPE_STRING => MysqlType::String, @@ -667,7 +668,6 @@ mod tests { let date_col = &results[12].0; assert_eq!(date_col.tpe, ffi::enum_field_types::MYSQL_TYPE_DATE); - assert!(!date_col.flags.contains(Flags::TIMESTAMP_FLAG)); assert!(!date_col.flags.contains(Flags::NUM_FLAG)); assert_eq!( to_value::(date_col).unwrap(), @@ -679,7 +679,6 @@ mod tests { date_time_col.tpe, ffi::enum_field_types::MYSQL_TYPE_DATETIME ); - assert!(!date_time_col.flags.contains(Flags::TIMESTAMP_FLAG)); assert!(!date_time_col.flags.contains(Flags::NUM_FLAG)); assert_eq!( to_value::(date_time_col).unwrap(), @@ -692,7 +691,6 @@ mod tests { timestamp_col.tpe, ffi::enum_field_types::MYSQL_TYPE_TIMESTAMP ); - assert!(timestamp_col.flags.contains(Flags::TIMESTAMP_FLAG)); assert!(!timestamp_col.flags.contains(Flags::NUM_FLAG)); assert_eq!( to_value::(timestamp_col).unwrap(), @@ -702,7 +700,6 @@ mod tests { let time_col = &results[15].0; assert_eq!(time_col.tpe, ffi::enum_field_types::MYSQL_TYPE_TIME); - assert!(!time_col.flags.contains(Flags::TIMESTAMP_FLAG)); assert!(!time_col.flags.contains(Flags::NUM_FLAG)); assert_eq!( to_value::(time_col).unwrap(), @@ -711,7 +708,6 @@ mod tests { let year_col = &results[16].0; assert_eq!(year_col.tpe, ffi::enum_field_types::MYSQL_TYPE_YEAR); - assert!(!year_col.flags.contains(Flags::TIMESTAMP_FLAG)); assert!(year_col.flags.contains(Flags::NUM_FLAG)); assert!(year_col.flags.contains(Flags::UNSIGNED_FLAG)); assert!(matches!(to_value::(year_col), Ok(2020))); @@ -855,9 +851,13 @@ mod tests { assert!(!multipoint_col.flags.contains(Flags::SET_FLAG)); assert!(!multipoint_col.flags.contains(Flags::ENUM_FLAG)); assert!(!multipoint_col.flags.contains(Flags::BINARY_FLAG)); - assert_eq!( - to_value::(multipoint_col).unwrap(), - "MULTIPOINT(0 0,10 10,10 20,20 20)" + // older mysql and mariadb versions get back another encoding here + // we test for both as there seems to be no clear pattern when one or + // the other is returned + let multipoint_res = to_value::(multipoint_col).unwrap(); + assert!( + multipoint_res == "MULTIPOINT((0 0),(10 10),(10 20),(20 20))" + || multipoint_res == "MULTIPOINT(0 0,10 10,10 20,20 20)" ); let multilinestring_col = &results[30].0; @@ -903,7 +903,14 @@ mod tests { ); let json_col = &results[33].0; - assert_eq!(json_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BLOB); + // mariadb >= 10.2 and mysql >=8.0 are supporting a json type + // from those mariadb >= 10.3 and mysql >= 8.0 are reporting + // json here, so we assert that we get back json + // mariadb 10.5 returns again blob + assert!( + json_col.tpe == ffi::enum_field_types::MYSQL_TYPE_JSON + || json_col.tpe == ffi::enum_field_types::MYSQL_TYPE_BLOB + ); assert!(!json_col.flags.contains(Flags::NUM_FLAG)); assert!(json_col.flags.contains(Flags::BLOB_FLAG)); assert!(!json_col.flags.contains(Flags::SET_FLAG)); @@ -1043,7 +1050,8 @@ mod tests { 41, ( b"{\"abc\": 42}".to_vec(), - (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), + MysqlType::String, + // (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), ), ); From 0b6c61ea55da266ba8a61cf70fe422b2077e71ab Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Sat, 16 May 2020 11:07:18 +0200 Subject: [PATCH 10/25] Enable json support for mysql --- diesel/src/mysql/types/json.rs | 20 ++++++++++++++++++++ diesel/src/mysql/types/mod.rs | 2 ++ diesel/src/pg/types/json.rs | 14 -------------- diesel/src/pg/types/mod.rs | 21 --------------------- diesel/src/sql_types/mod.rs | 22 ++++++++++++++++++++++ diesel/src/type_impls/json.rs | 13 +++++++++++++ diesel/src/type_impls/mod.rs | 2 ++ 7 files changed, 59 insertions(+), 35 deletions(-) create mode 100644 diesel/src/mysql/types/json.rs create mode 100644 diesel/src/type_impls/json.rs diff --git a/diesel/src/mysql/types/json.rs b/diesel/src/mysql/types/json.rs new file mode 100644 index 000000000000..22527a4690a0 --- /dev/null +++ b/diesel/src/mysql/types/json.rs @@ -0,0 +1,20 @@ +use crate::mysql::{Mysql, MysqlValue}; +use crate::deserialize::{self, FromSql}; +use crate::serialize::{self, IsNull, Output, ToSql}; +use crate::sql_types; +use std::io::prelude::*; + +impl FromSql for serde_json::Value { + fn from_sql(value: Option>) -> deserialize::Result { + let value = not_none!(value); + serde_json::from_slice(value.as_bytes()).map_err(|_| "Invalid Json".into()) + } +} + +impl ToSql for serde_json::Value { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + serde_json::to_writer(out, self) + .map(|_| IsNull::No) + .map_err(Into::into) + } +} diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index a3606074ecb7..4550aa1a7375 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -4,6 +4,8 @@ mod date_and_time; mod numeric; mod primitives; +#[cfg(featuer = "serde_json")] +mod json; use byteorder::WriteBytesExt; use std::io::Write; diff --git a/diesel/src/pg/types/json.rs b/diesel/src/pg/types/json.rs index 1717a682eea1..4b9c41648756 100644 --- a/diesel/src/pg/types/json.rs +++ b/diesel/src/pg/types/json.rs @@ -9,20 +9,6 @@ use crate::pg::{Pg, PgValue}; use crate::serialize::{self, IsNull, Output, ToSql}; use crate::sql_types; -#[allow(dead_code)] -mod foreign_derives { - use super::serde_json; - use crate::deserialize::FromSqlRow; - use crate::expression::AsExpression; - use crate::sql_types::{Json, Jsonb}; - - #[derive(FromSqlRow, AsExpression)] - #[diesel(foreign_derive)] - #[sql_type = "Json"] - #[sql_type = "Jsonb"] - struct SerdeJsonValueProxy(serde_json::Value); -} - impl FromSql for serde_json::Value { fn from_sql(value: Option>) -> deserialize::Result { let value = not_none!(value); diff --git a/diesel/src/pg/types/mod.rs b/diesel/src/pg/types/mod.rs index f946ec32be28..f29f84425195 100644 --- a/diesel/src/pg/types/mod.rs +++ b/diesel/src/pg/types/mod.rs @@ -196,27 +196,6 @@ pub mod sql_types { #[doc(hidden)] pub type Bpchar = crate::sql_types::VarChar; - /// The JSON SQL type. This type can only be used with `feature = - /// "serde_json"` - /// - /// Normally you should prefer [`Jsonb`](struct.Jsonb.html) instead, for the reasons - /// discussed there. - /// - /// ### [`ToSql`] impls - /// - /// - [`serde_json::Value`] - /// - /// ### [`FromSql`] impls - /// - /// - [`serde_json::Value`] - /// - /// [`ToSql`]: ../../../serialize/trait.ToSql.html - /// [`FromSql`]: ../../../deserialize/trait.FromSql.html - /// [`serde_json::Value`]: ../../../../serde_json/value/enum.Value.html - #[derive(Debug, Clone, Copy, Default, QueryId, SqlType)] - #[postgres(oid = "114", array_oid = "199")] - pub struct Json; - /// The `jsonb` SQL type. This type can only be used with `feature = /// "serde_json"` /// diff --git a/diesel/src/sql_types/mod.rs b/diesel/src/sql_types/mod.rs index a5bb16171f67..1c813c6100c7 100644 --- a/diesel/src/sql_types/mod.rs +++ b/diesel/src/sql_types/mod.rs @@ -341,6 +341,28 @@ pub struct Time; #[mysql_type = "Timestamp"] pub struct Timestamp; +/// The JSON SQL type. This type can only be used with `feature = +/// "serde_json"` +/// +/// Normally you should prefer [`Jsonb`](struct.Jsonb.html) instead, for the reasons +/// discussed there. +/// +/// ### [`ToSql`] impls +/// +/// - [`serde_json::Value`] +/// +/// ### [`FromSql`] impls +/// +/// - [`serde_json::Value`] +/// +/// [`ToSql`]: /serialize/trait.ToSql.html +/// [`FromSql`]: /deserialize/trait.FromSql.html +/// [`serde_json::Value`]: /../serde_json/value/enum.Value.html +#[derive(Debug, Clone, Copy, Default, QueryId, SqlType)] +#[postgres(oid = "114", array_oid = "199")] +#[mysql_type = "Json"] +pub struct Json; + /// The nullable SQL type. /// /// This wraps another SQL type to indicate that it can be null. diff --git a/diesel/src/type_impls/json.rs b/diesel/src/type_impls/json.rs new file mode 100644 index 000000000000..c5ae63bf81cc --- /dev/null +++ b/diesel/src/type_impls/json.rs @@ -0,0 +1,13 @@ +#![allow(dead_code)] + +use crate::deserialize::FromSqlRow; +use crate::expression::AsExpression; +use crate::sql_types::Json; +#[cfg(feature = "postgres")] +use crate::sql_types::Jsonb; + +#[derive(FromSqlRow, AsExpression)] +#[diesel(foreign_derive)] +#[sql_type = "Json"] +#[cfg_attr(feature = "postgres", sql_type = "Jsonb")] +struct SerdeJsonValueProxy(serde_json::Value); diff --git a/diesel/src/type_impls/mod.rs b/diesel/src/type_impls/mod.rs index bd0bcc3f6ad5..f71d5b3ffe3e 100644 --- a/diesel/src/type_impls/mod.rs +++ b/diesel/src/type_impls/mod.rs @@ -5,3 +5,5 @@ mod integers; pub mod option; mod primitives; mod tuples; +#[cfg(all(feature = "serde_json", any(feature = "postgresql", feature = "mysql")))] +mod json; From 2300231525b44d2bba976a734dad1e4bf3b2da78 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Tue, 2 Jun 2020 17:41:31 +0200 Subject: [PATCH 11/25] Fix CI --- diesel/Cargo.toml | 2 +- diesel/src/mysql/connection/bind.rs | 3 ++- diesel/src/test_helpers.rs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/diesel/Cargo.toml b/diesel/Cargo.toml index f4a9103b88e4..bd065defece3 100644 --- a/diesel/Cargo.toml +++ b/diesel/Cargo.toml @@ -43,7 +43,7 @@ dotenv = "0.15" quickcheck = "0.9" [features] -default = ["mysql", "extras"] +default = [] extras = ["chrono", "serde_json", "uuid", "deprecated-time", "network-address", "numeric", "r2d2"] unstable = ["diesel_derives/nightly"] large-tables = ["32-column-tables"] diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index d91691d4939b..9c7abd85aece 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -114,7 +114,7 @@ bitflags::bitflags! { const PART_KEY_FLAG = 16384; const GROUP_FLAG = 32768; const UNIQUE_FLAG = 65536; - const BINCMP_FLAG = 130172; + const BINCMP_FLAG = 130_172; const GET_FIXED_FIELDS_FLAG = (1<<18); const FIELD_IN_PART_FUNC_FLAG = (1 << 19); } @@ -448,6 +448,7 @@ mod tests { dbg!(T::from_sql(Some(value))) } + #[cfg(feature = "extras")] #[test] fn check_all_the_types() { let conn = crate::test_helpers::connection(); diff --git a/diesel/src/test_helpers.rs b/diesel/src/test_helpers.rs index f306fd923ab3..19d9fee21b46 100644 --- a/diesel/src/test_helpers.rs +++ b/diesel/src/test_helpers.rs @@ -37,9 +37,9 @@ cfg_if! { } pub fn database_url() -> String { - dotenv::var("MYSQL_UNIT_TEST_DATABASE_URL") + dbg!(dotenv::var("MYSQL_UNIT_TEST_DATABASE_URL") .or_else(|_| dotenv::var("DATABASE_URL")) - .expect("DATABASE_URL must be set in order to run tests") + .expect("DATABASE_URL must be set in order to run tests")) } } else { compile_error!( From 6ea97a392a850ed065926c1138e6bac347cc5c5c Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 3 Jun 2020 15:53:37 +0200 Subject: [PATCH 12/25] Fix and test the json support for mysql as well --- diesel/src/mysql/types/json.rs | 43 +++++++++++++++++-- diesel/src/mysql/types/mod.rs | 2 +- diesel/src/sql_types/mod.rs | 4 +- diesel/src/type_impls/mod.rs | 2 +- diesel_tests/Cargo.toml | 1 + diesel_tests/tests/types.rs | 22 ++++++++++ diesel_tests/tests/types_roundtrip.rs | 62 +++++++++++++++++++++++++++ 7 files changed, 129 insertions(+), 7 deletions(-) diff --git a/diesel/src/mysql/types/json.rs b/diesel/src/mysql/types/json.rs index 22527a4690a0..e9dd1608b856 100644 --- a/diesel/src/mysql/types/json.rs +++ b/diesel/src/mysql/types/json.rs @@ -1,5 +1,5 @@ -use crate::mysql::{Mysql, MysqlValue}; use crate::deserialize::{self, FromSql}; +use crate::mysql::{Mysql, MysqlValue}; use crate::serialize::{self, IsNull, Output, ToSql}; use crate::sql_types; use std::io::prelude::*; @@ -11,10 +11,47 @@ impl FromSql for serde_json::Value { } } -impl ToSql for serde_json::Value { - fn to_sql(&self, out: &mut Output) -> serialize::Result { +impl ToSql for serde_json::Value { + fn to_sql(&self, out: &mut Output) -> serialize::Result { serde_json::to_writer(out, self) .map(|_| IsNull::No) .map_err(Into::into) } } + +#[test] +fn json_to_sql() { + let mut bytes = Output::test(); + let test_json = serde_json::Value::Bool(true); + ToSql::::to_sql(&test_json, &mut bytes).unwrap(); + assert_eq!(bytes, b"true"); +} + +#[test] +fn some_json_from_sql() { + use crate::mysql::MysqlType; + let input_json = b"true"; + let output_json: serde_json::Value = FromSql::::from_sql(Some( + MysqlValue::new(input_json, MysqlType::Json), + )) + .unwrap(); + assert_eq!(output_json, serde_json::Value::Bool(true)); +} + +#[test] +fn bad_json_from_sql() { + use crate::mysql::MysqlType; + let uuid: Result = FromSql::::from_sql(Some( + MysqlValue::new(b"boom", MysqlType::Json), + )); + assert_eq!(uuid.unwrap_err().to_string(), "Invalid Json"); +} + +#[test] +fn no_json_from_sql() { + let uuid: Result = FromSql::::from_sql(None); + assert_eq!( + uuid.unwrap_err().to_string(), + "Unexpected null for non-null column" + ); +} diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index 4550aa1a7375..c7fa83f3407f 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -4,7 +4,7 @@ mod date_and_time; mod numeric; mod primitives; -#[cfg(featuer = "serde_json")] +#[cfg(feature = "serde_json")] mod json; use byteorder::WriteBytesExt; diff --git a/diesel/src/sql_types/mod.rs b/diesel/src/sql_types/mod.rs index 1c813c6100c7..0668e199ced0 100644 --- a/diesel/src/sql_types/mod.rs +++ b/diesel/src/sql_types/mod.rs @@ -344,8 +344,8 @@ pub struct Timestamp; /// The JSON SQL type. This type can only be used with `feature = /// "serde_json"` /// -/// Normally you should prefer [`Jsonb`](struct.Jsonb.html) instead, for the reasons -/// discussed there. +/// For postgresql you should normally prefer [`Jsonb`](struct.Jsonb.html) instead, +/// for the reasons discussed there. /// /// ### [`ToSql`] impls /// diff --git a/diesel/src/type_impls/mod.rs b/diesel/src/type_impls/mod.rs index f71d5b3ffe3e..f6fb7159fdeb 100644 --- a/diesel/src/type_impls/mod.rs +++ b/diesel/src/type_impls/mod.rs @@ -5,5 +5,5 @@ mod integers; pub mod option; mod primitives; mod tuples; -#[cfg(all(feature = "serde_json", any(feature = "postgresql", feature = "mysql")))] +#[cfg(all(feature = "serde_json", any(feature = "postgres", feature = "mysql")))] mod json; diff --git a/diesel_tests/Cargo.toml b/diesel_tests/Cargo.toml index 8e941a082ba8..2b7e8de4d0c0 100644 --- a/diesel_tests/Cargo.toml +++ b/diesel_tests/Cargo.toml @@ -23,6 +23,7 @@ uuid = { version = ">=0.7.0, <0.9.0" } serde_json = { version=">=0.9, <2.0" } ipnetwork = ">=0.12.2, <0.17.0" bigdecimal = ">= 0.0.13, < 0.2.0" +rand = "0.7" [features] default = [] diff --git a/diesel_tests/tests/types.rs b/diesel_tests/tests/types.rs index 9b4303f4d592..8c528cc9bbe4 100644 --- a/diesel_tests/tests/types.rs +++ b/diesel_tests/tests/types.rs @@ -354,6 +354,28 @@ fn i64_to_sql_bigint() { )); } +#[test] +#[cfg(feature = "mysql")] +fn mysql_json_from_sql() { + let query = "'true'"; + let expected_value = serde_json::Value::Bool(true); + assert_eq!( + expected_value, + query_single_value::(query) + ); +} + +#[test] +#[cfg(feature = "mysql")] +fn mysql_json_to_sql_json() { + let expected_value = "'false'"; + let value = serde_json::Value::Bool(false); + assert!(query_to_sql_equality::( + expected_value, + value + )); +} + use std::{f32, f64}; #[test] diff --git a/diesel_tests/tests/types_roundtrip.rs b/diesel_tests/tests/types_roundtrip.rs index 99e46700d1e8..0659f3c22415 100644 --- a/diesel_tests/tests/types_roundtrip.rs +++ b/diesel_tests/tests/types_roundtrip.rs @@ -204,6 +204,9 @@ mod pg_types { mk_tstz_bounds ); + test_round_trip!(json_roundtrips, Json, SerdeWrapper, mk_serde_json); + test_round_trip!(jsonb_roundtrips, Jsonb, SerdeWrapper, mk_serde_json); + fn mk_uuid(data: (u32, u16, u16, (u8, u8, u8, u8, u8, u8, u8, u8))) -> self::uuid::Uuid { let a = data.3; let b = [a.0, a.1, a.2, a.3, a.4, a.5, a.6, a.7]; @@ -293,6 +296,7 @@ mod mysql_types { test_round_trip!(u16_roundtrips, Unsigned, u16); test_round_trip!(u32_roundtrips, Unsigned, u32); test_round_trip!(u64_roundtrips, Unsigned, u64); + test_round_trip!(json_roundtrips, Json, SerdeWrapper, mk_serde_json); } pub fn mk_naive_datetime(data: (i64, u32)) -> NaiveDateTime { @@ -344,6 +348,64 @@ pub fn mk_naive_date(days: u32) -> NaiveDate { earliest_sqlite_date + Duration::days(days as i64 % num_days_representable) } +#[cfg(any(feature = "postgres", feature = "mysql"))] +#[derive(Clone, Debug)] +struct SerdeWrapper(serde_json::Value); + +#[cfg(any(feature = "postgres", feature = "mysql"))] +impl quickcheck::Arbitrary for SerdeWrapper { + fn arbitrary(g: &mut G) -> Self { + SerdeWrapper(arbitrary_serde(g, 0)) + } +} + +#[cfg(any(feature = "postgres", feature = "mysql"))] +fn arbitrary_serde(g: &mut G, depth: usize) -> serde_json::Value { + use rand::distributions::Alphanumeric; + use rand::Rng; + let variant = match g.gen_range(0, 6) { + 4 | 5 if depth > 0 => g.gen_range(0, 4), + i => i, + }; + match variant { + 0 => serde_json::Value::Null, + 1 => serde_json::Value::Bool(g.gen()), + 2 => { + // don't use floats here + // comparing floats is complicated + let n: i32 = g.gen(); + serde_json::Value::Number(n.into()) + } + 3 => { + let len = g.gen_range(0, 15); + let s: String = g.sample_iter(Alphanumeric).take(len).collect(); + serde_json::Value::String(s) + } + 4 => { + let len = g.gen_range(0, 15); + let values = (0..len).map(|_| arbitrary_serde(g, depth + 1)).collect(); + serde_json::Value::Array(values) + } + 5 => { + let fields = g.gen_range(1, 5); + let map = (0..fields) + .map(|_| { + let len = g.gen_range(0, 5); + let name = g.sample_iter(Alphanumeric).take(len).collect(); + (name, arbitrary_serde(g, depth + 1)) + }) + .collect(); + serde_json::Value::Object(map) + } + _ => unimplemented!(), + } +} + +#[cfg(any(feature = "postgres", feature = "mysql"))] +fn mk_serde_json(data: SerdeWrapper) -> serde_json::Value { + data.0 +} + #[cfg(feature = "postgres")] mod unstable_types { use super::{quickcheck, test_type_round_trips}; From 67aceb2893b72ccec104240617ca7fc1b29fc5c2 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 3 Jun 2020 16:12:46 +0200 Subject: [PATCH 13/25] Rustfmt --- diesel/src/mysql/connection/stmt/iterator.rs | 5 ++++- diesel/src/mysql/connection/stmt/mod.rs | 5 +---- diesel/src/mysql/types/mod.rs | 4 ++-- diesel/src/type_impls/mod.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/diesel/src/mysql/connection/stmt/iterator.rs b/diesel/src/mysql/connection/stmt/iterator.rs index ca018e3686c4..f7d9311a785c 100644 --- a/diesel/src/mysql/connection/stmt/iterator.rs +++ b/diesel/src/mysql/connection/stmt/iterator.rs @@ -121,7 +121,10 @@ impl<'a> NamedRow for NamedMysqlRow<'a> { } } -pub(in crate::mysql::connection) fn execute_statement(stmt: &mut Statement, binds: &mut Binds) -> QueryResult<()> { +pub(in crate::mysql::connection) fn execute_statement( + stmt: &mut Statement, + binds: &mut Binds, +) -> QueryResult<()> { unsafe { binds.with_mysql_binds(|bind_ptr| stmt.bind_result(bind_ptr))?; stmt.execute()?; diff --git a/diesel/src/mysql/connection/stmt/mod.rs b/diesel/src/mysql/connection/stmt/mod.rs index edb0952a4883..9526cc2e0c4b 100644 --- a/diesel/src/mysql/connection/stmt/mod.rs +++ b/diesel/src/mysql/connection/stmt/mod.rs @@ -72,10 +72,7 @@ impl Statement { /// This function should be called instead of `execute` for queries which /// have a return value. After calling this function, `execute` can never /// be called on this statement. - pub unsafe fn results( - &mut self, - types: Vec, - ) -> QueryResult { + pub unsafe fn results(&mut self, types: Vec) -> QueryResult { StatementIterator::new(self, types) } diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index c7fa83f3407f..48d6aa05e752 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -2,10 +2,10 @@ #[cfg(feature = "chrono")] mod date_and_time; -mod numeric; -mod primitives; #[cfg(feature = "serde_json")] mod json; +mod numeric; +mod primitives; use byteorder::WriteBytesExt; use std::io::Write; diff --git a/diesel/src/type_impls/mod.rs b/diesel/src/type_impls/mod.rs index f6fb7159fdeb..ac604b48260c 100644 --- a/diesel/src/type_impls/mod.rs +++ b/diesel/src/type_impls/mod.rs @@ -2,8 +2,8 @@ mod date_and_time; mod decimal; pub mod floats; mod integers; +#[cfg(all(feature = "serde_json", any(feature = "postgres", feature = "mysql")))] +mod json; pub mod option; mod primitives; mod tuples; -#[cfg(all(feature = "serde_json", any(feature = "postgres", feature = "mysql")))] -mod json; From 8325d516a070ba1827ec4ee7273891d75157154f Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 3 Jun 2020 16:15:34 +0200 Subject: [PATCH 14/25] Fix custom types example --- examples/postgres/custom_types/src/model.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/postgres/custom_types/src/model.rs b/examples/postgres/custom_types/src/model.rs index a76d02b470c2..2acc278faef2 100644 --- a/examples/postgres/custom_types/src/model.rs +++ b/examples/postgres/custom_types/src/model.rs @@ -1,5 +1,8 @@ -use diesel::pg::PgValue; +use diesel::deserialize::{self, FromSql, FromSqlRow}; +use diesel::expression::AsExpression; +use diesel::pg::{Pg, PgValue}; use diesel::serialize::{self, IsNull, Output, ToSql}; +use diesel::sql_types::SqlType; use std::io::Write; pub mod exports { @@ -29,9 +32,6 @@ impl ToSql for Language { } } -use diesel::deserialize::{self, FromSql}; -use diesel::pg::Pg; - impl FromSql for Language { fn from_sql(bytes: Option) -> deserialize::Result { match not_none!(bytes).as_bytes() { From 6ac48a76b130c9080da3665768b7b0e7e3977e67 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 3 Jun 2020 17:15:02 +0200 Subject: [PATCH 15/25] Try to fix json handling for mysql 8.0 --- diesel/src/mysql/backend.rs | 2 -- diesel/src/mysql/connection/bind.rs | 5 +++-- diesel/src/sql_types/mod.rs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index 62c07b1ee85a..3212f9c7207b 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -58,8 +58,6 @@ pub enum MysqlType { Set, /// Enum, - /// - Json, } impl Backend for Mysql { diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 9c7abd85aece..2268ee0fd48b 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -262,7 +262,6 @@ impl From for (ffi::enum_field_types, Flags) { MysqlType::Blob => MYSQL_TYPE_BLOB, MysqlType::Numeric => MYSQL_TYPE_NEWDECIMAL, MysqlType::Bit => MYSQL_TYPE_BIT, - MysqlType::Json => MYSQL_TYPE_JSON, MysqlType::UnsignedTiny => { flags = Flags::UNSIGNED_FLAG; MYSQL_TYPE_TINY @@ -320,7 +319,9 @@ impl From<(ffi::enum_field_types, Flags)> for MysqlType { MYSQL_TYPE_DATE => MysqlType::Date, MYSQL_TYPE_DATETIME => MysqlType::DateTime, MYSQL_TYPE_TIMESTAMP => MysqlType::Timestamp, - MYSQL_TYPE_JSON => MysqlType::Json, + // Treat json as string because even mysql 8.0 + // throws errors sometimes if we use json for json + MYSQL_TYPE_JSON => MysqlType::String, // The documentation states that // MYSQL_TYPE_STRING is used for enums and sets diff --git a/diesel/src/sql_types/mod.rs b/diesel/src/sql_types/mod.rs index 0668e199ced0..bb5994e55332 100644 --- a/diesel/src/sql_types/mod.rs +++ b/diesel/src/sql_types/mod.rs @@ -360,7 +360,7 @@ pub struct Timestamp; /// [`serde_json::Value`]: /../serde_json/value/enum.Value.html #[derive(Debug, Clone, Copy, Default, QueryId, SqlType)] #[postgres(oid = "114", array_oid = "199")] -#[mysql_type = "Json"] +#[mysql_type = "String"] pub struct Json; /// The nullable SQL type. From 94e66edefdd052c338059075702c5a02658e762c Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 3 Jun 2020 17:15:24 +0200 Subject: [PATCH 16/25] Update ci to ubuntu 20.04 to get hopefully a newer mysql version --- .github/workflows/ci.yml | 2 +- diesel/src/mysql/types/json.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26dee3bb1ddc..3a4438cfa72c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: matrix: rust: ["stable", "beta", "nightly"] backend: ["postgres", "sqlite", "mysql"] - os: [ubuntu-18.04, macos-latest, windows-latest] + os: [ubuntu-20.04, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout sources diff --git a/diesel/src/mysql/types/json.rs b/diesel/src/mysql/types/json.rs index e9dd1608b856..237027b13b91 100644 --- a/diesel/src/mysql/types/json.rs +++ b/diesel/src/mysql/types/json.rs @@ -32,7 +32,7 @@ fn some_json_from_sql() { use crate::mysql::MysqlType; let input_json = b"true"; let output_json: serde_json::Value = FromSql::::from_sql(Some( - MysqlValue::new(input_json, MysqlType::Json), + MysqlValue::new(input_json, MysqlType::String), )) .unwrap(); assert_eq!(output_json, serde_json::Value::Bool(true)); @@ -42,7 +42,7 @@ fn some_json_from_sql() { fn bad_json_from_sql() { use crate::mysql::MysqlType; let uuid: Result = FromSql::::from_sql(Some( - MysqlValue::new(b"boom", MysqlType::Json), + MysqlValue::new(b"boom", MysqlType::String), )); assert_eq!(uuid.unwrap_err().to_string(), "Invalid Json"); } From 1b0453f2cb814d39fbceb21807c5fb8c4ce9a6cc Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Thu, 4 Jun 2020 09:59:03 +0200 Subject: [PATCH 17/25] Fix #2373 This change fixes a out of buffer write in newer versions of libmysqlclient. That write was caused by a change in libmysqlclient around version 8.0.19 that added another field to `MYSQL_TIME` and by diesel using a statically generated binding (mysqlclient-sys). As consequence diesel would allocate a to small buffer for the struct while libmysql writes more bytes that expected, which results in a out of bound write. Interestingly this does not happen with a recent libmysqlclient version based on the sources in the mariadb source code. As a fix I move an updated version of `MYSQL_TIME` to diesel itself, as I do not have any control over `mysqlclient-sys`. This change is compatible with older libmysqlclient versions, because for them we just allocate a larger buffer than they actually use, which is fine. --- diesel/src/mysql/connection/bind.rs | 3 +- diesel/src/mysql/types/date_and_time.rs | 42 +++++++++++++------------ diesel/src/mysql/types/mod.rs | 17 ++++++++++ diesel/src/mysql/value.rs | 12 +++---- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 2268ee0fd48b..918ed34caaaa 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -4,6 +4,7 @@ use std::mem; use std::os::raw as libc; use super::stmt::Statement; +use crate::mysql::types::MYSQL_TIME; use crate::mysql::{MysqlType, MysqlValue}; use crate::result::QueryResult; @@ -421,7 +422,7 @@ fn known_buffer_size_for_ffi_type(tpe: ffi::enum_field_types) -> Option { t::MYSQL_TYPE_TIME | t::MYSQL_TYPE_DATE | t::MYSQL_TYPE_DATETIME - | t::MYSQL_TYPE_TIMESTAMP => Some(size_of::()), + | t::MYSQL_TYPE_TIMESTAMP => Some(size_of::()), _ => None, } } diff --git a/diesel/src/mysql/types/date_and_time.rs b/diesel/src/mysql/types/date_and_time.rs index c345b342d7cf..9709c1d2ef3f 100644 --- a/diesel/src/mysql/types/date_and_time.rs +++ b/diesel/src/mysql/types/date_and_time.rs @@ -1,11 +1,10 @@ -extern crate chrono; -extern crate mysqlclient_sys as ffi; - -use self::chrono::*; +use chrono::*; +use mysqlclient_sys as ffi; use std::io::Write; use std::os::raw as libc; use std::{mem, slice}; +use super::MYSQL_TIME; use crate::deserialize::{self, FromSql}; use crate::mysql::{Mysql, MysqlValue}; use crate::serialize::{self, IsNull, Output, ToSql}; @@ -13,18 +12,18 @@ use crate::sql_types::{Date, Datetime, Time, Timestamp}; macro_rules! mysql_time_impls { ($ty:ty) => { - impl ToSql<$ty, Mysql> for ffi::MYSQL_TIME { + impl ToSql<$ty, Mysql> for MYSQL_TIME { fn to_sql(&self, out: &mut Output) -> serialize::Result { let bytes = unsafe { - let bytes_ptr = self as *const ffi::MYSQL_TIME as *const u8; - slice::from_raw_parts(bytes_ptr, mem::size_of::()) + let bytes_ptr = self as *const MYSQL_TIME as *const u8; + slice::from_raw_parts(bytes_ptr, mem::size_of::()) }; out.write_all(bytes)?; Ok(IsNull::No) } } - impl FromSql<$ty, Mysql> for ffi::MYSQL_TIME { + impl FromSql<$ty, Mysql> for MYSQL_TIME { fn from_sql(value: Option>) -> deserialize::Result { let data = not_none!(value); data.time_value() @@ -52,7 +51,7 @@ impl FromSql for NaiveDateTime { impl ToSql for NaiveDateTime { fn to_sql(&self, out: &mut Output) -> serialize::Result { - let mysql_time = ffi::MYSQL_TIME { + let mysql_time = MYSQL_TIME { year: self.year() as libc::c_uint, month: self.month() as libc::c_uint, day: self.day() as libc::c_uint, @@ -60,17 +59,18 @@ impl ToSql for NaiveDateTime { minute: self.minute() as libc::c_uint, second: self.second() as libc::c_uint, second_part: libc::c_ulong::from(self.timestamp_subsec_micros()), - neg: 0, + neg: false, time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATETIME, + time_zone_displacement: 0, }; - >::to_sql(&mysql_time, out) + >::to_sql(&mysql_time, out) } } impl FromSql for NaiveDateTime { fn from_sql(bytes: Option>) -> deserialize::Result { - let mysql_time = >::from_sql(bytes)?; + let mysql_time = >::from_sql(bytes)?; NaiveDate::from_ymd_opt( mysql_time.year as i32, @@ -91,7 +91,7 @@ impl FromSql for NaiveDateTime { impl ToSql for NaiveTime { fn to_sql(&self, out: &mut serialize::Output) -> serialize::Result { - let mysql_time = ffi::MYSQL_TIME { + let mysql_time = MYSQL_TIME { hour: self.hour() as libc::c_uint, minute: self.minute() as libc::c_uint, second: self.second() as libc::c_uint, @@ -99,17 +99,18 @@ impl ToSql for NaiveTime { month: 0, second_part: 0, year: 0, - neg: 0, + neg: false, time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_TIME, + time_zone_displacement: 0, }; - >::to_sql(&mysql_time, out) + >::to_sql(&mysql_time, out) } } impl FromSql for NaiveTime { fn from_sql(bytes: Option>) -> deserialize::Result { - let mysql_time = >::from_sql(bytes)?; + let mysql_time = >::from_sql(bytes)?; NaiveTime::from_hms_opt( mysql_time.hour as u32, mysql_time.minute as u32, @@ -121,7 +122,7 @@ impl FromSql for NaiveTime { impl ToSql for NaiveDate { fn to_sql(&self, out: &mut Output) -> serialize::Result { - let mysql_time = ffi::MYSQL_TIME { + let mysql_time = MYSQL_TIME { year: self.year() as libc::c_uint, month: self.month() as libc::c_uint, day: self.day() as libc::c_uint, @@ -129,17 +130,18 @@ impl ToSql for NaiveDate { minute: 0, second: 0, second_part: 0, - neg: 0, + neg: false, time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATE, + time_zone_displacement: 0, }; - >::to_sql(&mysql_time, out) + >::to_sql(&mysql_time, out) } } impl FromSql for NaiveDate { fn from_sql(bytes: Option>) -> deserialize::Result { - let mysql_time = >::from_sql(bytes)?; + let mysql_time = >::from_sql(bytes)?; NaiveDate::from_ymd_opt( mysql_time.year as i32, mysql_time.month as u32, diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index 48d6aa05e752..1bda52895d9f 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -8,7 +8,9 @@ mod numeric; mod primitives; use byteorder::WriteBytesExt; +use mysqlclient_sys as ffi; use std::io::Write; +use std::os::raw as libc; use crate::deserialize::{self, FromSql}; use crate::mysql::{Mysql, MysqlType, MysqlValue}; @@ -17,6 +19,21 @@ use crate::serialize::{self, IsNull, Output, ToSql}; use crate::sql_types::ops::*; use crate::sql_types::*; +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub(crate) struct MYSQL_TIME { + pub year: libc::c_uint, + pub month: libc::c_uint, + pub day: libc::c_uint, + pub hour: libc::c_uint, + pub minute: libc::c_uint, + pub second: libc::c_uint, + pub second_part: libc::c_ulong, + pub neg: bool, + pub time_type: ffi::enum_mysql_timestamp_type, + pub time_zone_displacement: libc::c_int, +} + impl ToSql for i8 { fn to_sql(&self, out: &mut Output) -> serialize::Result { out.write_i8(*self).map(|_| IsNull::No).map_err(Into::into) diff --git a/diesel/src/mysql/value.rs b/diesel/src/mysql/value.rs index a9e6e5ce1bb4..8b33bffc7543 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -1,6 +1,6 @@ use super::MysqlType; use crate::deserialize; -use mysqlclient_sys as ffi; +use crate::mysql::types::MYSQL_TIME; use std::error::Error; /// Raw mysql value as received from the database @@ -26,15 +26,15 @@ impl<'a> MysqlValue<'a> { // so clippy is clearly wrong here // https://github.com/rust-lang/rust-clippy/issues/2881 #[allow(dead_code, clippy::cast_ptr_alignment)] - pub(crate) fn time_value(&self) -> deserialize::Result { + pub(crate) fn time_value(&self) -> deserialize::Result { match self.tpe { MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { - let ptr = self.raw.as_ptr() as *const ffi::MYSQL_TIME; + let ptr = self.raw.as_ptr() as *const MYSQL_TIME; let result = unsafe { ptr.read_unaligned() }; - if result.neg == 0 { - Ok(result) - } else { + if result.neg { Err("Negative dates/times are not yet supported".into()) + } else { + Ok(result) } } _ => Err(self.invalid_type_code("timestamp")), From 54ffbf3fa288d0e68f282d48074aa0303d2c4be2 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 5 Jun 2020 23:36:42 +0200 Subject: [PATCH 18/25] Add a changelog entry for mysql value change --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67beefeda50a..0f2b1b521d84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,8 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/ `#[non_exhaustive]`. If you matched on one of those variants explicitly you need to introduce a wild card match instead. +* The `TypeMetadata` type for `Mysql` changed to `MysqlType` + ### Fixed * Many types were incorrectly considered non-aggregate when they should not @@ -127,6 +129,8 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/ [the SQLite URI documentation]: https://www.sqlite.org/uri.html +* We've refactored our type translation layer for Mysql to handle more types now. + ### Deprecated * `diesel_(prefix|postfix|infix)_operator!` have been deprecated. These macros From db56fcea5b5997c54bdb44eabe7689333fb83de7 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 5 Jun 2020 23:37:07 +0200 Subject: [PATCH 19/25] Add a comment clarifying why we have a own variant of `MYSQL_TIME` --- diesel/src/mysql/types/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/diesel/src/mysql/types/mod.rs b/diesel/src/mysql/types/mod.rs index 1bda52895d9f..302bb8683ba7 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -19,6 +19,11 @@ use crate::serialize::{self, IsNull, Output, ToSql}; use crate::sql_types::ops::*; use crate::sql_types::*; +// A internal helper type +// This type also exists in mysqlclient_sys +// but the definition changed over time +// to remain backward compatible with old mysqlclient_sys +// version we just have our own copy here #[repr(C)] #[derive(Debug, Clone, Copy)] pub(crate) struct MYSQL_TIME { From 5668b751b8fffb5ad706692780c42b4dc99fe943 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 5 Jun 2020 23:37:46 +0200 Subject: [PATCH 20/25] Improve the documentation for `MysqlType` --- diesel/src/mysql/backend.rs | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/diesel/src/mysql/backend.rs b/diesel/src/mysql/backend.rs index 3212f9c7207b..39482b871747 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -13,50 +13,52 @@ use crate::sql_types::TypeMetadata; pub struct Mysql; #[allow(missing_debug_implementations)] -/// Represents the possible types, that can be transmitted as via the +/// Represents possible types, that can be transmitted as via the /// Mysql wire protocol #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum MysqlType { - /// + /// A 8 bit signed integer Tiny, - /// Sets + /// A 8 bit unsigned integer UnsignedTiny, - /// Sets `buffer_type` to `MYSQL_TYPE_SHORT` + /// A 16 bit signed integer Short, - /// + /// A 16 bit unsigned integer UnsignedShort, - /// Sets `buffer_type` to `MYSQL_TYPE_LONG` + /// A 32 bit signed integer Long, - /// + /// A 32 bit unsigned integer UnsignedLong, - /// Sets `buffer_type` to `MYSQL_TYPE_LONGLONG` + /// A 64 bit signed integer LongLong, - /// + /// A 64 bit unsigned integer UnsignedLongLong, - /// Sets `buffer_type` to `MYSQL_TYPE_FLOAT` + /// A 32 bit floating point number Float, - /// Sets `buffer_type` to `MYSQL_TYPE_DOUBLE` + /// A 64 bit floating point number Double, - /// Sets `buffer_type` to `MYSQL_TYPE_NEWDECIMAL` + /// A fixed point decimal value Numeric, - /// Sets `buffer_type` to `MYSQL_TYPE_TIME` + /// A datatype to store a time value Time, - /// Sets `buffer_type` to `MYSQL_TYPE_DATE` + /// A datatype to store a date value Date, - /// Sets `buffer_type` to `MYSQL_TYPE_DATETIME` + /// A datatype containing timestamp values ranging from + /// '1000-01-01 00:00:00' to '9999-12-31 23:59:59'. DateTime, - /// Sets `buffer_type` to `MYSQL_TYPE_TIMESTAMP` + /// A datatype containing timestamp values ranging from + /// 1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC. Timestamp, - /// Sets `buffer_type` to `MYSQL_TYPE_STRING` + /// A datatype for string values String, - /// Sets `buffer_type` to `MYSQL_TYPE_BLOB` + /// A datatype containing binary large objects Blob, - /// Sets `buffer_type` to `MYSQL_TYPE_BIT` + /// A value containing a set of bit's Bit, - /// + /// A user defined set type Set, - /// + /// A user defined enum type Enum, } From f94495af43bf9681e3f431d6b6a07210fc7f57c2 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 5 Jun 2020 23:38:50 +0200 Subject: [PATCH 21/25] Some cleanup and minor style improvments for changes in binds.rs --- diesel/src/mysql/connection/bind.rs | 45 +++++++------------- diesel/src/mysql/connection/stmt/iterator.rs | 40 ++++------------- diesel/src/mysql/connection/stmt/mod.rs | 33 ++++++++++++-- 3 files changed, 53 insertions(+), 65 deletions(-) diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 918ed34caaaa..9daff5b4fd5f 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -1,5 +1,4 @@ -extern crate mysqlclient_sys as ffi; - +use mysqlclient_sys as ffi; use std::mem; use std::os::raw as libc; @@ -42,7 +41,11 @@ impl Binds { .map(|field| { ( field.type_, - Flags::from_bits(field.flags).expect("No unknown flags"), + Flags::from_bits(field.flags).expect( + "We encountered a unknown type flag while parsing \ + Mysql's type information. If you see this error message \ + please open an issue at diesels github page.", + ), ) }) .map(BindData::for_output) @@ -427,7 +430,6 @@ fn known_buffer_size_for_ffi_type(tpe: ffi::enum_field_types) -> Option { } } -#[allow(warnings)] #[cfg(test)] mod tests { use super::*; @@ -435,7 +437,6 @@ mod tests { use super::MysqlValue; use crate::deserialize::FromSql; - use crate::mysql::connection::stmt::iterator::NamedStatementIterator; use crate::sql_types::*; fn to_value( @@ -552,16 +553,10 @@ mod tests { )) .unwrap(); - let results = unsafe { stmt.named_results().unwrap() }; - - let NamedStatementIterator { - stmt, - mut output_binds, - metadata, - } = results; - - crate::mysql::connection::stmt::iterator::populate_row_buffers(stmt, &mut output_binds) - .unwrap(); + let metadata = stmt.metadata().unwrap(); + let mut output_binds = Binds::from_result_metadata(metadata.fields()); + stmt.execute_statement(&mut output_binds).unwrap(); + stmt.populate_row_buffers(&mut output_binds).unwrap(); let results: Vec<(BindData, &ffi::st_mysql_field)> = output_binds .data @@ -654,13 +649,13 @@ mod tests { assert_eq!(float_col.tpe, ffi::enum_field_types::MYSQL_TYPE_FLOAT); assert!(float_col.flags.contains(Flags::NUM_FLAG)); assert!(!float_col.flags.contains(Flags::UNSIGNED_FLAG)); - assert!(matches!(to_value::(float_col), Ok(1.23))); + assert_eq!(to_value::(float_col).unwrap(), 1.23); let double_col = &results[10].0; assert_eq!(double_col.tpe, ffi::enum_field_types::MYSQL_TYPE_DOUBLE); assert!(double_col.flags.contains(Flags::NUM_FLAG)); assert!(!double_col.flags.contains(Flags::UNSIGNED_FLAG)); - assert!(matches!(to_value::(double_col), Ok(4.5678))); + assert_eq!(to_value::(double_col).unwrap(), 4.5678); let bit_col = &results[11].0; assert_eq!(bit_col.tpe, ffi::enum_field_types::MYSQL_TYPE_BIT); @@ -936,9 +931,8 @@ mod tests { let mut binds = Binds { data: vec![bind] }; - crate::mysql::connection::stmt::iterator::execute_statement(&mut stmt, &mut binds).unwrap(); - - crate::mysql::connection::stmt::iterator::populate_row_buffers(&stmt, &mut binds).unwrap(); + stmt.execute_statement(&mut binds).unwrap(); + stmt.populate_row_buffers(&mut binds).unwrap(); binds.data.remove(0) } @@ -974,18 +968,11 @@ mod tests { is_truncated: None, }; - let mut binds = Binds { + let binds = Binds { data: vec![id_bind, field_bind], }; - - binds.with_mysql_binds(|bind_ptr| unsafe { - ffi::mysql_stmt_bind_param(stmt.stmt.as_ptr(), bind_ptr); - }); - + stmt.input_bind(binds).unwrap(); stmt.did_an_error_occur().unwrap(); - - let mut out_binds = Binds { data: vec![] }; - unsafe { stmt.execute().unwrap(); } diff --git a/diesel/src/mysql/connection/stmt/iterator.rs b/diesel/src/mysql/connection/stmt/iterator.rs index f7d9311a785c..2321efcacd07 100644 --- a/diesel/src/mysql/connection/stmt/iterator.rs +++ b/diesel/src/mysql/connection/stmt/iterator.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use super::{ffi, libc, Binds, Statement, StatementMetadata}; +use super::{Binds, Statement, StatementMetadata}; use crate::mysql::{Mysql, MysqlType, MysqlValue}; use crate::result::QueryResult; use crate::row::*; @@ -16,7 +16,7 @@ impl<'a> StatementIterator<'a> { pub fn new(stmt: &'a mut Statement, types: Vec) -> QueryResult { let mut output_binds = Binds::from_output_types(types); - execute_statement(stmt, &mut output_binds)?; + stmt.execute_statement(&mut output_binds)?; Ok(StatementIterator { stmt, output_binds }) } @@ -33,7 +33,7 @@ impl<'a> StatementIterator<'a> { } fn next(&mut self) -> Option> { - match populate_row_buffers(self.stmt, &mut self.output_binds) { + match self.stmt.populate_row_buffers(&mut self.output_binds) { Ok(Some(())) => Some(Ok(MysqlRow { col_idx: 0, binds: &mut self.output_binds, @@ -62,9 +62,9 @@ impl<'a> Row for MysqlRow<'a> { } pub struct NamedStatementIterator<'a> { - pub(crate) stmt: &'a mut Statement, - pub(crate) output_binds: Binds, - pub(crate) metadata: StatementMetadata, + stmt: &'a mut Statement, + output_binds: Binds, + metadata: StatementMetadata, } #[allow(clippy::should_implement_trait)] // don't need `Iterator` here @@ -74,7 +74,7 @@ impl<'a> NamedStatementIterator<'a> { let metadata = stmt.metadata()?; let mut output_binds = Binds::from_result_metadata(metadata.fields()); - execute_statement(stmt, &mut output_binds)?; + stmt.execute_statement(&mut output_binds)?; Ok(NamedStatementIterator { stmt, @@ -95,7 +95,7 @@ impl<'a> NamedStatementIterator<'a> { } fn next(&mut self) -> Option> { - match populate_row_buffers(self.stmt, &mut self.output_binds) { + match self.stmt.populate_row_buffers(&mut self.output_binds) { Ok(Some(())) => Some(Ok(NamedMysqlRow { binds: &self.output_binds, column_indices: self.metadata.column_indices(), @@ -120,27 +120,3 @@ impl<'a> NamedRow for NamedMysqlRow<'a> { self.binds.field_data(idx) } } - -pub(in crate::mysql::connection) fn execute_statement( - stmt: &mut Statement, - binds: &mut Binds, -) -> QueryResult<()> { - unsafe { - binds.with_mysql_binds(|bind_ptr| stmt.bind_result(bind_ptr))?; - stmt.execute()?; - } - Ok(()) -} - -pub(crate) fn populate_row_buffers(stmt: &Statement, binds: &mut Binds) -> QueryResult> { - let next_row_result = unsafe { ffi::mysql_stmt_fetch(stmt.stmt.as_ptr()) }; - match next_row_result as libc::c_uint { - ffi::MYSQL_NO_DATA => Ok(None), - ffi::MYSQL_DATA_TRUNCATED => binds.populate_dynamic_buffers(stmt).map(Some), - 0 => { - binds.update_buffer_lengths(); - Ok(Some(())) - } - _error => stmt.did_an_error_occur().map(Some), - } -} diff --git a/diesel/src/mysql/connection/stmt/mod.rs b/diesel/src/mysql/connection/stmt/mod.rs index 9526cc2e0c4b..713a4dd74fc6 100644 --- a/diesel/src/mysql/connection/stmt/mod.rs +++ b/diesel/src/mysql/connection/stmt/mod.rs @@ -1,6 +1,6 @@ extern crate mysqlclient_sys as ffi; -pub(super) mod iterator; +mod iterator; mod metadata; use std::ffi::CStr; @@ -14,7 +14,7 @@ use crate::mysql::MysqlType; use crate::result::{DatabaseErrorKind, QueryResult}; pub struct Statement { - pub(super) stmt: NonNull, + stmt: NonNull, input_binds: Option, } @@ -41,7 +41,11 @@ impl Statement { where Iter: IntoIterator>)>, { - let mut input_binds = Binds::from_input_data(binds); + let input_binds = Binds::from_input_data(binds); + self.input_bind(input_binds) + } + + pub(super) fn input_bind(&mut self, mut input_binds: Binds) -> QueryResult<()> { input_binds.with_mysql_binds(|bind_ptr| { // This relies on the invariant that the current value of `self.input_binds` // will not change without this function being called @@ -111,7 +115,7 @@ impl Statement { self.did_an_error_occur() } - fn metadata(&self) -> QueryResult { + pub(super) fn metadata(&self) -> QueryResult { use crate::result::Error::DeserializationError; let result_ptr = unsafe { ffi::mysql_stmt_result_metadata(self.stmt.as_ptr()).as_mut() }; @@ -149,6 +153,27 @@ impl Statement { _ => DatabaseErrorKind::__Unknown, } } + + pub(super) fn execute_statement(&mut self, binds: &mut Binds) -> QueryResult<()> { + unsafe { + binds.with_mysql_binds(|bind_ptr| self.bind_result(bind_ptr))?; + self.execute()?; + } + Ok(()) + } + + pub(super) fn populate_row_buffers(&self, binds: &mut Binds) -> QueryResult> { + let next_row_result = unsafe { ffi::mysql_stmt_fetch(self.stmt.as_ptr()) }; + match next_row_result as libc::c_uint { + ffi::MYSQL_NO_DATA => Ok(None), + ffi::MYSQL_DATA_TRUNCATED => binds.populate_dynamic_buffers(self).map(Some), + 0 => { + binds.update_buffer_lengths(); + Ok(Some(())) + } + _error => self.did_an_error_occur().map(Some), + } + } } impl Drop for Statement { From 1d46c44f5d43451d572e354f9da782e4c849d4cc Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Fri, 5 Jun 2020 23:39:19 +0200 Subject: [PATCH 22/25] Add back default features --- diesel/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diesel/Cargo.toml b/diesel/Cargo.toml index bd065defece3..90a48319d4cb 100644 --- a/diesel/Cargo.toml +++ b/diesel/Cargo.toml @@ -43,7 +43,7 @@ dotenv = "0.15" quickcheck = "0.9" [features] -default = [] +default = ["with-deprecated", "32-column-tables"] extras = ["chrono", "serde_json", "uuid", "deprecated-time", "network-address", "numeric", "r2d2"] unstable = ["diesel_derives/nightly"] large-tables = ["32-column-tables"] From 9ad955823076a7b6756ae4f95ec1f930da7e2566 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Mon, 8 Jun 2020 10:26:12 +0200 Subject: [PATCH 23/25] Address review comments --- diesel_tests/tests/types_roundtrip.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/diesel_tests/tests/types_roundtrip.rs b/diesel_tests/tests/types_roundtrip.rs index 0659f3c22415..ea2577e03ebd 100644 --- a/diesel_tests/tests/types_roundtrip.rs +++ b/diesel_tests/tests/types_roundtrip.rs @@ -363,11 +363,7 @@ impl quickcheck::Arbitrary for SerdeWrapper { fn arbitrary_serde(g: &mut G, depth: usize) -> serde_json::Value { use rand::distributions::Alphanumeric; use rand::Rng; - let variant = match g.gen_range(0, 6) { - 4 | 5 if depth > 0 => g.gen_range(0, 4), - i => i, - }; - match variant { + match g.gen_range(0, if depth > 0 { 4 } else { 6 }) { 0 => serde_json::Value::Null, 1 => serde_json::Value::Bool(g.gen()), 2 => { From f783850698f3f13332c01260afa0430da1183337 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 10 Jun 2020 11:26:07 +0200 Subject: [PATCH 24/25] Unify changelog entries regarding to `MysqlType` --- CHANGELOG.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f2b1b521d84..3c2aa98e958c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,9 +60,8 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/ [raw-value-2-0-0]: http://docs.diesel.rs/diesel/backend/type.RawValue.html * The type metadata for MySQL has been changed to include sign information. If - you are implementing `HasSqlType` for `Mysql` manually, or manipulating a - `Mysql::TypeMetadata`, you will need to take the new struct - `MysqlTypeMetadata` instead. + you are implementing `HasSqlType` for `Mysql` manually, you may need to adjust + your implementation to fully use the new unsigned variants in `MysqlType` * The minimal officially supported rustc version is now 1.40.0 @@ -94,7 +93,6 @@ for Rust libraries in [RFC #1105](https://github.com/rust-lang/rfcs/blob/master/ `#[non_exhaustive]`. If you matched on one of those variants explicitly you need to introduce a wild card match instead. -* The `TypeMetadata` type for `Mysql` changed to `MysqlType` ### Fixed From a1d71bb5b1bde13771cc7db508f0daaf9a8c4092 Mon Sep 17 00:00:00 2001 From: Georg Semmler Date: Wed, 10 Jun 2020 11:26:46 +0200 Subject: [PATCH 25/25] Remove a few debug annotations from prototypeing --- diesel/src/mysql/connection/bind.rs | 2 -- diesel/src/test_helpers.rs | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/diesel/src/mysql/connection/bind.rs b/diesel/src/mysql/connection/bind.rs index 9daff5b4fd5f..74c07ddf243f 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -7,7 +7,6 @@ use crate::mysql::types::MYSQL_TIME; use crate::mysql::{MysqlType, MysqlValue}; use crate::result::QueryResult; -#[derive(Debug)] pub struct Binds { data: Vec, } @@ -124,7 +123,6 @@ bitflags::bitflags! { } } -#[derive(Debug)] struct BindData { tpe: ffi::enum_field_types, bytes: Vec, diff --git a/diesel/src/test_helpers.rs b/diesel/src/test_helpers.rs index 19d9fee21b46..f306fd923ab3 100644 --- a/diesel/src/test_helpers.rs +++ b/diesel/src/test_helpers.rs @@ -37,9 +37,9 @@ cfg_if! { } pub fn database_url() -> String { - dbg!(dotenv::var("MYSQL_UNIT_TEST_DATABASE_URL") + dotenv::var("MYSQL_UNIT_TEST_DATABASE_URL") .or_else(|_| dotenv::var("DATABASE_URL")) - .expect("DATABASE_URL must be set in order to run tests")) + .expect("DATABASE_URL must be set in order to run tests") } } else { compile_error!(