zero_mysql/
opts.rs

1use std::sync::Arc;
2
3use url::Url;
4
5use crate::buffer_pool::{BufferPool, GLOBAL_BUFFER_POOL};
6use crate::constant::CapabilityFlags;
7use crate::error::Error;
8
9/// A configuration for connection
10///
11/// ```rs
12/// let mut opts1 = Opts::default();
13/// opts1.port = 5000;
14///
15/// let mut opts2 = Opts::try_from("mysql://root:password@localhost:3306?compress=true&tcp_nodelay=false");
16/// opts2.compress = true;
17/// ```
18#[derive(Debug, Clone)]
19pub struct Opts {
20    /// Enable TCP_NODELAY socket option to disable Nagle's algorithm.
21    /// Unix socket is not affected.
22    /// Default: `true`
23    pub tcp_nodelay: bool,
24
25    /// The client capabilities are `CAPABILITIES_ALWAYS_ENABLED | (opts.capabilities & CAPABILITIES_CONFIGURABLE)`.
26    /// The final negotiated capabilities are `SERVER_CAPABILITIES & CLIENT_CAPABILITIES`.
27    /// Default: `CapabilityFlags::empty()`
28    pub capabilities: CapabilityFlags,
29
30    /// Enable compression for the connection.
31    /// Default: `false`
32    pub compress: bool,
33
34    /// Database name to use.
35    /// Default: `None`
36    pub db: Option<String>,
37
38    /// Hostname or IP address.
39    /// Default: `""`
40    pub host: String,
41
42    /// Port number for the MySQL server.
43    /// Default: `3306`
44    pub port: u16,
45
46    /// Unix socket path. Only supported on Unix platforms.
47    /// Default: `None`
48    pub socket: Option<String>,
49
50    /// Username for authentication (can be empty for anonymous connections).
51    /// Default: `""`
52    pub user: String,
53
54    /// Password for authentication.
55    /// Default: `""`
56    pub password: String,
57
58    /// Enable TLS.
59    /// Default: `false`
60    pub tls: bool,
61
62    /// When connected via TCP, read `SELECT @@socket` and reconnect to the unix socket.
63    /// Only has effect on Unix platforms.
64    /// Default: `true`
65    pub upgrade_to_unix_socket: bool,
66
67    /// SQL command to execute after connection is established.
68    /// Default: `None`
69    pub init_command: Option<String>,
70
71    /// Reset connection state when returning to pool.
72    /// Default: `true`
73    pub pool_reset_conn: bool,
74
75    /// Maximum number of idle connections in the pool.
76    /// Default: `100`
77    pub pool_max_idle_conn: usize,
78
79    /// Maximum number of concurrent connections (active + idle).
80    /// `None` means unlimited.
81    /// Default: `None`
82    pub pool_max_concurrency: Option<usize>,
83
84    /// `BufferPool` to reuse byte buffers (`Vec<u8>`).
85    /// Default: `GLOBAL_BUFFER_POOL`
86    pub buffer_pool: Arc<BufferPool>,
87}
88
89impl Default for Opts {
90    fn default() -> Self {
91        Self {
92            tcp_nodelay: true,
93            capabilities: CapabilityFlags::empty(),
94            compress: false,
95            db: None,
96            host: String::new(),
97            port: 3306,
98            socket: None,
99            user: String::new(),
100            password: String::new(),
101            tls: false,
102            upgrade_to_unix_socket: true,
103            init_command: None,
104            pool_reset_conn: true,
105            pool_max_idle_conn: 100,
106            pool_max_concurrency: None,
107            buffer_pool: Arc::clone(&GLOBAL_BUFFER_POOL),
108        }
109    }
110}
111
112/// Parse a boolean value from a query parameter.
113/// Accepts: "1", "0", "true", "false", "True", "False"
114fn parse_bool(key: &str, value: &str) -> Result<bool, Error> {
115    match value {
116        "1" | "true" | "True" => Ok(true),
117        "0" | "false" | "False" => Ok(false),
118        _ => Err(Error::BadUsageError(format!(
119            "Invalid boolean value '{}' for parameter '{}', expected 1, 0, true, false, True, or False",
120            value, key
121        ))),
122    }
123}
124
125/// Parse a usize value from a query parameter.
126fn parse_usize(key: &str, value: &str) -> Result<usize, Error> {
127    value.parse().map_err(|_| {
128        Error::BadUsageError(format!(
129            "Invalid unsigned integer value '{}' for parameter '{}'",
130            value, key
131        ))
132    })
133}
134
135/// Parse connection options from a MySQL URL.
136///
137/// # URL Format
138///
139/// ```text
140/// mysql://[user[:password]@]host[:port][/database][?parameters]
141/// ```
142///
143/// # Query Parameters
144///
145/// - `socket`
146/// - `tls` (or `ssl`)
147/// - `compress`
148/// - `tcp_nodelay`
149/// - `upgrade_to_unix_socket`
150/// - `init_command`
151/// - `pool_reset_conn`
152/// - `pool_max_idle_conn`
153/// - `pool_max_concurrency`
154///
155/// Boolean values accept: `1`, `0`, `true`, `false`, `True`, `False`
156///
157/// # Examples
158///
159/// ```
160/// use zero_mysql::Opts;
161///
162/// // Basic connection
163/// let opts = Opts::try_from("mysql://localhost").unwrap();
164///
165/// // With credentials and database
166/// let opts = Opts::try_from("mysql://root:password@localhost:3306/mydb").unwrap();
167///
168/// // With query parameters
169/// let opts = Opts::try_from("mysql://localhost?tls=true&compress=true").unwrap();
170///
171/// // Unix socket (hostname is ignored)
172/// let opts = Opts::try_from("mysql://localhost?socket=/var/run/mysqld/mysqld.sock").unwrap();
173/// ```
174impl TryFrom<&Url> for Opts {
175    type Error = Error;
176
177    fn try_from(url: &Url) -> Result<Self, Self::Error> {
178        if url.scheme() != "mysql" {
179            return Err(Error::BadUsageError(format!(
180                "Invalid URL scheme '{}', expected 'mysql'",
181                url.scheme()
182            )));
183        }
184
185        let host = url.host_str().unwrap_or_default().to_string();
186        let port = url.port().unwrap_or(3306);
187        let user = url.username().to_string();
188        let password = url.password().unwrap_or_default().to_string();
189        let db = url
190            .path()
191            .strip_prefix('/')
192            .filter(|db| !db.is_empty())
193            .map(ToString::to_string);
194
195        let mut opts = Self {
196            host,
197            port,
198            user,
199            password,
200            db,
201            ..Default::default()
202        };
203
204        for (key, value) in url.query_pairs() {
205            match key.as_ref() {
206                "socket" => opts.socket = Some(value.into_owned()),
207                "tls" | "ssl" => opts.tls = parse_bool(&key, &value)?,
208                "compress" => opts.compress = parse_bool(&key, &value)?,
209                "tcp_nodelay" => opts.tcp_nodelay = parse_bool(&key, &value)?,
210                "upgrade_to_unix_socket" => opts.upgrade_to_unix_socket = parse_bool(&key, &value)?,
211                "init_command" => opts.init_command = Some(value.into_owned()),
212                "pool_reset_conn" => opts.pool_reset_conn = parse_bool(&key, &value)?,
213                "pool_max_idle_conn" => opts.pool_max_idle_conn = parse_usize(&key, &value)?,
214                "pool_max_concurrency" => {
215                    opts.pool_max_concurrency = Some(parse_usize(&key, &value)?)
216                }
217                _ => {
218                    return Err(Error::BadUsageError(format!(
219                        "Unknown query parameter '{}'",
220                        key
221                    )));
222                }
223            }
224        }
225
226        Ok(opts)
227    }
228}
229
230impl TryFrom<&str> for Opts {
231    type Error = Error;
232
233    fn try_from(url: &str) -> Result<Self, Self::Error> {
234        let parsed = Url::parse(url)
235            .map_err(|e| Error::BadUsageError(format!("Failed to parse MySQL URL: {}", e)))?;
236        Self::try_from(&parsed)
237    }
238}