mysql/conn/opts/
mod.rs

1// Copyright (c) 2020 rust-mysql-simple contributors
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use percent_encoding::percent_decode;
10use url::Url;
11
12use std::{
13    borrow::Cow, collections::HashMap, fmt, hash::Hash, net::SocketAddr, path::Path, time::Duration,
14};
15
16use crate::{
17    consts::CapabilityFlags, Compression, LocalInfileHandler, PoolConstraints, PoolOpts, UrlError,
18};
19
20/// Default value for client side per-connection statement cache.
21pub const DEFAULT_STMT_CACHE_SIZE: usize = 32;
22
23mod native_tls_opts;
24mod rustls_opts;
25
26pub mod pool_opts;
27
28#[cfg(feature = "native-tls")]
29pub use native_tls_opts::ClientIdentity;
30
31#[cfg(feature = "rustls")]
32pub use rustls_opts::ClientIdentity;
33
34/// Ssl Options.
35#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
36pub struct SslOpts {
37    #[cfg(any(feature = "native-tls", feature = "rustls"))]
38    client_identity: Option<ClientIdentity>,
39    root_cert_path: Option<Cow<'static, Path>>,
40    skip_domain_validation: bool,
41    accept_invalid_certs: bool,
42}
43
44impl SslOpts {
45    /// Sets the client identity.
46    #[cfg(any(feature = "native-tls", feature = "rustls"))]
47    #[cfg_attr(
48        docsrs,
49        doc(cfg(any(
50            feature = "native-tls",
51            feature = "rustls-tls",
52            feature = "rustls-tls-ring"
53        )))
54    )]
55    pub fn with_client_identity(mut self, identity: Option<ClientIdentity>) -> Self {
56        self.client_identity = identity;
57        self
58    }
59
60    /// Sets path to a certificate of the root that connector will trust.
61    ///
62    /// Supported certificate formats are .der and .pem.
63    /// Multiple certs are allowed in .pem files.
64    pub fn with_root_cert_path<T: Into<Cow<'static, Path>>>(
65        mut self,
66        root_cert_path: Option<T>,
67    ) -> Self {
68        self.root_cert_path = root_cert_path.map(Into::into);
69        self
70    }
71
72    /// The way to not validate the server's domain
73    /// name against its certificate (defaults to `false`).
74    pub fn with_danger_skip_domain_validation(mut self, value: bool) -> Self {
75        self.skip_domain_validation = value;
76        self
77    }
78
79    /// If `true` then client will accept invalid certificate (expired, not trusted, ..)
80    /// (defaults to `false`).
81    pub fn with_danger_accept_invalid_certs(mut self, value: bool) -> Self {
82        self.accept_invalid_certs = value;
83        self
84    }
85
86    #[cfg(any(feature = "native-tls", feature = "rustls"))]
87    #[cfg_attr(
88        docsrs,
89        doc(cfg(any(
90            feature = "native-tls",
91            feature = "rustls-tls",
92            feature = "rustls-tls-ring"
93        )))
94    )]
95    pub fn client_identity(&self) -> Option<&ClientIdentity> {
96        self.client_identity.as_ref()
97    }
98
99    pub fn root_cert_path(&self) -> Option<&Path> {
100        self.root_cert_path.as_ref().map(AsRef::as_ref)
101    }
102
103    pub fn skip_domain_validation(&self) -> bool {
104        self.skip_domain_validation
105    }
106
107    pub fn accept_invalid_certs(&self) -> bool {
108        self.accept_invalid_certs
109    }
110}
111
112/// Options structure is quite large so we'll store it separately.
113#[derive(Debug, Clone, Eq, PartialEq)]
114pub(crate) struct InnerOpts {
115    /// Address of mysql server (defaults to `127.0.0.1`). Host names should also work.
116    ip_or_hostname: url::Host,
117    /// TCP port of mysql server (defaults to `3306`).
118    tcp_port: u16,
119    /// Path to unix socket on unix or pipe name on windows (defaults to `None`).
120    ///
121    /// Can be defined using `socket` connection url parameter.
122    socket: Option<String>,
123    /// User (defaults to `None`).
124    user: Option<String>,
125    /// Password (defaults to `None`).
126    pass: Option<String>,
127    /// Database name (defaults to `None`).
128    db_name: Option<String>,
129
130    /// The timeout for each attempt to read from the server.
131    read_timeout: Option<Duration>,
132
133    /// The timeout for each attempt to write to the server.
134    write_timeout: Option<Duration>,
135
136    /// Prefer socket connection (defaults to `true`).
137    ///
138    /// Will reconnect via socket (or named pipe on windows) after TCP
139    /// connection to `127.0.0.1` if `true`.
140    ///
141    /// Will fall back to TCP on error. Use `socket` option to enforce socket connection.
142    ///
143    /// Can be defined using `prefer_socket` connection url parameter.
144    prefer_socket: bool,
145
146    /// Whether to enable `TCP_NODELAY` (defaults to `true`).
147    ///
148    /// This option disables Nagle's algorithm, which can cause unusually high latency (~40ms) at
149    /// some cost to maximum throughput. See #132.
150    tcp_nodelay: bool,
151
152    /// TCP keep alive time for mysql connection.
153    ///
154    /// Can be defined using `tcp_keepalive_time_ms` connection url parameter.
155    tcp_keepalive_time: Option<u32>,
156
157    /// TCP keep alive interval between subsequent probe for mysql connection.
158    ///
159    /// Can be defined using `tcp_keepalive_probe_interval_secs` connection url parameter.
160    #[cfg(any(target_os = "linux", target_os = "macos"))]
161    tcp_keepalive_probe_interval_secs: Option<u32>,
162
163    /// TCP keep alive probe count for mysql connection.
164    ///
165    /// Can be defined using `tcp_keepalive_probe_count` connection url parameter.
166    #[cfg(any(target_os = "linux", target_os = "macos"))]
167    tcp_keepalive_probe_count: Option<u32>,
168
169    /// TCP_USER_TIMEOUT time for mysql connection.
170    ///
171    /// Can be defined using `tcp_user_timeout_ms` connection url parameter.
172    #[cfg(target_os = "linux")]
173    tcp_user_timeout: Option<u32>,
174
175    /// Commands to execute on each new database connection.
176    init: Vec<String>,
177
178    /// Driver will require SSL connection if this option isn't `None` (default to `None`).
179    ssl_opts: Option<SslOpts>,
180
181    /// Connection pool options (defaults to [`PoolOpts::default`]).
182    pool_opts: PoolOpts,
183
184    /// Callback to handle requests for local files.
185    ///
186    /// These are caused by using `LOAD DATA LOCAL INFILE` queries.
187    /// The callback is passed the filename, and a `Write`able object
188    /// to receive the contents of that file.
189    ///
190    /// If unset, the default callback will read files relative to
191    /// the current directory.
192    local_infile_handler: Option<LocalInfileHandler>,
193
194    /// Tcp connect timeout (defaults to `None`).
195    ///
196    /// Can be defined using `tcp_connect_timeout_ms` connection url parameter.
197    tcp_connect_timeout: Option<Duration>,
198
199    /// Bind address for a client (defaults to `None`).
200    ///
201    /// Use carefully. Will probably make pool unusable because of *address already in use*
202    /// errors.
203    bind_address: Option<SocketAddr>,
204
205    /// Number of prepared statements cached on the client side (per connection).
206    /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`].
207    ///
208    /// Can be defined using `stmt_cache_size` connection url parameter.
209    stmt_cache_size: usize,
210
211    /// If not `None`, then client will ask for compression if server supports it
212    /// (defaults to `None`).
213    ///
214    /// Can be defined using `compress` connection url parameter with values `true`, `fast`, `best`,
215    /// `0`, `1`, ..., `9`.
216    ///
217    /// Note that compression level defined here will affect only outgoing packets.
218    compress: Option<crate::Compression>,
219
220    /// Additional client capabilities to set (defaults to empty).
221    ///
222    /// This value will be OR'ed with other client capabilities during connection initialization.
223    ///
224    /// ### Note
225    ///
226    /// It is a good way to set something like `CLIENT_FOUND_ROWS` but you should note that it
227    /// won't let you to interfere with capabilities managed by other options (like
228    /// `CLIENT_SSL` or `CLIENT_COMPRESS`). Also note that some capabilities are reserved,
229    /// pointless or may broke the connection, so this option should be used with caution.
230    additional_capabilities: CapabilityFlags,
231
232    /// Connect attributes
233    connect_attrs: Option<HashMap<String, String>>,
234
235    /// Disables `mysql_old_password` plugin (defaults to `true`).
236    ///
237    /// Available via `secure_auth` connection url parameter.
238    secure_auth: bool,
239
240    /// Enables Client-Side Cleartext Pluggable Authentication (defaults to `false`).
241    ///
242    /// Enables client to send passwords to the server as cleartext, without hashing or encryption
243    /// (consult MySql documentation for more info).
244    ///
245    /// # Security Notes
246    ///
247    /// Sending passwords as cleartext may be a security problem in some configurations. Please
248    /// consider using TLS or encrypted tunnels for server connection.
249    enable_cleartext_plugin: bool,
250
251    /// Client side `max_allowed_packet` value (defaults to `None`).
252    ///
253    /// By default `Conn` will query this value from the server. One can avoid this step
254    /// by explicitly specifying it.
255    max_allowed_packet: Option<usize>,
256
257    /// For tests only
258    #[cfg(test)]
259    pub injected_socket: Option<String>,
260}
261
262impl Default for InnerOpts {
263    fn default() -> Self {
264        InnerOpts {
265            ip_or_hostname: url::Host::Domain(String::from("localhost")),
266            tcp_port: 3306,
267            socket: None,
268            max_allowed_packet: None,
269            user: None,
270            pass: None,
271            db_name: None,
272            read_timeout: None,
273            write_timeout: None,
274            prefer_socket: true,
275            init: vec![],
276            ssl_opts: None,
277            pool_opts: PoolOpts::default(),
278            tcp_keepalive_time: None,
279            #[cfg(any(target_os = "linux", target_os = "macos",))]
280            tcp_keepalive_probe_interval_secs: None,
281            #[cfg(any(target_os = "linux", target_os = "macos",))]
282            tcp_keepalive_probe_count: None,
283            #[cfg(target_os = "linux")]
284            tcp_user_timeout: None,
285            tcp_nodelay: true,
286            local_infile_handler: None,
287            tcp_connect_timeout: None,
288            bind_address: None,
289            stmt_cache_size: DEFAULT_STMT_CACHE_SIZE,
290            compress: None,
291            additional_capabilities: CapabilityFlags::empty(),
292            connect_attrs: Some(HashMap::new()),
293            secure_auth: true,
294            enable_cleartext_plugin: false,
295            #[cfg(test)]
296            injected_socket: None,
297        }
298    }
299}
300
301impl TryFrom<&'_ str> for Opts {
302    type Error = UrlError;
303
304    fn try_from(url: &'_ str) -> Result<Self, Self::Error> {
305        Opts::from_url(url)
306    }
307}
308
309/// Mysql connection options.
310///
311/// Build one with [`OptsBuilder`](struct.OptsBuilder.html).
312#[derive(Clone, Eq, PartialEq, Debug, Default)]
313pub struct Opts(pub(crate) Box<InnerOpts>);
314
315impl Opts {
316    #[doc(hidden)]
317    pub fn addr_is_loopback(&self) -> bool {
318        match self.0.ip_or_hostname {
319            url::Host::Domain(ref name) => name == "localhost",
320            url::Host::Ipv4(ref addr) => addr.is_loopback(),
321            url::Host::Ipv6(ref addr) => addr.is_loopback(),
322        }
323    }
324
325    pub fn from_url(url: &str) -> Result<Opts, UrlError> {
326        from_url(url)
327    }
328
329    pub(crate) fn get_host(&self) -> url::Host {
330        self.0.ip_or_hostname.clone()
331    }
332
333    /// Address of mysql server (defaults to `127.0.0.1`). Host names should also work.
334    pub fn get_ip_or_hostname(&self) -> Cow<str> {
335        self.0.ip_or_hostname.to_string().into()
336    }
337    /// TCP port of mysql server (defaults to `3306`).
338    pub fn get_tcp_port(&self) -> u16 {
339        self.0.tcp_port
340    }
341    /// Socket path on unix or pipe name on windows (defaults to `None`).
342    pub fn get_socket(&self) -> Option<&str> {
343        self.0.socket.as_deref()
344    }
345    /// Client side `max_allowed_packet` value (defaults to `None`).
346    ///
347    /// By default `Conn` will query this value from the server. One can avoid this step
348    /// by explicitly specifying it. Server side default is 4MB.
349    ///
350    /// Available in connection URL via `max_allowed_packet` parameter.
351    pub fn get_max_allowed_packet(&self) -> Option<usize> {
352        self.0.max_allowed_packet
353    }
354    /// User (defaults to `None`).
355    pub fn get_user(&self) -> Option<&str> {
356        self.0.user.as_deref()
357    }
358    /// Password (defaults to `None`).
359    pub fn get_pass(&self) -> Option<&str> {
360        self.0.pass.as_deref()
361    }
362    /// Database name (defaults to `None`).
363    pub fn get_db_name(&self) -> Option<&str> {
364        self.0.db_name.as_deref()
365    }
366
367    /// The timeout for each attempt to write to the server.
368    pub fn get_read_timeout(&self) -> Option<&Duration> {
369        self.0.read_timeout.as_ref()
370    }
371
372    /// The timeout for each attempt to write to the server.
373    pub fn get_write_timeout(&self) -> Option<&Duration> {
374        self.0.write_timeout.as_ref()
375    }
376
377    /// Prefer socket connection (defaults to `true`).
378    ///
379    /// Will reconnect via socket (or named pipe on windows) after TCP connection
380    /// to `127.0.0.1` if `true`.
381    ///
382    /// Will fall back to TCP on error. Use `socket` option to enforce socket connection.
383    pub fn get_prefer_socket(&self) -> bool {
384        self.0.prefer_socket
385    }
386    // XXX: Wait for keepalive_timeout stabilization
387    /// Commands to execute on each new database connection.
388    pub fn get_init(&self) -> Vec<String> {
389        self.0.init.clone()
390    }
391
392    /// Driver will require SSL connection if this option isn't `None` (default to `None`).
393    pub fn get_ssl_opts(&self) -> Option<&SslOpts> {
394        self.0.ssl_opts.as_ref()
395    }
396
397    /// Connection pool options (defaults to [`Default::default`]).
398    pub fn get_pool_opts(&self) -> &PoolOpts {
399        &self.0.pool_opts
400    }
401
402    /// Whether `TCP_NODELAY` will be set for mysql connection.
403    pub fn get_tcp_nodelay(&self) -> bool {
404        self.0.tcp_nodelay
405    }
406
407    /// TCP keep alive time for mysql connection.
408    pub fn get_tcp_keepalive_time_ms(&self) -> Option<u32> {
409        self.0.tcp_keepalive_time
410    }
411
412    /// TCP keep alive interval between subsequent probes for mysql connection.
413    #[cfg(any(target_os = "linux", target_os = "macos",))]
414    pub fn get_tcp_keepalive_probe_interval_secs(&self) -> Option<u32> {
415        self.0.tcp_keepalive_probe_interval_secs
416    }
417
418    /// TCP keep alive probe count for mysql connection.
419    #[cfg(any(target_os = "linux", target_os = "macos",))]
420    pub fn get_tcp_keepalive_probe_count(&self) -> Option<u32> {
421        self.0.tcp_keepalive_probe_count
422    }
423
424    /// TCP_USER_TIMEOUT time for mysql connection.
425    #[cfg(target_os = "linux")]
426    pub fn get_tcp_user_timeout_ms(&self) -> Option<u32> {
427        self.0.tcp_user_timeout
428    }
429
430    /// Callback to handle requests for local files.
431    pub fn get_local_infile_handler(&self) -> Option<&LocalInfileHandler> {
432        self.0.local_infile_handler.as_ref()
433    }
434
435    /// Tcp connect timeout (defaults to `None`).
436    pub fn get_tcp_connect_timeout(&self) -> Option<Duration> {
437        self.0.tcp_connect_timeout
438    }
439
440    /// Bind address for a client (defaults to `None`).
441    ///
442    /// Use carefully. Will probably make pool unusable because of *address already in use*
443    /// errors.
444    pub fn bind_address(&self) -> Option<&SocketAddr> {
445        self.0.bind_address.as_ref()
446    }
447
448    /// Number of prepared statements cached on the client side (per connection).
449    /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`].
450    ///
451    /// Can be defined using `stmt_cache_size` connection url parameter.
452    pub fn get_stmt_cache_size(&self) -> usize {
453        self.0.stmt_cache_size
454    }
455
456    /// If not `None`, then client will ask for compression if server supports it
457    /// (defaults to `None`).
458    ///
459    /// Can be defined using `compress` connection url parameter with values:
460    /// * `true` - library defined default compression level;
461    /// * `fast` - library defined fast compression level;
462    /// * `best` - library defined best compression level;
463    /// * `0`, `1`, ..., `9` - explicitly defined compression level where `0` stands for
464    ///   "no compression";
465    ///
466    /// Note that compression level defined here will affect only outgoing packets.
467    pub fn get_compress(&self) -> Option<crate::Compression> {
468        self.0.compress
469    }
470
471    /// Additional client capabilities to set (defaults to empty).
472    ///
473    /// This value will be OR'ed with other client capabilities during connection initialization.
474    ///
475    /// ### Note
476    ///
477    /// It is a good way to set something like `CLIENT_FOUND_ROWS` but you should note that it
478    /// won't let you to interfere with capabilities managed by other options (like
479    /// `CLIENT_SSL` or `CLIENT_COMPRESS`). Also note that some capabilities are reserved,
480    /// pointless or may broke the connection, so this option should be used with caution.
481    pub fn get_additional_capabilities(&self) -> CapabilityFlags {
482        self.0.additional_capabilities
483    }
484
485    /// Connect attributes (the default connect attributes are sent by default).
486    ///
487    /// This value is sent to the server as custom name-value attributes.
488    /// You can see them from performance_schema tables: [`session_account_connect_attrs`
489    /// and `session_connect_attrs`][attr_tables] when all of the following conditions
490    /// are met.
491    ///
492    /// * The server is MySQL 5.6 or later, or MariaDB 10.0 or later.
493    /// * [`performance_schema`] is on.
494    /// * [`performance_schema_session_connect_attrs_size`] is -1 or big enough
495    ///   to store specified attributes.
496    ///
497    /// ### Note
498    ///
499    /// - set `connect_attrs` to `None` to completely remove connect attributes
500    /// - set `connect_attrs` to an empty map to send only the default attributes
501    ///
502    /// #### Warning
503    ///
504    /// > There is a bug in MySql 5.6 that kills COM_CHANGE_USER in the presence of connection
505    /// > attributes so it's better to stick to `None` for mysql < 5.7.
506    ///
507    /// Attribute names that begin with an underscore (`_`) are not set by
508    /// application programs because they are reserved for internal use.
509    ///
510    /// The following default attributes are sent in addition to ones set by programs.
511    ///
512    /// name            | value
513    /// ----------------|--------------------------
514    /// _client_name    | The client library name (`rust-mysql-simple`)
515    /// _client_version | The client library version
516    /// _os             | The operation system (`target_os` cfg feature)
517    /// _pid            | The client process ID
518    /// _platform       | The machine platform (`target_arch` cfg feature)
519    /// program_name    | The first element of `std::env::args` if program_name isn't set by programs.
520    ///
521    /// [attr_tables]: https://dev.mysql.com/doc/refman/en/performance-schema-connection-attribute-tables.html
522    /// [`performance_schema`]: https://dev.mysql.com/doc/refman/8.0/en/performance-schema-system-variables.html#sysvar_performance_schema
523    /// [`performance_schema_session_connect_attrs_size`]: https://dev.mysql.com/doc/refman/en/performance-schema-system-variables.html#sysvar_performance_schema_session_connect_attrs_size
524    ///
525    pub fn get_connect_attrs(&self) -> Option<&HashMap<String, String>> {
526        self.0.connect_attrs.as_ref()
527    }
528
529    /// Disables `mysql_old_password` plugin (defaults to `true`).
530    ///
531    /// Available via `secure_auth` connection url parameter.
532    pub fn get_secure_auth(&self) -> bool {
533        self.0.secure_auth
534    }
535
536    /// Returns `true` if `mysql_clear_password` plugin support is enabled (defaults to `false`).
537    ///
538    /// `mysql_clear_password` enables client to send passwords to the server as cleartext, without
539    /// hashing or encryption (consult MySql documentation for more info).
540    ///
541    /// # Security Notes
542    ///
543    /// Sending passwords as cleartext may be a security problem in some configurations. Please
544    /// consider using TLS or encrypted tunnels for server connection.
545    ///
546    /// # Connection URL
547    ///
548    /// Use `enable_cleartext_plugin` URL parameter to set this value. E.g.
549    ///
550    /// ```
551    /// # use mysql::*;
552    /// # fn main() -> Result<()> {
553    /// let opts = Opts::from_url("mysql://localhost/db?enable_cleartext_plugin=true")?;
554    /// assert!(opts.get_enable_cleartext_plugin());
555    /// # Ok(()) }
556    /// ```
557    pub fn get_enable_cleartext_plugin(&self) -> bool {
558        self.0.enable_cleartext_plugin
559    }
560}
561
562/// Provides a way to build [`Opts`](struct.Opts.html).
563///
564/// ```ignore
565/// let mut ssl_opts = SslOpts::default();
566/// ssl_opts = ssl_opts.with_pkcs12_path(Some(Path::new("/foo/cert.p12")))
567///         .with_root_ca_path(Some(Path::new("/foo/root_ca.der")));
568///
569/// // You can create new default builder
570/// let mut builder = OptsBuilder::new();
571/// builder = builder.ip_or_hostname(Some("foo"))
572///        .db_name(Some("bar"))
573///        .ssl_opts(Some(ssl_opts));
574///
575/// // Or use existing T: Into<Opts>
576/// let builder = OptsBuilder::from_opts(existing_opts)
577///        .ip_or_hostname(Some("foo"))
578///        .db_name(Some("bar"));
579/// ```
580///
581/// ## Connection URL
582///
583/// `Opts` also could be constructed using connection URL. See docs on `OptsBuilder`'s methods for
584/// the list of options available via URL.
585///
586/// Example:
587///
588/// ```ignore
589/// let connection_opts = mysql::Opts::from_url("mysql://root:password@localhost:3307/mysql?prefer_socket=false").unwrap();
590/// let pool = mysql::Pool::new(connection_opts).unwrap();
591/// ```
592#[derive(Debug, Clone, PartialEq, Default)]
593pub struct OptsBuilder {
594    opts: Opts,
595}
596
597impl OptsBuilder {
598    pub fn new() -> Self {
599        OptsBuilder::default()
600    }
601
602    pub fn from_opts<T: Into<Opts>>(opts: T) -> Self {
603        OptsBuilder { opts: opts.into() }
604    }
605
606    /// Use a HashMap for creating an OptsBuilder instance:
607    /// ```ignore
608    /// OptsBuilder::new().from_hash_map(client);
609    /// ```
610    /// `HashMap` key,value pairs:
611    /// - pool_min = upper bound for [`PoolConstraints`]
612    /// - pool_max = lower bound for [`PoolConstraints`]
613    /// - user = Username
614    /// - password = Password
615    /// - host = Host name or ip address
616    /// - port = Port, default is 3306
617    /// - socket = Unix socket or pipe name(on windows) defaults to `None`
618    /// - db_name = Database name (defaults to `None`).
619    /// - prefer_socket = Prefer socket connection (defaults to `true`)
620    /// - tcp_keepalive_time_ms = TCP keep alive time for mysql connection (defaults to `None`)
621    /// - tcp_keepalive_probe_interval_secs = TCP keep alive interval between probes for mysql connection (defaults to `None`)
622    /// - tcp_keepalive_probe_count = TCP keep alive probe count for mysql connection (defaults to `None`)
623    /// - tcp_user_timeout_ms = TCP_USER_TIMEOUT time for mysql connection (defaults to `None`)
624    /// - compress = Compression level(defaults to `None`)
625    /// - tcp_connect_timeout_ms = Tcp connect timeout (defaults to `None`)
626    /// - stmt_cache_size = Number of prepared statements cached on the client side (per connection)
627    /// - secure_auth = Disable `mysql_old_password` auth plugin
628    ///
629    /// Login .cnf file parsing lib <https://github.com/rjcortese/myloginrs> returns a HashMap for client configs
630    ///
631    /// **Note:** You do **not** have to use myloginrs lib.
632    pub fn from_hash_map(mut self, client: &HashMap<String, String>) -> Result<Self, UrlError> {
633        let mut pool_min = PoolConstraints::DEFAULT.min();
634        let mut pool_max = PoolConstraints::DEFAULT.max();
635
636        for (key, value) in client.iter() {
637            match key.as_str() {
638                "pool_min" => match value.parse::<usize>() {
639                    Ok(parsed) => pool_min = parsed,
640                    Err(_) => {
641                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
642                    }
643                },
644                "pool_max" => match value.parse::<usize>() {
645                    Ok(parsed) => pool_max = parsed,
646                    Err(_) => {
647                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
648                    }
649                },
650                "user" => self.opts.0.user = Some(value.to_string()),
651                "password" => self.opts.0.pass = Some(value.to_string()),
652                "host" => {
653                    let host = url::Host::parse(value)
654                        .unwrap_or_else(|_| url::Host::Domain(value.to_owned()));
655                    self.opts.0.ip_or_hostname = host;
656                }
657                "port" => match value.parse::<u16>() {
658                    Ok(parsed) => self.opts.0.tcp_port = parsed,
659                    Err(_) => {
660                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
661                    }
662                },
663                "socket" => self.opts.0.socket = Some(value.to_string()),
664                "db_name" => self.opts.0.db_name = Some(value.to_string()),
665                "prefer_socket" => {
666                    //default to true like standard opts builder method
667                    match value.parse::<bool>() {
668                        Ok(parsed) => self.opts.0.prefer_socket = parsed,
669                        Err(_) => {
670                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
671                        }
672                    }
673                }
674                "enable_cleartext_plugin" => match value.parse::<bool>() {
675                    Ok(parsed) => self.opts.0.enable_cleartext_plugin = parsed,
676                    Err(_) => {
677                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
678                    }
679                },
680                "secure_auth" => match value.parse::<bool>() {
681                    Ok(parsed) => self.opts.0.secure_auth = parsed,
682                    Err(_) => {
683                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
684                    }
685                },
686                "tcp_keepalive_time_ms" => {
687                    //if cannot parse, default to none
688                    self.opts.0.tcp_keepalive_time = match value.parse::<u32>() {
689                        Ok(val) => Some(val),
690                        _ => {
691                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
692                        }
693                    }
694                }
695                #[cfg(any(target_os = "linux", target_os = "macos",))]
696                "tcp_keepalive_probe_interval_secs" => {
697                    //if cannot parse, default to none
698                    self.opts.0.tcp_keepalive_probe_interval_secs = match value.parse::<u32>() {
699                        Ok(val) => Some(val),
700                        _ => {
701                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
702                        }
703                    }
704                }
705                #[cfg(any(target_os = "linux", target_os = "macos",))]
706                "tcp_keepalive_probe_count" => {
707                    //if cannot parse, default to none
708                    self.opts.0.tcp_keepalive_probe_count = match value.parse::<u32>() {
709                        Ok(val) => Some(val),
710                        _ => {
711                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
712                        }
713                    }
714                }
715                #[cfg(target_os = "linux")]
716                "tcp_user_timeout_ms" => {
717                    self.opts.0.tcp_user_timeout = match value.parse::<u32>() {
718                        Ok(val) => Some(val),
719                        _ => {
720                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
721                        }
722                    }
723                }
724                "compress" => match value.parse::<u32>() {
725                    Ok(val) => self.opts.0.compress = Some(Compression::new(val)),
726                    Err(_) => {
727                        //not an int
728                        match value.as_str() {
729                            "fast" => self.opts.0.compress = Some(Compression::fast()),
730                            "best" => self.opts.0.compress = Some(Compression::best()),
731                            "true" => self.opts.0.compress = Some(Compression::default()),
732                            _ => {
733                                return Err(UrlError::InvalidValue(
734                                    key.to_string(),
735                                    value.to_string(),
736                                )); //should not go below this due to catch all
737                            }
738                        }
739                    }
740                },
741                "tcp_connect_timeout_ms" => {
742                    self.opts.0.tcp_connect_timeout = match value.parse::<u64>() {
743                        Ok(val) => Some(Duration::from_millis(val)),
744                        _ => {
745                            return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
746                        }
747                    }
748                }
749                "stmt_cache_size" => match value.parse::<usize>() {
750                    Ok(parsed) => self.opts.0.stmt_cache_size = parsed,
751                    Err(_) => {
752                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
753                    }
754                },
755                "reset_connection" => match value.parse::<bool>() {
756                    Ok(parsed) => {
757                        self.opts.0.pool_opts = self.opts.0.pool_opts.with_reset_connection(parsed)
758                    }
759                    Err(_) => {
760                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
761                    }
762                },
763                "check_health" => match value.parse::<bool>() {
764                    Ok(parsed) => {
765                        self.opts.0.pool_opts = self.opts.0.pool_opts.with_check_health(parsed)
766                    }
767                    Err(_) => {
768                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
769                    }
770                },
771                "max_allowed_packet" => match value.parse::<usize>() {
772                    Ok(parsed) => self.opts.0.max_allowed_packet = Some(parsed),
773                    Err(_) => {
774                        return Err(UrlError::InvalidValue(key.to_string(), value.to_string()))
775                    }
776                },
777                _ => {
778                    //throw an error if there is an unrecognized param
779                    return Err(UrlError::UnknownParameter(key.to_string()));
780                }
781            }
782        }
783
784        if let Some(pool_constraints) = PoolConstraints::new(pool_min, pool_max) {
785            self.opts.0.pool_opts = self.opts.0.pool_opts.with_constraints(pool_constraints);
786        } else {
787            return Err(UrlError::InvalidPoolConstraints {
788                min: pool_min,
789                max: pool_max,
790            });
791        }
792
793        Ok(self)
794    }
795
796    /// Address of mysql server (defaults to `127.0.0.1`). Host names should also work.
797    ///
798    /// **Note:** IPv6 addresses must be given in square brackets, e.g. `[::1]`.
799    pub fn ip_or_hostname<T: Into<String>>(mut self, ip_or_hostname: Option<T>) -> Self {
800        let new = ip_or_hostname
801            .map(Into::into)
802            .unwrap_or_else(|| "127.0.0.1".into());
803        self.opts.0.ip_or_hostname =
804            url::Host::parse(&new).unwrap_or_else(|_| url::Host::Domain(new.to_owned()));
805        self
806    }
807
808    /// TCP port of mysql server (defaults to `3306`).
809    pub fn tcp_port(mut self, tcp_port: u16) -> Self {
810        self.opts.0.tcp_port = tcp_port;
811        self
812    }
813
814    /// Socket path on unix or pipe name on windows (defaults to `None`).
815    ///
816    /// Can be defined using `socket` connection url parameter.
817    pub fn socket<T: Into<String>>(mut self, socket: Option<T>) -> Self {
818        self.opts.0.socket = socket.map(Into::into);
819        self
820    }
821
822    /// Defines `max_allowed_packet` option. See [`Opts::max_allowed_packet`].
823    ///
824    /// Note that it'll saturate to proper minimum and maximum values
825    /// for this parameter (see MySql documentation).
826    pub fn max_allowed_packet(mut self, max_allowed_packet: Option<usize>) -> Self {
827        self.opts.0.max_allowed_packet = max_allowed_packet.map(|x| x.clamp(1024, 1073741824));
828        self
829    }
830
831    /// User (defaults to `None`).
832    pub fn user<T: Into<String>>(mut self, user: Option<T>) -> Self {
833        self.opts.0.user = user.map(Into::into);
834        self
835    }
836
837    /// Password (defaults to `None`).
838    pub fn pass<T: Into<String>>(mut self, pass: Option<T>) -> Self {
839        self.opts.0.pass = pass.map(Into::into);
840        self
841    }
842
843    /// Database name (defaults to `None`).
844    pub fn db_name<T: Into<String>>(mut self, db_name: Option<T>) -> Self {
845        self.opts.0.db_name = db_name.map(Into::into);
846        self
847    }
848
849    /// The timeout for each attempt to read from the server (defaults to `None`).
850    ///
851    /// Note that named pipe connection will ignore duration's `nanos`, and also note that
852    /// it is an error to pass the zero `Duration` to this method.
853    pub fn read_timeout(mut self, read_timeout: Option<Duration>) -> Self {
854        self.opts.0.read_timeout = read_timeout;
855        self
856    }
857
858    /// The timeout for each attempt to write to the server (defaults to `None`).
859    ///
860    /// Note that named pipe connection will ignore duration's `nanos`, and also note that
861    /// it is likely error to pass the zero `Duration` to this method.
862    pub fn write_timeout(mut self, write_timeout: Option<Duration>) -> Self {
863        self.opts.0.write_timeout = write_timeout;
864        self
865    }
866
867    /// TCP keep alive time for mysql connection (defaults to `None`). Available as
868    /// `tcp_keepalive_time_ms` url parameter.
869    ///
870    /// Can be defined using `tcp_keepalive_time_ms` connection url parameter.
871    pub fn tcp_keepalive_time_ms(mut self, tcp_keepalive_time_ms: Option<u32>) -> Self {
872        self.opts.0.tcp_keepalive_time = tcp_keepalive_time_ms;
873        self
874    }
875
876    /// TCP keep alive interval between probes for mysql connection (defaults to `None`). Available as
877    /// `tcp_keepalive_probe_interval_secs` url parameter.
878    ///
879    /// Can be defined using `tcp_keepalive_probe_interval_secs` connection url parameter.
880    #[cfg(any(target_os = "linux", target_os = "macos",))]
881    pub fn tcp_keepalive_probe_interval_secs(
882        mut self,
883        tcp_keepalive_probe_interval_secs: Option<u32>,
884    ) -> Self {
885        self.opts.0.tcp_keepalive_probe_interval_secs = tcp_keepalive_probe_interval_secs;
886        self
887    }
888
889    /// TCP keep alive probe count for mysql connection (defaults to `None`). Available as
890    /// `tcp_keepalive_probe_count` url parameter.
891    ///
892    /// Can be defined using `tcp_keepalive_probe_count` connection url parameter.
893    #[cfg(any(target_os = "linux", target_os = "macos",))]
894    pub fn tcp_keepalive_probe_count(mut self, tcp_keepalive_probe_count: Option<u32>) -> Self {
895        self.opts.0.tcp_keepalive_probe_count = tcp_keepalive_probe_count;
896        self
897    }
898
899    /// TCP_USER_TIMEOUT for mysql connection (defaults to `None`). Available as
900    /// `tcp_user_timeout_ms` url parameter.
901    ///
902    /// Can be defined using `tcp_user_timeout_ms` connection url parameter.
903    #[cfg(target_os = "linux")]
904    pub fn tcp_user_timeout_ms(mut self, tcp_user_timeout_ms: Option<u32>) -> Self {
905        self.opts.0.tcp_user_timeout = tcp_user_timeout_ms;
906        self
907    }
908
909    /// Set the `TCP_NODELAY` option for the mysql connection (defaults to `true`).
910    ///
911    /// Setting this option to false re-enables Nagle's algorithm, which can cause unusually high
912    /// latency (~40ms) but may increase maximum throughput. See #132.
913    pub fn tcp_nodelay(mut self, nodelay: bool) -> Self {
914        self.opts.0.tcp_nodelay = nodelay;
915        self
916    }
917
918    /// Prefer socket connection (defaults to `true`). Available as `prefer_socket` url parameter
919    /// with value `true` or `false`.
920    ///
921    /// Will reconnect via socket (on named pipe on windows) after TCP connection
922    /// to `127.0.0.1` if `true`.
923    ///
924    /// Will fall back to TCP on error. Use `socket` option to enforce socket connection.
925    ///
926    /// Can be defined using `prefer_socket` connection url parameter.
927    pub fn prefer_socket(mut self, prefer_socket: bool) -> Self {
928        self.opts.0.prefer_socket = prefer_socket;
929        self
930    }
931
932    /// Commands to execute on each new database connection.
933    pub fn init<T: Into<String>>(mut self, init: Vec<T>) -> Self {
934        self.opts.0.init = init.into_iter().map(Into::into).collect();
935        self
936    }
937
938    /// Driver will require SSL connection if this option isn't `None` (default to `None`).
939    pub fn ssl_opts<T: Into<Option<SslOpts>>>(mut self, ssl_opts: T) -> Self {
940        self.opts.0.ssl_opts = ssl_opts.into();
941        self
942    }
943
944    /// Connection pool options (see [`Opts::get_pool_opts`]).
945    ///
946    /// Pass `None` to reset to default.
947    pub fn pool_opts<T: Into<Option<PoolOpts>>>(mut self, pool_opts: T) -> Self {
948        self.opts.0.pool_opts = pool_opts.into().unwrap_or_default();
949        self
950    }
951
952    /// Callback to handle requests for local files. These are
953    /// caused by using `LOAD DATA LOCAL INFILE` queries. The
954    /// callback is passed the filename, and a `Write`able object
955    /// to receive the contents of that file.
956    /// If unset, the default callback will read files relative to
957    /// the current directory.
958    pub fn local_infile_handler(mut self, handler: Option<LocalInfileHandler>) -> Self {
959        self.opts.0.local_infile_handler = handler;
960        self
961    }
962
963    /// Tcp connect timeout (defaults to `None`). Available as `tcp_connect_timeout_ms`
964    /// url parameter.
965    ///
966    /// Can be defined using `tcp_connect_timeout_ms` connection url parameter.
967    pub fn tcp_connect_timeout(mut self, timeout: Option<Duration>) -> Self {
968        self.opts.0.tcp_connect_timeout = timeout;
969        self
970    }
971
972    /// Bind address for a client (defaults to `None`).
973    ///
974    /// Use carefully. Will probably make pool unusable because of *address already in use*
975    /// errors.
976    pub fn bind_address<T>(mut self, bind_address: Option<T>) -> Self
977    where
978        T: Into<SocketAddr>,
979    {
980        self.opts.0.bind_address = bind_address.map(Into::into);
981        self
982    }
983
984    /// Number of prepared statements cached on the client side (per connection).
985    /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`].
986    ///
987    /// Can be defined using `stmt_cache_size` connection url parameter.
988    ///
989    /// Call with `None` to reset to default.
990    pub fn stmt_cache_size<T>(mut self, cache_size: T) -> Self
991    where
992        T: Into<Option<usize>>,
993    {
994        self.opts.0.stmt_cache_size = cache_size.into().unwrap_or(128);
995        self
996    }
997
998    /// If not `None`, then client will ask for compression if server supports it
999    /// (defaults to `None`).
1000    ///
1001    /// Can be defined using `compress` connection url parameter with values:
1002    /// * `true` - library defined default compression level;
1003    /// * `fast` - library defined fast compression level;
1004    /// * `best` - library defined best compression level;
1005    /// * `0`, `1`, ..., `9` - explicitly defined compression level where `0` stands for
1006    ///   "no compression";
1007    ///
1008    /// Note that compression level defined here will affect only outgoing packets.
1009    pub fn compress(mut self, compress: Option<crate::Compression>) -> Self {
1010        self.opts.0.compress = compress;
1011        self
1012    }
1013
1014    /// Additional client capabilities to set (defaults to empty).
1015    ///
1016    /// This value will be OR'ed with other client capabilities during connection initialization.
1017    ///
1018    /// ### Note
1019    ///
1020    /// It is a good way to set something like `CLIENT_FOUND_ROWS` but you should note that it
1021    /// won't let you to interfere with capabilities managed by other options (like
1022    /// `CLIENT_SSL` or `CLIENT_COMPRESS`). Also note that some capabilities are reserved,
1023    /// pointless or may broke the connection, so this option should be used with caution.
1024    pub fn additional_capabilities(mut self, additional_capabilities: CapabilityFlags) -> Self {
1025        let forbidden_flags: CapabilityFlags = CapabilityFlags::CLIENT_PROTOCOL_41
1026            | CapabilityFlags::CLIENT_SSL
1027            | CapabilityFlags::CLIENT_COMPRESS
1028            | CapabilityFlags::CLIENT_SECURE_CONNECTION
1029            | CapabilityFlags::CLIENT_LONG_PASSWORD
1030            | CapabilityFlags::CLIENT_TRANSACTIONS
1031            | CapabilityFlags::CLIENT_LOCAL_FILES
1032            | CapabilityFlags::CLIENT_MULTI_STATEMENTS
1033            | CapabilityFlags::CLIENT_MULTI_RESULTS
1034            | CapabilityFlags::CLIENT_PS_MULTI_RESULTS;
1035
1036        self.opts.0.additional_capabilities = additional_capabilities & !forbidden_flags;
1037        self
1038    }
1039
1040    /// Connect attributes (the default connect attributes are sent by default).
1041    ///
1042    /// This value is sent to the server as custom name-value attributes.
1043    /// You can see them from performance_schema tables: [`session_account_connect_attrs`
1044    /// and `session_connect_attrs`][attr_tables] when all of the following conditions
1045    /// are met.
1046    ///
1047    /// * The server is MySQL 5.6 or later, or MariaDB 10.0 or later.
1048    /// * [`performance_schema`] is on.
1049    /// * [`performance_schema_session_connect_attrs_size`] is -1 or big enough
1050    ///   to store specified attributes.
1051    ///
1052    /// ### Note
1053    ///
1054    /// - set `connect_attrs` to `None` to completely remove connect attributes
1055    /// - set `connect_attrs` to an empty map to send only the default attributes
1056    ///
1057    /// #### Warning
1058    ///
1059    /// > There is a bug in MySql 5.6 that kills COM_CHANGE_USER in the presence of connection
1060    /// > attributes so it's better to stick to `None` for mysql < 5.7.
1061    ///
1062    /// Attribute names that begin with an underscore (`_`) are not set by
1063    /// application programs because they are reserved for internal use.
1064    ///
1065    /// The following default attributes are sent in addition to ones set by programs.
1066    ///
1067    /// name            | value
1068    /// ----------------|--------------------------
1069    /// _client_name    | The client library name (`rust-mysql-simple`)
1070    /// _client_version | The client library version
1071    /// _os             | The operation system (`target_os` cfg feature)
1072    /// _pid            | The client process ID
1073    /// _platform       | The machine platform (`target_arch` cfg feature)
1074    /// program_name    | The first element of `std::env::args` if program_name isn't set by programs.
1075    ///
1076    /// [attr_tables]: https://dev.mysql.com/doc/refman/en/performance-schema-connection-attribute-tables.html
1077    /// [`performance_schema`]: https://dev.mysql.com/doc/refman/8.0/en/performance-schema-system-variables.html#sysvar_performance_schema
1078    /// [`performance_schema_session_connect_attrs_size`]: https://dev.mysql.com/doc/refman/en/performance-schema-system-variables.html#sysvar_performance_schema_session_connect_attrs_size
1079    ///
1080    pub fn connect_attrs<T1: Into<String> + Eq + Hash, T2: Into<String>>(
1081        mut self,
1082        connect_attrs: Option<HashMap<T1, T2>>,
1083    ) -> Self {
1084        if let Some(connect_attrs) = connect_attrs {
1085            let mut attrs = HashMap::with_capacity(connect_attrs.len());
1086            for (name, value) in connect_attrs {
1087                let name = name.into();
1088                if !name.starts_with('_') {
1089                    attrs.insert(name, value.into());
1090                }
1091            }
1092            self.opts.0.connect_attrs = Some(attrs);
1093        } else {
1094            self.opts.0.connect_attrs = None;
1095        }
1096        self
1097    }
1098
1099    /// Disables `mysql_old_password` plugin (defaults to `true`).
1100    ///
1101    /// Available via `secure_auth` connection url parameter.
1102    pub fn secure_auth(mut self, secure_auth: bool) -> Self {
1103        self.opts.0.secure_auth = secure_auth;
1104        self
1105    }
1106
1107    /// Enables Client-Side Cleartext Pluggable Authentication (defaults to `false`).
1108    ///
1109    /// Enables client to send passwords to the server as cleartext, without hashing or encryption
1110    /// (consult MySql documentation for more info).
1111    ///
1112    /// # Security Notes
1113    ///
1114    /// Sending passwords as cleartext may be a security problem in some configurations. Please
1115    /// consider using TLS or encrypted tunnels for server connection.
1116    ///
1117    /// # Connection URL
1118    ///
1119    /// Use `enable_cleartext_plugin` URL parameter to set this value. E.g.
1120    ///
1121    /// ```
1122    /// # use mysql::*;
1123    /// # fn main() -> Result<()> {
1124    /// let opts = Opts::from_url("mysql://localhost/db?enable_cleartext_plugin=true")?;
1125    /// assert!(opts.get_enable_cleartext_plugin());
1126    /// # Ok(()) }
1127    /// ```
1128    pub fn enable_cleartext_plugin(mut self, enable_cleartext_plugin: bool) -> Self {
1129        self.opts.0.enable_cleartext_plugin = enable_cleartext_plugin;
1130        self
1131    }
1132}
1133
1134impl From<OptsBuilder> for Opts {
1135    fn from(builder: OptsBuilder) -> Opts {
1136        builder.opts
1137    }
1138}
1139
1140fn get_opts_user_from_url(url: &Url) -> Option<String> {
1141    let user = url.username();
1142    if !user.is_empty() {
1143        Some(
1144            percent_decode(user.as_ref())
1145                .decode_utf8_lossy()
1146                .into_owned(),
1147        )
1148    } else {
1149        None
1150    }
1151}
1152
1153fn get_opts_pass_from_url(url: &Url) -> Option<String> {
1154    url.password().map(|pass| {
1155        percent_decode(pass.as_ref())
1156            .decode_utf8_lossy()
1157            .into_owned()
1158    })
1159}
1160
1161fn get_opts_db_name_from_url(url: &Url) -> Option<String> {
1162    if let Some(mut segments) = url.path_segments() {
1163        segments
1164            .next()
1165            .filter(|&db_name| !db_name.is_empty())
1166            .map(|db_name| {
1167                percent_decode(db_name.as_ref())
1168                    .decode_utf8_lossy()
1169                    .into_owned()
1170            })
1171    } else {
1172        None
1173    }
1174}
1175
1176fn from_url_basic(url_str: &str) -> Result<(Opts, Vec<(String, String)>), UrlError> {
1177    let url = Url::parse(url_str)?;
1178    if url.scheme() != "mysql" {
1179        return Err(UrlError::UnsupportedScheme(url.scheme().to_string()));
1180    }
1181    if url.cannot_be_a_base() {
1182        return Err(UrlError::BadUrl);
1183    }
1184    let user = get_opts_user_from_url(&url);
1185    let pass = get_opts_pass_from_url(&url);
1186    let ip_or_hostname = url
1187        .host()
1188        .ok_or(UrlError::BadUrl)
1189        .and_then(|host| url::Host::parse(&host.to_string()).map_err(|_| UrlError::BadUrl))?;
1190    let tcp_port = url.port().unwrap_or(3306);
1191    let db_name = get_opts_db_name_from_url(&url);
1192
1193    let query_pairs = url.query_pairs().into_owned().collect();
1194    let opts = Opts(Box::new(InnerOpts {
1195        user,
1196        pass,
1197        ip_or_hostname,
1198        tcp_port,
1199        db_name,
1200        ..InnerOpts::default()
1201    }));
1202
1203    Ok((opts, query_pairs))
1204}
1205
1206fn from_url(url: &str) -> Result<Opts, UrlError> {
1207    let (opts, query_pairs) = from_url_basic(url)?;
1208    let hash_map = query_pairs.into_iter().collect::<HashMap<String, String>>();
1209    OptsBuilder::from_opts(opts)
1210        .from_hash_map(&hash_map)
1211        .map(Into::into)
1212}
1213
1214/// [`COM_CHANGE_USER`][1] options.
1215///
1216/// Connection [`Opts`] are going to be updated accordingly upon `COM_CHANGE_USER`.
1217///
1218/// [`Opts`] won't be updated by default, because default `ChangeUserOpts` will reuse
1219/// connection's `user`, `pass` and `db_name`.
1220///
1221/// [1]: https://dev.mysql.com/doc/c-api/5.7/en/mysql-change-user.html
1222#[derive(Clone, Eq, PartialEq)]
1223pub struct ChangeUserOpts {
1224    user: Option<Option<String>>,
1225    pass: Option<Option<String>>,
1226    db_name: Option<Option<String>>,
1227}
1228
1229impl ChangeUserOpts {
1230    pub const DEFAULT: Self = Self {
1231        user: None,
1232        pass: None,
1233        db_name: None,
1234    };
1235
1236    pub(crate) fn update_opts(self, opts: &mut Opts) {
1237        if self.user.is_none() && self.pass.is_none() && self.db_name.is_none() {
1238            return;
1239        }
1240
1241        let mut builder = OptsBuilder::from_opts(opts.clone());
1242
1243        if let Some(user) = self.user {
1244            builder = builder.user(user);
1245        }
1246
1247        if let Some(pass) = self.pass {
1248            builder = builder.pass(pass);
1249        }
1250
1251        if let Some(db_name) = self.db_name {
1252            builder = builder.db_name(db_name);
1253        }
1254
1255        *opts = Opts::from(builder);
1256    }
1257
1258    /// Creates change user options that'll reuse connection options.
1259    pub fn new() -> Self {
1260        Self {
1261            user: None,
1262            pass: None,
1263            db_name: None,
1264        }
1265    }
1266
1267    /// Set [`Opts::get_user`] to the given value.
1268    pub fn with_user(mut self, user: Option<String>) -> Self {
1269        self.user = Some(user);
1270        self
1271    }
1272
1273    /// Set [`Opts::get_pass`] to the given value.
1274    pub fn with_pass(mut self, pass: Option<String>) -> Self {
1275        self.pass = Some(pass);
1276        self
1277    }
1278
1279    /// Set [`Opts::get_db_name`] to the given value.
1280    pub fn with_db_name(mut self, db_name: Option<String>) -> Self {
1281        self.db_name = Some(db_name);
1282        self
1283    }
1284
1285    /// Returns user.
1286    ///
1287    /// * if `None` then `self` does not meant to change user
1288    /// * if `Some(None)` then `self` will clear user
1289    /// * if `Some(Some(_))` then `self` will change user
1290    pub fn user(&self) -> Option<Option<&str>> {
1291        self.user.as_ref().map(|x| x.as_deref())
1292    }
1293
1294    /// Returns password.
1295    ///
1296    /// * if `None` then `self` does not meant to change password
1297    /// * if `Some(None)` then `self` will clear password
1298    /// * if `Some(Some(_))` then `self` will change password
1299    pub fn pass(&self) -> Option<Option<&str>> {
1300        self.pass.as_ref().map(|x| x.as_deref())
1301    }
1302
1303    /// Returns database name.
1304    ///
1305    /// * if `None` then `self` does not meant to change database name
1306    /// * if `Some(None)` then `self` will clear database name
1307    /// * if `Some(Some(_))` then `self` will change database name
1308    pub fn db_name(&self) -> Option<Option<&str>> {
1309        self.db_name.as_ref().map(|x| x.as_deref())
1310    }
1311}
1312
1313impl Default for ChangeUserOpts {
1314    fn default() -> Self {
1315        Self::new()
1316    }
1317}
1318
1319impl fmt::Debug for ChangeUserOpts {
1320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1321        f.debug_struct("ChangeUserOpts")
1322            .field("user", &self.user)
1323            .field(
1324                "pass",
1325                &self.pass.as_ref().map(|x| x.as_ref().map(|_| "...")),
1326            )
1327            .field("db_name", &self.db_name)
1328            .finish()
1329    }
1330}
1331
1332#[cfg(test)]
1333mod test {
1334    use mysql_common::proto::codec::Compression;
1335    use std::time::Duration;
1336
1337    use super::{InnerOpts, Opts, OptsBuilder};
1338
1339    #[allow(dead_code)]
1340    fn assert_conn_from_url_opts_optsbuilder(url: &str, opts: Opts, opts_builder: OptsBuilder) {
1341        crate::Conn::new(url).unwrap();
1342        crate::Conn::new(opts.clone()).unwrap();
1343        crate::Conn::new(opts_builder.clone()).unwrap();
1344        crate::Pool::new(url).unwrap();
1345        crate::Pool::new(opts).unwrap();
1346        crate::Pool::new(opts_builder).unwrap();
1347    }
1348
1349    #[test]
1350    fn should_report_empty_url_database_as_none() {
1351        let opt = Opts::from_url("mysql://localhost/").unwrap();
1352        assert_eq!(opt.get_db_name(), None);
1353    }
1354
1355    #[test]
1356    fn should_convert_url_into_opts() {
1357        #[cfg(any(target_os = "linux", target_os = "macos",))]
1358        let tcp_keepalive_probe_interval_secs = "&tcp_keepalive_probe_interval_secs=8";
1359        #[cfg(not(any(target_os = "linux", target_os = "macos",)))]
1360        let tcp_keepalive_probe_interval_secs = "";
1361
1362        #[cfg(any(target_os = "linux", target_os = "macos",))]
1363        let tcp_keepalive_probe_count = "&tcp_keepalive_probe_count=5";
1364        #[cfg(not(any(target_os = "linux", target_os = "macos",)))]
1365        let tcp_keepalive_probe_count = "";
1366
1367        #[cfg(target_os = "linux")]
1368        let tcp_user_timeout = "&tcp_user_timeout_ms=6000";
1369        #[cfg(not(target_os = "linux"))]
1370        let tcp_user_timeout = "";
1371
1372        let opts = format!(
1373            "mysql://us%20r:p%20w@localhost:3308/db%2dname?prefer_socket=false&tcp_keepalive_time_ms=5000{}{}{}&socket=%2Ftmp%2Fmysql.sock&compress=8",
1374            tcp_keepalive_probe_interval_secs,
1375            tcp_keepalive_probe_count,
1376            tcp_user_timeout,
1377        );
1378        assert_eq!(
1379            Opts(Box::new(InnerOpts {
1380                user: Some("us r".to_string()),
1381                pass: Some("p w".to_string()),
1382                ip_or_hostname: url::Host::Domain("localhost".to_string()),
1383                tcp_port: 3308,
1384                db_name: Some("db-name".to_string()),
1385                prefer_socket: false,
1386                tcp_keepalive_time: Some(5000),
1387                #[cfg(any(target_os = "linux", target_os = "macos",))]
1388                tcp_keepalive_probe_interval_secs: Some(8),
1389                #[cfg(any(target_os = "linux", target_os = "macos",))]
1390                tcp_keepalive_probe_count: Some(5),
1391                #[cfg(target_os = "linux")]
1392                tcp_user_timeout: Some(6000),
1393                socket: Some("/tmp/mysql.sock".into()),
1394                compress: Some(Compression::new(8)),
1395                ..InnerOpts::default()
1396            })),
1397            Opts::from_url(&opts).unwrap(),
1398        );
1399    }
1400
1401    #[test]
1402    #[should_panic]
1403    fn should_panic_on_invalid_url() {
1404        let opts = "42";
1405        Opts::from_url(opts).unwrap();
1406    }
1407
1408    #[test]
1409    #[should_panic]
1410    fn should_panic_on_invalid_scheme() {
1411        let opts = "postgres://localhost";
1412        Opts::from_url(opts).unwrap();
1413    }
1414
1415    #[test]
1416    #[should_panic]
1417    fn should_panic_on_unknown_query_param() {
1418        let opts = "mysql://localhost/foo?bar=baz";
1419        Opts::from_url(opts).unwrap();
1420    }
1421
1422    #[test]
1423    fn should_read_hashmap_into_opts() {
1424        use crate::OptsBuilder;
1425        macro_rules!  map(
1426            { $($key:expr => $value:expr), + }=> {
1427                {
1428                    let mut h = std::collections::HashMap::new();
1429                    $(
1430                        h.insert($key, $value);
1431                    )+
1432                    h
1433                }
1434            };
1435        );
1436
1437        let mut cnf_map = map! {
1438            "user".to_string() => "test".to_string(),
1439            "password".to_string() => "password".to_string(),
1440            "host".to_string() => "127.0.0.1".to_string(),
1441            "port".to_string() => "8080".to_string(),
1442            "db_name".to_string() => "test_db".to_string(),
1443            "prefer_socket".to_string() => "false".to_string(),
1444            "tcp_keepalive_time_ms".to_string() => "5000".to_string(),
1445            "compress".to_string() => "best".to_string(),
1446            "tcp_connect_timeout_ms".to_string() => "1000".to_string(),
1447            "stmt_cache_size".to_string() => "33".to_string(),
1448            "max_allowed_packet".to_string() => "65536".to_string()
1449        };
1450        #[cfg(any(target_os = "linux", target_os = "macos",))]
1451        cnf_map.insert(
1452            "tcp_keepalive_probe_interval_secs".to_string(),
1453            "8".to_string(),
1454        );
1455        #[cfg(any(target_os = "linux", target_os = "macos",))]
1456        cnf_map.insert("tcp_keepalive_probe_count".to_string(), "5".to_string());
1457
1458        let parsed_opts = OptsBuilder::new().from_hash_map(&cnf_map).unwrap();
1459
1460        assert_eq!(parsed_opts.opts.get_user(), Some("test"));
1461        assert_eq!(parsed_opts.opts.get_pass(), Some("password"));
1462        assert_eq!(parsed_opts.opts.get_ip_or_hostname(), "127.0.0.1");
1463        assert_eq!(parsed_opts.opts.get_tcp_port(), 8080);
1464        assert_eq!(parsed_opts.opts.get_db_name(), Some("test_db"));
1465        assert_eq!(parsed_opts.opts.get_max_allowed_packet(), Some(65536));
1466        assert!(!parsed_opts.opts.get_prefer_socket());
1467        assert_eq!(parsed_opts.opts.get_tcp_keepalive_time_ms(), Some(5000));
1468        #[cfg(any(target_os = "linux", target_os = "macos",))]
1469        assert_eq!(
1470            parsed_opts.opts.get_tcp_keepalive_probe_interval_secs(),
1471            Some(8)
1472        );
1473        #[cfg(any(target_os = "linux", target_os = "macos",))]
1474        assert_eq!(parsed_opts.opts.get_tcp_keepalive_probe_count(), Some(5));
1475        assert_eq!(
1476            parsed_opts.opts.get_compress(),
1477            Some(crate::Compression::best())
1478        );
1479        assert_eq!(
1480            parsed_opts.opts.get_tcp_connect_timeout(),
1481            Some(Duration::from_millis(1000))
1482        );
1483        assert_eq!(parsed_opts.opts.get_stmt_cache_size(), 33);
1484    }
1485
1486    #[test]
1487    fn should_have_url_err() {
1488        use crate::OptsBuilder;
1489        use crate::UrlError;
1490        macro_rules!  map(
1491            { $($key:expr => $value:expr), + }=> {
1492                {
1493                    let mut h = std::collections::HashMap::new();
1494                    $(
1495                        h.insert($key, $value);
1496                    )+
1497                    h
1498                }
1499            };
1500        );
1501
1502        let cnf_map = map! {
1503            "user".to_string() => "test".to_string(),
1504            "password".to_string() => "password".to_string(),
1505            "host".to_string() => "127.0.0.1".to_string(),
1506            "port".to_string() => "NOTAPORT".to_string(),
1507            "db_name".to_string() => "test_db".to_string(),
1508            "prefer_socket".to_string() => "false".to_string(),
1509            "tcp_keepalive_time_ms".to_string() => "5000".to_string(),
1510            "compress".to_string() => "best".to_string(),
1511            "tcp_connect_timeout_ms".to_string() => "1000".to_string(),
1512            "stmt_cache_size".to_string() => "33".to_string()
1513        };
1514
1515        let parsed = OptsBuilder::new().from_hash_map(&cnf_map);
1516        assert_eq!(
1517            parsed,
1518            Err(UrlError::InvalidValue(
1519                "port".to_string(),
1520                "NOTAPORT".to_string()
1521            ))
1522        );
1523    }
1524}