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/CHANGELOG.md b/CHANGELOG.md index 67beefeda50a..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,6 +93,7 @@ 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. + ### Fixed * Many types were incorrectly considered non-aggregate when they should not @@ -127,6 +127,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 diff --git a/diesel/Cargo.toml b/diesel/Cargo.toml index bc86a64065a2..90a48319d4cb 100644 --- a/diesel/Cargo.toml +++ b/diesel/Cargo.toml @@ -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 a8ceb3351e46..39482b871747 100644 --- a/diesel/src/mysql/backend.rs +++ b/diesel/src/mysql/backend.rs @@ -12,56 +12,54 @@ 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 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` + /// A 8 bit signed integer Tiny, - /// Sets `buffer_type` to `MYSQL_TYPE_SHORT` + /// A 8 bit unsigned integer + UnsignedTiny, + /// A 16 bit signed integer Short, - /// Sets `buffer_type` to `MYSQL_TYPE_LONG` + /// A 16 bit unsigned integer + UnsignedShort, + /// A 32 bit signed integer Long, - /// Sets `buffer_type` to `MYSQL_TYPE_LONGLONG` + /// A 32 bit unsigned integer + UnsignedLong, + /// A 64 bit signed integer LongLong, - /// Sets `buffer_type` to `MYSQL_TYPE_FLOAT` + /// A 64 bit unsigned integer + UnsignedLongLong, + /// 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_TIME` + /// A fixed point decimal value + Numeric, + /// 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, + /// A value containing a set of bit's + Bit, + /// A user defined set type + Set, + /// A user defined enum type + Enum, } impl Backend for Mysql { @@ -75,7 +73,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 abf9f12d732e..74c07ddf243f 100644 --- a/diesel/src/mysql/connection/bind.rs +++ b/diesel/src/mysql/connection/bind.rs @@ -1,10 +1,10 @@ -extern crate mysqlclient_sys as ffi; - +use mysqlclient_sys as ffi; use std::mem; use std::os::raw as libc; use super::stmt::Statement; -use crate::mysql::{MysqlType, MysqlTypeMetadata, MysqlValue}; +use crate::mysql::types::MYSQL_TIME; +use crate::mysql::{MysqlType, MysqlValue}; use crate::result::QueryResult; pub struct Binds { @@ -14,27 +14,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| { - ( - mysql_type_to_ffi_type(metadata.data_type), - metadata.is_unsigned as _, - ) - }) + .map(|metadata| metadata.into()) .map(BindData::for_output) .collect(); @@ -44,7 +37,16 @@ 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( + "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) .collect(); @@ -87,7 +89,37 @@ impl Binds { } pub fn field_data(&self, idx: usize) -> Option> { - self.data[idx].bytes().map(MysqlValue::new) + let data = &self.data[idx]; + self.data[idx].bytes().map(|bytes| { + let tpe = (data.tpe, data.flags).into(); + MysqlValue::new(bytes, tpe) + }) + } +} + +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 = 130_172; + const GET_FIXED_FIELDS_FLAG = (1<<18); + const FIELD_IN_PART_FUNC_FLAG = (1 << 19); } } @@ -95,40 +127,40 @@ 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, 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: mysql_type_to_ffi_type(tpe), - bytes: bytes, - length: length, - is_null: is_null, + tpe, + bytes, + length, + is_null, is_truncated: None, - is_unsigned, + flags, } } - 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(); 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, + flags, } } @@ -162,7 +194,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; @@ -213,22 +245,169 @@ 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, Flags) { + fn from(tpe: MysqlType) -> Self { + use self::ffi::enum_field_types::*; + let mut flags = Flags::empty(); + let tpe = 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, + 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) + } +} - 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<(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_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, + // 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 + // 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_STRING + if flags.contains(Flags::ENUM_FLAG) => + { + MysqlType::Enum + } + 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::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_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 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 + | MYSQL_TYPE_DATETIME2 + | 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 \ + the diesel github repo." + ), + } } } @@ -244,12 +423,1004 @@ 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, } } -fn is_field_unsigned(field: &ffi::MYSQL_FIELD) -> ffi::my_bool { - const UNSIGNED_FLAG: libc::c_uint = 32; - (field.flags & UNSIGNED_FLAG > 0) as _ +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + use super::MysqlValue; + use crate::deserialize::FromSql; + use crate::sql_types::*; + + fn to_value( + bind: &BindData, + ) -> Result> + where + T: FromSql + std::fmt::Debug, + { + let meta = (bind.tpe, bind.flags).into(); + dbg!(meta); + let value = MysqlValue::new(&bind.bytes, meta); + dbg!(T::from_sql(Some(value))) + } + + #[cfg(feature = "extras")] + #[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 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 + .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_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_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); + 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::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::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::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::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::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)); + // 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; + 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; + // 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)); + 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: 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.into()); + + let mut binds = Binds { data: vec![bind] }; + + stmt.execute_statement(&mut binds).unwrap(); + stmt.populate_row_buffers(&mut binds).unwrap(); + + binds.data.remove(0) + } + + fn input_bind( + query: &'static str, + conn: &MysqlConnection, + id: i32, + (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 field_bind = BindData { + tpe, + bytes: field, + length, + flags, + 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 binds = Binds { + data: vec![id_bind, field_bind], + }; + stmt.input_bind(binds).unwrap(); + stmt.did_an_error_occur().unwrap(); + 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, Flags::empty()), + ); + + 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, Flags::empty()), + ); + + 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(), + MysqlType::String, + // (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, Flags::empty()), + ); + + 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, Flags::empty()), + ); + + 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(), MysqlType::String), + ); + + let json_col_as_json = query_single_table( + "SELECT json_field FROM json_test", + &conn, + (ffi::enum_field_types::MYSQL_TYPE_JSON, Flags::empty()), + ); + + 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, Flags::empty()), + ); + + 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, MysqlType::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)); + 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" + ); + + 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, 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::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(), MysqlType::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_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)); + 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, 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); + 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, Flags::ENUM_FLAG), + ), + ); + + 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_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)); + 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, 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(), "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, MysqlType::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)); + 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"); + + 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 set_test", + &conn, + (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::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(), MysqlType::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_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)); + 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, Flags::SET_FLAG), + ); + + 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(), MysqlType::String), + ); + + 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_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)); + 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, Flags::SET_FLAG), + ); + + 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..2321efcacd07 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 super::{Binds, Statement, StatementMetadata}; +use crate::mysql::{Mysql, MysqlType, MysqlValue}; use crate::result::QueryResult; use crate::row::*; @@ -13,10 +13,10 @@ 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)?; + 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, @@ -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,24 +120,3 @@ impl<'a> NamedRow for NamedMysqlRow<'a> { self.binds.field_data(idx) } } - -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(()) -} - -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 6524a43c1a02..713a4dd74fc6 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,9 +39,13 @@ impl Statement { pub fn bind(&mut self, binds: Iter) -> QueryResult<()> where - Iter: IntoIterator>)>, + 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 @@ -72,10 +76,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) } @@ -114,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() }; @@ -124,7 +125,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(); @@ -152,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 { diff --git a/diesel/src/mysql/mod.rs b/diesel/src/mysql/mod.rs index 6fcf29a466aa..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; +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..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, ptr, slice}; +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,31 +12,21 @@ 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 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,23 +51,26 @@ 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()); - - >::to_sql(&mysql_time, out) + 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, + 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: false, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATETIME, + time_zone_displacement: 0, + }; + + >::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, @@ -98,20 +90,27 @@ 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; - - >::to_sql(&mysql_time, out) + fn to_sql(&self, out: &mut serialize::Output) -> serialize::Result { + 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, + day: 0, + month: 0, + second_part: 0, + year: 0, + neg: false, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_TIME, + time_zone_displacement: 0, + }; + + >::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, @@ -123,19 +122,26 @@ 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; - - >::to_sql(&mysql_time, out) + 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, + hour: 0, + minute: 0, + second: 0, + second_part: 0, + neg: false, + time_type: ffi::enum_mysql_timestamp_type::MYSQL_TIMESTAMP_DATE, + time_zone_displacement: 0, + }; + + >::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/json.rs b/diesel/src/mysql/types/json.rs new file mode 100644 index 000000000000..237027b13b91 --- /dev/null +++ b/diesel/src/mysql/types/json.rs @@ -0,0 +1,57 @@ +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::*; + +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) + } +} + +#[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::String), + )) + .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::String), + )); + 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 de593a142198..302bb8683ba7 100644 --- a/diesel/src/mysql/types/mod.rs +++ b/diesel/src/mysql/types/mod.rs @@ -2,18 +2,43 @@ #[cfg(feature = "chrono")] mod date_and_time; +#[cfg(feature = "serde_json")] +mod json; 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, 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::*; 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 { + 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) @@ -129,15 +154,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/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..6e6d9c0f5733 --- /dev/null +++ b/diesel/src/mysql/types/primitives.rs @@ -0,0 +1,128 @@ +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::*; + + 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) => decimal_to_integer(bytes), + } + } +} + +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) => decimal_to_integer(bytes), + } + } +} + +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) => decimal_to_integer(bytes), + } + } +} + +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..8b33bffc7543 100644 --- a/diesel/src/mysql/value.rs +++ b/diesel/src/mysql/value.rs @@ -1,25 +1,96 @@ -use super::Mysql; -use crate::backend::BinaryRawValue; +use super::MysqlType; +use crate::deserialize; +use crate::mysql::types::MYSQL_TIME; +use std::error::Error; /// Raw mysql value as received from the database #[derive(Copy, Clone, Debug)] pub struct MysqlValue<'a> { raw: &'a [u8], + tpe: MysqlType, } impl<'a> MysqlValue<'a> { - pub(crate) fn new(raw: &'a [u8]) -> Self { - Self { raw } + pub(crate) fn new(raw: &'a [u8], tpe: MysqlType) -> 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 + // 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 { + MysqlType::Time | MysqlType::Date | MysqlType::DateTime | MysqlType::Timestamp => { + let ptr = self.raw.as_ptr() as *const MYSQL_TIME; + let result = unsafe { ptr.read_unaligned() }; + if result.neg { + Err("Negative dates/times are not yet supported".into()) + } else { + Ok(result) + } + } + _ => 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 { + 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()?)), + + 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)] +#[non_exhaustive] +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/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 9cd8e981461d..bb5994e55332 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; @@ -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"` +/// +/// For postgresql you should normally 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 = "String"] +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..ac604b48260c 100644 --- a/diesel/src/type_impls/mod.rs +++ b/diesel/src/type_impls/mod.rs @@ -2,6 +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; 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 } } }) 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(); 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..ea2577e03ebd 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,60 @@ 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; + match g.gen_range(0, if depth > 0 { 4 } else { 6 }) { + 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}; 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() {