diff --git a/src/conn/mod.rs b/src/conn/mod.rs index 0df5a28c..3715a468 100644 --- a/src/conn/mod.rs +++ b/src/conn/mod.rs @@ -44,7 +44,7 @@ use crate::{ transaction::TxStatus, BinaryProtocol, Queryable, TextProtocol, }, - BinlogStream, InfileData, OptsBuilder, + BinlogStream, ChangeUserOpts, InfileData, OptsBuilder, }; use self::routines::Routine; @@ -102,6 +102,7 @@ struct ConnInner { pool: Option, pending_result: std::result::Result, ServerError>, tx_status: TxStatus, + reset_upon_returning_to_a_pool: bool, opts: Opts, last_io: Instant, wait_timeout: Duration, @@ -109,6 +110,7 @@ struct ConnInner { nonce: Vec, auth_plugin: AuthPlugin<'static>, auth_switched: bool, + server_key: Option>, /// Connection is already disconnected. pub(crate) disconnected: bool, /// One-time connection-level infile handler. @@ -126,6 +128,8 @@ impl fmt::Debug for ConnInner { .field("tx_status", &self.tx_status) .field("stream", &self.stream) .field("options", &self.opts) + .field("server_key", &self.server_key) + .field("auth_plugin", &self.auth_plugin) .finish() } } @@ -154,7 +158,9 @@ impl ConnInner { auth_plugin: AuthPlugin::MysqlNativePassword, auth_switched: false, disconnected: false, + server_key: None, infile_handler: None, + reset_upon_returning_to_a_pool: false, } } @@ -416,16 +422,33 @@ impl Conn { /// Returns true if io stream is encrypted. fn is_secure(&self) -> bool { #[cfg(any(feature = "native-tls-tls", feature = "rustls-tls"))] - if let Some(ref stream) = self.inner.stream { - stream.is_secure() - } else { - false + { + self.inner + .stream + .as_ref() + .map(|x| x.is_secure()) + .unwrap_or_default() } #[cfg(not(any(feature = "native-tls-tls", feature = "rustls-tls")))] false } + /// Returns true if io stream is socket. + fn is_socket(&self) -> bool { + #[cfg(unix)] + { + self.inner + .stream + .as_ref() + .map(|x| x.is_socket()) + .unwrap_or_default() + } + + #[cfg(not(unix))] + false + } + /// Hacky way to move connection through &mut. `self` becomes unusable. fn take(&mut self) -> Conn { mem::replace(self, Conn::empty(Default::default())) @@ -663,16 +686,21 @@ impl Conn { let mut pass = crate::BUFFER_POOL.get_with(pass.as_bytes()); pass.as_mut().push(0); - if self.is_secure() { + if self.is_secure() || self.is_socket() { self.write_packet(pass).await?; } else { - self.write_bytes(&[0x02][..]).await?; - let packet = self.read_packet().await?; - let key = &packet[1..]; + if self.inner.server_key.is_none() { + self.write_bytes(&[0x02][..]).await?; + let packet = self.read_packet().await?; + self.inner.server_key = Some(packet[1..].to_vec()); + } for (i, byte) in pass.as_mut().iter_mut().enumerate() { *byte ^= self.inner.nonce[i % self.inner.nonce.len()]; } - let encrypted_pass = crypto::encrypt(&*pass, key); + let encrypted_pass = crypto::encrypt( + &*pass, + self.inner.server_key.as_deref().expect("unreachable"), + ); self.write_bytes(&*encrypted_pass).await?; }; self.drop_packet().await?; @@ -958,12 +986,13 @@ impl Conn { self.inner.last_io.elapsed() } - /// Executes `COM_RESET_CONNECTION` on `self`. + /// Executes [`COM_RESET_CONNECTION`][1]. /// - /// If server version is older than 5.7.2, then it'll reconnect. - pub async fn reset(&mut self) -> Result<()> { - let pool = self.inner.pool.clone(); - + /// Returns `false` if command is not supported (requires MySql >5.7.2, MariaDb >10.2.3). + /// For older versions consider using [`Conn::change_user`]. + /// + /// [1]: https://dev.mysql.com/doc/c-api/5.7/en/mysql-reset-connection.html + pub async fn reset(&mut self) -> Result { let supports_com_reset_connection = if self.inner.is_mariadb { self.inner.version >= (10, 2, 4) } else { @@ -973,19 +1002,62 @@ impl Conn { if supports_com_reset_connection { self.routine(routines::ResetRoutine).await?; - } else { - let opts = self.inner.opts.clone(); - let old_conn = std::mem::replace(self, Conn::new(opts).await?); - // tidy up the old connection - old_conn.close_conn().await?; - }; + self.inner.stmt_cache.clear(); + self.inner.infile_handler = None; + } + + Ok(supports_com_reset_connection) + } + /// Executes [`COM_CHANGE_USER`][1]. + /// + /// This might be used as an older and slower alternative to `COM_RESET_CONNECTION` that + /// works on MySql prior to 5.7.3 (MariaDb prior ot 10.2.4). + /// + /// ## Note + /// + /// * Using non-default `opts` for a pooled connection is discouraging. + /// * Connection options will be permanently updated. + /// + /// [1]: https://dev.mysql.com/doc/c-api/5.7/en/mysql-change-user.html + pub async fn change_user(&mut self, opts: ChangeUserOpts) -> Result<()> { + // We'll kick this connection from a pool if opts are changed. + if opts != ChangeUserOpts::default() { + let mut opts_changed = false; + if let Some(user) = opts.user() { + opts_changed |= user != self.opts().user() + }; + if let Some(pass) = opts.pass() { + opts_changed |= pass != self.opts().pass() + }; + if let Some(db_name) = opts.db_name() { + opts_changed |= db_name != self.opts().db_name() + }; + if opts_changed { + if let Some(pool) = self.inner.pool.take() { + pool.cancel_connection(); + } + } + } + + let conn_opts = &mut self.inner.opts; + opts.update_opts(conn_opts); + self.routine(routines::ChangeUser).await?; self.inner.stmt_cache.clear(); self.inner.infile_handler = None; - self.inner.pool = pool; Ok(()) } + /// Resets the connection upon returning it to a pool. + /// + /// Will invoke `COM_CHANGE_USER` if `COM_RESET_CONNECTION` is not supported. + async fn reset_for_pool(mut self) -> Result { + if !self.reset().await? { + self.change_user(Default::default()).await?; + } + Ok(self) + } + /// Requires that `self.inner.tx_status != TxStatus::None` async fn rollback_transaction(&mut self) -> Result<()> { debug_assert_ne!(self.inner.tx_status, TxStatus::None); @@ -1094,13 +1166,14 @@ mod test { use bytes::Bytes; use futures_util::stream::{self, StreamExt}; use mysql_common::{binlog::events::EventData, constants::MAX_PAYLOAD_LEN}; + use rand::Fill; use tokio::time::timeout; use std::time::Duration; use crate::{ - from_row, params, prelude::*, test_misc::get_opts, BinlogDumpFlags, BinlogRequest, Conn, - Error, OptsBuilder, Pool, WhiteListFsHandler, + from_row, params, prelude::*, test_misc::get_opts, BinlogDumpFlags, BinlogRequest, + ChangeUserOpts, Conn, Error, OptsBuilder, Pool, Value, WhiteListFsHandler, }; async fn gen_dummy_data() -> super::Result<()> { @@ -1471,9 +1544,115 @@ mod test { #[tokio::test] async fn should_reset_the_connection() -> super::Result<()> { let mut conn = Conn::new(get_opts()).await?; - conn.exec_drop("SELECT ?", (1_u8,)).await?; - conn.reset().await?; - conn.exec_drop("SELECT ?", (1_u8,)).await?; + let max_execution_time = conn + .query_first::("SELECT @@max_execution_time") + .await? + .unwrap(); + + conn.exec_drop( + "SET SESSION max_execution_time = ?", + (max_execution_time + 1,), + ) + .await?; + + assert_eq!( + conn.query_first::("SELECT @@max_execution_time") + .await?, + Some(max_execution_time + 1) + ); + + if conn.reset().await? { + assert_eq!( + conn.query_first::("SELECT @@max_execution_time") + .await?, + Some(max_execution_time) + ); + } else { + assert_eq!( + conn.query_first::("SELECT @@max_execution_time") + .await?, + Some(max_execution_time + 1) + ); + } + + conn.disconnect().await?; + Ok(()) + } + + #[tokio::test] + async fn should_change_user() -> super::Result<()> { + let mut conn = Conn::new(get_opts()).await?; + let max_execution_time = conn + .query_first::("SELECT @@max_execution_time") + .await? + .unwrap(); + + conn.exec_drop( + "SET SESSION max_execution_time = ?", + (max_execution_time + 1,), + ) + .await?; + + assert_eq!( + conn.query_first::("SELECT @@max_execution_time") + .await?, + Some(max_execution_time + 1) + ); + + conn.change_user(Default::default()).await?; + assert_eq!( + conn.query_first::("SELECT @@max_execution_time") + .await?, + Some(max_execution_time) + ); + + let plugins: &[&str] = if !conn.inner.is_mariadb && conn.server_version() >= (5, 8, 0) { + &["mysql_native_password", "caching_sha2_password"] + } else { + &["mysql_native_password"] + }; + + for plugin in plugins { + let mut conn2 = Conn::new(get_opts()).await.unwrap(); + + let mut rng = rand::thread_rng(); + let mut pass = [0u8; 10]; + pass.try_fill(&mut rng).unwrap(); + let pass: String = IntoIterator::into_iter(pass) + .map(|x| ((x % (123 - 97)) + 97) as char) + .collect(); + conn.query_drop("DROP USER IF EXISTS __mysql_async_test_user") + .await + .unwrap(); + conn.query_drop(format!( + "CREATE USER '__mysql_async_test_user'@'%' IDENTIFIED WITH {} BY {}", + plugin, + Value::from(pass.clone()).as_sql(false) + )) + .await + .unwrap(); + conn.query_drop("FLUSH PRIVILEGES").await.unwrap(); + + conn2 + .change_user( + ChangeUserOpts::default() + .with_db_name(None) + .with_user(Some("__mysql_async_test_user".into())) + .with_pass(Some(pass)), + ) + .await + .unwrap(); + assert_eq!( + conn2 + .query_first::<(Option, String), _>("SELECT DATABASE(), USER();") + .await + .unwrap(), + Some((None, String::from("__mysql_async_test_user@localhost"))), + ); + + conn2.disconnect().await.unwrap(); + } + conn.disconnect().await?; Ok(()) } diff --git a/src/conn/pool/futures/get_conn.rs b/src/conn/pool/futures/get_conn.rs index 73e8a999..8b21e685 100644 --- a/src/conn/pool/futures/get_conn.rs +++ b/src/conn/pool/futures/get_conn.rs @@ -69,16 +69,18 @@ pub struct GetConn { pub(crate) queue_id: Option, pub(crate) pool: Option, pub(crate) inner: GetConnInner, + reset_upon_returning_to_a_pool: bool, #[cfg(feature = "tracing")] span: Arc, } impl GetConn { - pub(crate) fn new(pool: &Pool) -> GetConn { + pub(crate) fn new(pool: &Pool, reset_upon_returning_to_a_pool: bool) -> GetConn { GetConn { queue_id: None, pool: Some(pool.clone()), inner: GetConnInner::New, + reset_upon_returning_to_a_pool, #[cfg(feature = "tracing")] span: Arc::new(debug_span!("mysql_async::get_conn")), } @@ -141,6 +143,8 @@ impl Future for GetConn { return match result { Ok(mut c) => { c.inner.pool = Some(pool); + c.inner.reset_upon_returning_to_a_pool = + self.reset_upon_returning_to_a_pool; Poll::Ready(Ok(c)) } Err(e) => { @@ -152,12 +156,14 @@ impl Future for GetConn { GetConnInner::Checking(ref mut f) => { let result = ready!(Pin::new(f).poll(cx)); match result { - Ok(mut checked_conn) => { + Ok(mut c) => { self.inner = GetConnInner::Done; let pool = self.pool_take(); - checked_conn.inner.pool = Some(pool); - return Poll::Ready(Ok(checked_conn)); + c.inner.pool = Some(pool); + c.inner.reset_upon_returning_to_a_pool = + self.reset_upon_returning_to_a_pool; + return Poll::Ready(Ok(c)); } Err(_) => { // Idling connection is broken. We'll drop it and try again. diff --git a/src/conn/pool/mod.rs b/src/conn/pool/mod.rs index fd38affe..d00c8157 100644 --- a/src/conn/pool/mod.rs +++ b/src/conn/pool/mod.rs @@ -232,7 +232,7 @@ impl Pool { /// Async function that resolves to `Conn`. pub fn get_conn(&self) -> GetConn { - GetConn::new(self) + GetConn::new(self, true) } /// Starts a new transaction. @@ -296,7 +296,7 @@ impl Pool { /// /// Decreases the exist counter since a broken or dropped connection should not count towards /// the total. - fn cancel_connection(&self) { + pub(super) fn cancel_connection(&self) { let mut exchange = self.inner.exchange.lock().unwrap(); exchange.exist -= 1; // we just enabled the creation of a new connection! @@ -573,20 +573,29 @@ mod test { let pool = Pool::new(opts); - "CREATE TEMPORARY TABLE tmp(id int)".ignore(&pool).await?; + "CREATE TABLE IF NOT EXISTS mysql.tmp(id int)" + .ignore(&pool) + .await?; + "DELETE FROM mysql.tmp".ignore(&pool).await?; let mut tx = pool.start_transaction(TxOpts::default()).await?; - tx.exec_batch("INSERT INTO tmp (id) VALUES (?)", vec![(1_u8,), (2_u8,)]) - .await?; - tx.exec_drop("SELECT * FROM tmp", ()).await?; + tx.exec_batch( + "INSERT INTO mysql.tmp (id) VALUES (?)", + vec![(1_u8,), (2_u8,)], + ) + .await?; + tx.exec_drop("SELECT * FROM mysql.tmp", ()).await?; drop(tx); let row_opt = pool .get_conn() .await? - .query_first("SELECT COUNT(*) FROM tmp") + .query_first("SELECT COUNT(*) FROM mysql.tmp") .await?; assert_eq!(row_opt, Some((0u8,))); - pool.get_conn().await?.query_drop("DROP TABLE tmp").await?; + pool.get_conn() + .await? + .query_drop("DROP TABLE mysql.tmp") + .await?; pool.disconnect().await?; Ok(()) } diff --git a/src/conn/pool/recycler.rs b/src/conn/pool/recycler.rs index 2a704dbc..5a705868 100644 --- a/src/conn/pool/recycler.rs +++ b/src/conn/pool/recycler.rs @@ -28,6 +28,7 @@ pub(crate) struct Recycler { discard: FuturesUnordered>, discarded: usize, cleaning: FuturesUnordered>, + reset: FuturesUnordered>, // Option so that we have a way to send a "I didn't make a Conn after all" signal dropped: mpsc::UnboundedReceiver>, @@ -47,6 +48,7 @@ impl Recycler { discard: FuturesUnordered::new(), discarded: 0, cleaning: FuturesUnordered::new(), + reset: FuturesUnordered::new(), dropped, pool_opts, eof: false, @@ -60,6 +62,21 @@ impl Future for Recycler { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut close = self.inner.close.load(Ordering::Acquire); + macro_rules! conn_return { + ($self:ident, $conn:ident) => {{ + let mut exchange = $self.inner.exchange.lock().unwrap(); + if exchange.available.len() >= $self.pool_opts.active_bound() { + drop(exchange); + $self.discard.push($conn.close_conn().boxed()); + } else { + exchange.available.push_back($conn.into()); + if let Some(w) = exchange.waiting.pop() { + w.wake(); + } + } + }}; + } + macro_rules! conn_decision { ($self:ident, $conn:ident) => { if $conn.inner.stream.is_none() || $conn.inner.disconnected { @@ -69,17 +86,10 @@ impl Future for Recycler { $self.cleaning.push($conn.cleanup_for_pool().boxed()); } else if $conn.expired() || close { $self.discard.push($conn.close_conn().boxed()); + } else if $conn.inner.reset_upon_returning_to_a_pool { + $self.reset.push($conn.reset_for_pool().boxed()); } else { - let mut exchange = $self.inner.exchange.lock().unwrap(); - if exchange.available.len() >= $self.pool_opts.active_bound() { - drop(exchange); - $self.discard.push($conn.close_conn().boxed()); - } else { - exchange.available.push_back($conn.into()); - if let Some(w) = exchange.waiting.pop() { - w.wake(); - } - } + conn_return!($self, $conn); } }; } @@ -138,6 +148,21 @@ impl Future for Recycler { } } + // let's iterate through connections being successfully reset + loop { + match Pin::new(&mut self.reset).poll_next(cx) { + Poll::Pending | Poll::Ready(None) => break, + Poll::Ready(Some(Ok(conn))) => conn_return!(self, conn), + Poll::Ready(Some(Err(e))) => { + // an error during reset. + // replace with a new connection + self.discarded += 1; + // NOTE: we're discarding the error here + let _ = e; + } + } + } + // are there any torn-down connections for us to deal with? loop { match Pin::new(&mut self.discard).poll_next(cx) { diff --git a/src/conn/routines/change_user.rs b/src/conn/routines/change_user.rs new file mode 100644 index 00000000..2a110fd8 --- /dev/null +++ b/src/conn/routines/change_user.rs @@ -0,0 +1,58 @@ +use futures_core::future::BoxFuture; +use futures_util::FutureExt; +use mysql_common::{ + constants::{UTF8MB4_GENERAL_CI, UTF8_GENERAL_CI}, + packets::{ComChangeUser, ComChangeUserMoreData}, +}; +#[cfg(feature = "tracing")] +use tracing::debug_span; + +use crate::Conn; + +use super::Routine; + +/// A routine that performs `COM_RESET_CONNECTION`. +#[derive(Debug, Copy, Clone)] +pub struct ChangeUser; + +impl Routine<()> for ChangeUser { + fn call<'a>(&'a mut self, conn: &'a mut Conn) -> BoxFuture<'a, crate::Result<()>> { + #[cfg(feature = "tracing")] + let span = debug_span!( + "mysql_async::change_user", + mysql_async.connection.id = conn.id() + ); + + let com_change_user = ComChangeUser::new() + .with_user(conn.opts().user().map(|x| x.as_bytes())) + .with_database(conn.opts().db_name().map(|x| x.as_bytes())) + .with_auth_plugin_data( + conn.inner + .auth_plugin + .gen_data(conn.opts().pass(), &conn.inner.nonce) + .as_deref(), + ) + .with_more_data(Some( + ComChangeUserMoreData::new(if conn.inner.version >= (5, 5, 3) { + UTF8MB4_GENERAL_CI + } else { + UTF8_GENERAL_CI + }) + .with_auth_plugin(Some(conn.inner.auth_plugin.clone())) + .with_connect_attributes(None), + )) + .into_owned(); + + let fut = async move { + conn.write_command(&com_change_user).await?; + conn.inner.auth_switched = false; + conn.continue_auth().await?; + Ok(()) + }; + + #[cfg(feature = "tracing")] + let fut = instrument_result!(fut, span); + + fut.boxed() + } +} diff --git a/src/conn/routines/mod.rs b/src/conn/routines/mod.rs index 928bd771..80ecf1a5 100644 --- a/src/conn/routines/mod.rs +++ b/src/conn/routines/mod.rs @@ -2,8 +2,9 @@ use futures_core::future::BoxFuture; use crate::Conn; -pub use self::{exec::*, next_set::*, ping::*, prepare::*, query::*, reset::*}; +pub use self::{change_user::*, exec::*, next_set::*, ping::*, prepare::*, query::*, reset::*}; +mod change_user; mod exec; mod next_set; mod ping; diff --git a/src/io/mod.rs b/src/io/mod.rs index 6498b33e..d46b2dc3 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -153,7 +153,7 @@ impl Future for CheckTcpStream<'_> { } impl Endpoint { - #[cfg(all(any(feature = "native-tls-tls", feature = "rustls-tls"), unix))] + #[cfg(unix)] fn is_socket(&self) -> bool { match self { Self::Socket(_) => true, @@ -419,6 +419,11 @@ impl Stream { self.codec.as_ref().unwrap().get_ref().is_secure() } + #[cfg(unix)] + pub(crate) fn is_socket(&self) -> bool { + self.codec.as_ref().unwrap().get_ref().is_socket() + } + pub(crate) fn reset_seq_id(&mut self) { if let Some(codec) = self.codec.as_mut() { codec.codec_mut().reset_seq_id(); diff --git a/src/lib.rs b/src/lib.rs index 2f638e78..5d6d78b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,7 @@ //! * [`Pool`] is a smart pointer – each clone will point to the same pool instance. //! * [`Pool`] is `Send + Sync + 'static` – feel free to pass it around. //! * use [`Pool::disconnect`] to gracefuly close the pool. -//! * [`Pool::new`] is lazy and won't assert server availability. +//! * ⚠️ [`Pool::new`] is lazy and won't assert server availability. //! //! # Transaction //! @@ -470,8 +470,9 @@ pub use self::opts::ClientIdentity; #[doc(inline)] pub use self::opts::{ - Opts, OptsBuilder, PoolConstraints, PoolOpts, SslOpts, DEFAULT_INACTIVE_CONNECTION_TTL, - DEFAULT_POOL_CONSTRAINTS, DEFAULT_STMT_CACHE_SIZE, DEFAULT_TTL_CHECK_INTERVAL, + ChangeUserOpts, Opts, OptsBuilder, PoolConstraints, PoolOpts, SslOpts, + DEFAULT_INACTIVE_CONNECTION_TTL, DEFAULT_POOL_CONSTRAINTS, DEFAULT_STMT_CACHE_SIZE, + DEFAULT_TTL_CHECK_INTERVAL, }; #[doc(inline)] diff --git a/src/opts/mod.rs b/src/opts/mod.rs index 18953ae8..3d7a2800 100644 --- a/src/opts/mod.rs +++ b/src/opts/mod.rs @@ -21,6 +21,7 @@ use url::{Host, Url}; use std::{ borrow::Cow, convert::TryFrom, + fmt, net::{Ipv4Addr, Ipv6Addr}, path::Path, str::FromStr, @@ -1132,6 +1133,118 @@ impl From for Opts { } } +/// [`COM_CHANGE_USER`][1] options. +/// +/// Connection [`Opts`] are going to be updated accordingly upon `COM_CHANGE_USER`. +/// +/// [`Opts`] won't be updated by default, because default `ChangeUserOpts` will reuse +/// connection's `user`, `pass` and `db_name`. +/// +/// [1]: https://dev.mysql.com/doc/c-api/5.7/en/mysql-change-user.html +#[derive(Clone, Eq, PartialEq)] +pub struct ChangeUserOpts { + user: Option>, + pass: Option>, + db_name: Option>, +} + +impl ChangeUserOpts { + pub(crate) fn update_opts(self, opts: &mut Opts) { + if self.user.is_none() && self.pass.is_none() && self.db_name.is_none() { + return; + } + + let mut builder = OptsBuilder::from_opts(opts.clone()); + + if let Some(user) = self.user { + builder = builder.user(user); + } + + if let Some(pass) = self.pass { + builder = builder.pass(pass); + } + + if let Some(db_name) = self.db_name { + builder = builder.db_name(db_name); + } + + *opts = Opts::from(builder); + } + + /// Creates change user options that'll reuse connection options. + pub fn new() -> Self { + Self { + user: None, + pass: None, + db_name: None, + } + } + + /// Set [`Opts::user`] to the given value. + pub fn with_user(mut self, user: Option) -> Self { + self.user = Some(user); + self + } + + /// Set [`Opts::pass`] to the given value. + pub fn with_pass(mut self, pass: Option) -> Self { + self.pass = Some(pass); + self + } + + /// Set [`Opts::db_name`] to the given value. + pub fn with_db_name(mut self, db_name: Option) -> Self { + self.db_name = Some(db_name); + self + } + + /// Returns user. + /// + /// * if `None` then `self` does not meant to change user + /// * if `Some(None)` then `self` will clear user + /// * if `Some(Some(_))` then `self` will change user + pub fn user(&self) -> Option> { + self.user.as_ref().map(|x| x.as_deref()) + } + + /// Returns password. + /// + /// * if `None` then `self` does not meant to change password + /// * if `Some(None)` then `self` will clear password + /// * if `Some(Some(_))` then `self` will change password + pub fn pass(&self) -> Option> { + self.pass.as_ref().map(|x| x.as_deref()) + } + + /// Returns database name. + /// + /// * if `None` then `self` does not meant to change database name + /// * if `Some(None)` then `self` will clear database name + /// * if `Some(Some(_))` then `self` will change database name + pub fn db_name(&self) -> Option> { + self.db_name.as_ref().map(|x| x.as_deref()) + } +} + +impl Default for ChangeUserOpts { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for ChangeUserOpts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChangeUserOpts") + .field("user", &self.user) + .field( + "pass", + &self.pass.as_ref().map(|x| x.as_ref().map(|_| "...")), + ) + .field("db_name", &self.db_name) + .finish() + } +} + fn get_opts_user_from_url(url: &Url) -> Option { let user = url.username(); if !user.is_empty() {