diff --git a/sqlx-mysql/src/options/connect.rs b/sqlx-mysql/src/options/connect.rs index e6a8600272..0b52a761bb 100644 --- a/sqlx-mysql/src/options/connect.rs +++ b/sqlx-mysql/src/options/connect.rs @@ -46,22 +46,36 @@ impl ConnectOptions for MySqlConnectOptions { // https://mathiasbynens.be/notes/mysql-utf8mb4 - let mut options = String::new(); + let mut sql_mode = Vec::new(); if self.pipes_as_concat { - options.push_str(r#"SET sql_mode=(SELECT CONCAT(@@sql_mode, ',PIPES_AS_CONCAT,NO_ENGINE_SUBSTITUTION')),"#); - } else { - options.push_str( - r#"SET sql_mode=(SELECT CONCAT(@@sql_mode, ',NO_ENGINE_SUBSTITUTION')),"#, - ); + sql_mode.push(r#"PIPES_AS_CONCAT"#); + } + if self.no_engine_subsitution { + sql_mode.push(r#"NO_ENGINE_SUBSTITUTION"#); + } + + let mut options = Vec::new(); + if !sql_mode.is_empty() { + options.push(format!( + r#"sql_mode=(SELECT CONCAT(@@sql_mode, ',{}'))"#, + sql_mode.join(",") + )); + } + if let Some(timezone) = &self.timezone { + options.push(format!(r#"time_zone='{}'"#, timezone)); + } + if self.set_names { + options.push(format!( + r#"NAMES {} COLLATE {}"#, + conn.stream.charset.as_str(), + conn.stream.collation.as_str() + )) + } + + if !options.is_empty() { + conn.execute(&*format!(r#"SET {};"#, options.join(","))) + .await?; } - options.push_str(r#"time_zone='+00:00',"#); - options.push_str(&format!( - r#"NAMES {} COLLATE {};"#, - conn.stream.charset.as_str(), - conn.stream.collation.as_str() - )); - - conn.execute(&*options).await?; Ok(conn) }) diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs index a797576937..1aaab0ead7 100644 --- a/sqlx-mysql/src/options/mod.rs +++ b/sqlx-mysql/src/options/mod.rs @@ -77,6 +77,9 @@ pub struct MySqlConnectOptions { pub(crate) log_settings: LogSettings, pub(crate) pipes_as_concat: bool, pub(crate) enable_cleartext_plugin: bool, + pub(crate) no_engine_subsitution: bool, + pub(crate) timezone: Option, + pub(crate) set_names: bool, } impl Default for MySqlConnectOptions { @@ -105,6 +108,9 @@ impl MySqlConnectOptions { log_settings: Default::default(), pipes_as_concat: true, enable_cleartext_plugin: false, + no_engine_subsitution: true, + timezone: Some(String::from("+00:00")), + set_names: true, } } @@ -333,6 +339,65 @@ impl MySqlConnectOptions { self.enable_cleartext_plugin = flag_val; self } + + /// Flag that enables or disables the `NO_ENGINE_SUBSTITUTION` sql_mode setting after + /// connection. + /// + /// If not set, if the available storage engine specified by a `CREATE TABLE` is not available, + /// a warning is given and the default storage engine is used instead. + /// + /// By default, this is `true` (`NO_ENGINE_SUBSTITUTION` is passed, forbidding engine + /// substitution). + /// + /// https://mariadb.com/kb/en/sql-mode/ + pub fn no_engine_subsitution(mut self, flag_val: bool) -> Self { + self.no_engine_subsitution = flag_val; + self + } + + /// If `Some`, sets the `time_zone` option to the given string after connecting to the database. + /// + /// If `None`, no `time_zone` parameter is sent; the server timezone will be used instead. + /// + /// Defaults to `Some(String::from("+00:00"))` to ensure all timestamps are in UTC. + /// + /// ### Warning + /// Changing this setting from its default will apply an unexpected skew to any + /// `time::OffsetDateTime` or `chrono::DateTime` value, whether passed as a parameter or + /// decoded as a result. `TIMESTAMP` values are not encoded with their UTC offset in the MySQL + /// protocol, so encoding and decoding of these types assumes the server timezone is *always* + /// UTC. + /// + /// If you are changing this option, ensure your application only uses + /// `time::PrimitiveDateTime` or `chrono::NaiveDateTime` and that it does not assume these + /// timestamps can be placed on a real timeline without applying the proper offset. + pub fn timezone(mut self, value: impl Into>) -> Self { + self.timezone = value.into(); + self + } + + /// If enabled, `SET NAMES '{charset}' COLLATE '{collation}'` is passed with the values of + /// [`.charset()`] and [`.collation()`] after connecting to the database. + /// + /// This ensures the connection uses the specified character set and collation. + /// + /// Enabled by default. + /// + /// ### Warning + /// If this is disabled and the default charset is not binary-compatible with UTF-8, query + /// strings, column names and string values will likely not decode (or encode) correctly, which + /// may result in unexpected errors or garbage outputs at runtime. + /// + /// For proper functioning, you *must* ensure the server is using a binary-compatible charset, + /// such as ASCII or Latin-1 (ISO 8859-1), and that you do not pass any strings containing + /// codepoints not supported by said charset. + /// + /// Instead of disabling this, you may also consider setting [`.charset()`] to a charset that + /// is supported by your MySQL or MariaDB server version and compatible with UTF-8. + pub fn set_names(mut self, flag_val: bool) -> Self { + self.set_names = flag_val; + self + } } impl MySqlConnectOptions {