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.
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 /// Default: `true`
64 pub upgrade_to_unix_socket: bool,
65
66 /// SQL command to execute after connection is established.
67 /// Default: `None`
68 pub init_command: Option<String>,
69
70 /// Reset connection state when returning to pool.
71 /// Default: `true`
72 pub pool_reset_conn: bool,
73
74 /// Maximum number of idle connections in the pool.
75 /// Default: `100`
76 pub pool_max_idle_conn: usize,
77
78 /// Maximum number of concurrent connections (active + idle).
79 /// `None` means unlimited.
80 /// Default: `None`
81 pub pool_max_concurrency: Option<usize>,
82
83 /// `BufferPool` to reuse byte buffers (`Vec<u8>`).
84 /// Default: `GLOBAL_BUFFER_POOL`
85 pub buffer_pool: Arc<BufferPool>,
86}
87
88impl Default for Opts {
89 fn default() -> Self {
90 Self {
91 tcp_nodelay: true,
92 capabilities: CapabilityFlags::empty(),
93 compress: false,
94 db: None,
95 host: String::new(),
96 port: 3306,
97 socket: None,
98 user: String::new(),
99 password: String::new(),
100 tls: false,
101 upgrade_to_unix_socket: true,
102 init_command: None,
103 pool_reset_conn: true,
104 pool_max_idle_conn: 100,
105 pool_max_concurrency: None,
106 buffer_pool: Arc::clone(&GLOBAL_BUFFER_POOL),
107 }
108 }
109}
110
111/// Parse a boolean value from a query parameter.
112/// Accepts: "1", "0", "true", "false", "True", "False"
113fn parse_bool(key: &str, value: &str) -> Result<bool, Error> {
114 match value {
115 "1" | "true" | "True" => Ok(true),
116 "0" | "false" | "False" => Ok(false),
117 _ => Err(Error::BadUsageError(format!(
118 "Invalid boolean value '{}' for parameter '{}', expected 1, 0, true, false, True, or False",
119 value, key
120 ))),
121 }
122}
123
124/// Parse a usize value from a query parameter.
125fn parse_usize(key: &str, value: &str) -> Result<usize, Error> {
126 value.parse().map_err(|_| {
127 Error::BadUsageError(format!(
128 "Invalid unsigned integer value '{}' for parameter '{}'",
129 value, key
130 ))
131 })
132}
133
134/// Parse connection options from a MySQL URL.
135///
136/// # URL Format
137///
138/// ```text
139/// mysql://[user[:password]@]host[:port][/database][?parameters]
140/// ```
141///
142/// # Query Parameters
143///
144/// - `socket`
145/// - `tls` (or `ssl`)
146/// - `compress`
147/// - `tcp_nodelay`
148/// - `upgrade_to_unix_socket`
149/// - `init_command`
150/// - `pool_reset_conn`
151/// - `pool_max_idle_conn`
152/// - `pool_max_concurrency`
153///
154/// Boolean values accept: `1`, `0`, `true`, `false`, `True`, `False`
155///
156/// # Examples
157///
158/// ```
159/// use zero_mysql::Opts;
160///
161/// // Basic connection
162/// let opts = Opts::try_from("mysql://localhost").unwrap();
163///
164/// // With credentials and database
165/// let opts = Opts::try_from("mysql://root:password@localhost:3306/mydb").unwrap();
166///
167/// // With query parameters
168/// let opts = Opts::try_from("mysql://localhost?tls=true&compress=true").unwrap();
169///
170/// // Unix socket (hostname is ignored)
171/// let opts = Opts::try_from("mysql://localhost?socket=/var/run/mysqld/mysqld.sock").unwrap();
172/// ```
173impl TryFrom<&Url> for Opts {
174 type Error = Error;
175
176 fn try_from(url: &Url) -> Result<Self, Self::Error> {
177 if url.scheme() != "mysql" {
178 return Err(Error::BadUsageError(format!(
179 "Invalid URL scheme '{}', expected 'mysql'",
180 url.scheme()
181 )));
182 }
183
184 let host = url.host_str().unwrap_or_default().to_string();
185 let port = url.port().unwrap_or(3306);
186 let user = url.username().to_string();
187 let password = url.password().unwrap_or_default().to_string();
188 let db = url
189 .path()
190 .strip_prefix('/')
191 .filter(|db| !db.is_empty())
192 .map(ToString::to_string);
193
194 let mut opts = Self {
195 host,
196 port,
197 user,
198 password,
199 db,
200 ..Default::default()
201 };
202
203 for (key, value) in url.query_pairs() {
204 match key.as_ref() {
205 "socket" => opts.socket = Some(value.into_owned()),
206 "tls" | "ssl" => opts.tls = parse_bool(&key, &value)?,
207 "compress" => opts.compress = parse_bool(&key, &value)?,
208 "tcp_nodelay" => opts.tcp_nodelay = parse_bool(&key, &value)?,
209 "upgrade_to_unix_socket" => opts.upgrade_to_unix_socket = parse_bool(&key, &value)?,
210 "init_command" => opts.init_command = Some(value.into_owned()),
211 "pool_reset_conn" => opts.pool_reset_conn = parse_bool(&key, &value)?,
212 "pool_max_idle_conn" => opts.pool_max_idle_conn = parse_usize(&key, &value)?,
213 "pool_max_concurrency" => {
214 opts.pool_max_concurrency = Some(parse_usize(&key, &value)?)
215 }
216 _ => {
217 return Err(Error::BadUsageError(format!(
218 "Unknown query parameter '{}'",
219 key
220 )));
221 }
222 }
223 }
224
225 Ok(opts)
226 }
227}
228
229impl TryFrom<&str> for Opts {
230 type Error = Error;
231
232 fn try_from(url: &str) -> Result<Self, Self::Error> {
233 let parsed = Url::parse(url)
234 .map_err(|e| Error::BadUsageError(format!("Failed to parse MySQL URL: {}", e)))?;
235 Self::try_from(&parsed)
236 }
237}