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