Skip to main content

idb/util/
mysql.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4/// MySQL connection configuration parsed from CLI args or .my.cnf.
5#[derive(Debug, Clone)]
6pub struct MysqlConfig {
7    pub host: String,
8    pub port: u16,
9    pub user: String,
10    pub password: Option<String>,
11    pub database: Option<String>,
12    pub socket: Option<String>,
13}
14
15impl Default for MysqlConfig {
16    fn default() -> Self {
17        MysqlConfig {
18            host: "localhost".to_string(),
19            port: 3306,
20            user: "root".to_string(),
21            password: None,
22            database: None,
23            socket: None,
24        }
25    }
26}
27
28impl MysqlConfig {
29    /// Build a mysql_async connection URL from the config.
30    pub fn connection_url(&self) -> String {
31        let mut url = format!("mysql://{}@{}:{}", self.user, self.host, self.port);
32        if let Some(ref db) = self.database {
33            url.push('/');
34            url.push_str(db);
35        }
36        url
37    }
38
39    /// Build an opts builder from config.
40    pub fn to_opts(&self) -> mysql_async::OptsBuilder {
41        let mut builder = mysql_async::OptsBuilder::default()
42            .ip_or_hostname(&self.host)
43            .tcp_port(self.port)
44            .user(Some(&self.user));
45
46        if let Some(ref pw) = self.password {
47            builder = builder.pass(Some(pw));
48        }
49        if let Some(ref db) = self.database {
50            builder = builder.db_name(Some(db));
51        }
52        if let Some(ref sock) = self.socket {
53            builder = builder.socket(Some(sock));
54        }
55
56        builder
57    }
58}
59
60/// Parse a MySQL defaults file (`.my.cnf` format) for `[client]` section credentials.
61pub fn parse_defaults_file(path: &Path) -> Option<MysqlConfig> {
62    let content = std::fs::read_to_string(path).ok()?;
63    let mut config = MysqlConfig::default();
64    let mut in_client = false;
65
66    for line in content.lines() {
67        let line = line.trim();
68        if line.starts_with('[') {
69            in_client = line.eq_ignore_ascii_case("[client]");
70            continue;
71        }
72        if !in_client || line.is_empty() || line.starts_with('#') {
73            continue;
74        }
75
76        if let Some((key, value)) = line.split_once('=') {
77            let key = key.trim().to_lowercase();
78            let value = value.trim().trim_matches('"').trim_matches('\'');
79            match key.as_str() {
80                "host" => config.host = value.to_string(),
81                "port" => {
82                    if let Ok(p) = value.parse() {
83                        config.port = p;
84                    }
85                }
86                "user" => config.user = value.to_string(),
87                "password" => config.password = Some(value.to_string()),
88                "socket" => config.socket = Some(value.to_string()),
89                "database" => config.database = Some(value.to_string()),
90                _ => {}
91            }
92        }
93    }
94
95    Some(config)
96}
97
98/// Find the default .my.cnf file.
99pub fn find_defaults_file() -> Option<std::path::PathBuf> {
100    // Check $HOME/.my.cnf
101    if let Some(home) = std::env::var_os("HOME") {
102        let path = Path::new(&home).join(".my.cnf");
103        if path.exists() {
104            return Some(path);
105        }
106    }
107    // Check /etc/my.cnf
108    let etc = Path::new("/etc/my.cnf");
109    if etc.exists() {
110        return Some(etc.to_path_buf());
111    }
112    None
113}
114
115/// Query result row as a HashMap of column name -> string value.
116pub type Row = HashMap<String, String>;