sqlint/connector/
connection_info.rs

1use crate::error::{Error, ErrorKind};
2use std::{borrow::Cow, fmt};
3use url::Url;
4
5#[cfg(feature = "mssql")]
6use crate::connector::MssqlUrl;
7#[cfg(feature = "mysql")]
8use crate::connector::MysqlUrl;
9#[cfg(feature = "postgresql")]
10use crate::connector::PostgresUrl;
11#[cfg(feature = "sqlite")]
12use crate::connector::SqliteParams;
13#[cfg(feature = "sqlite")]
14use std::convert::TryFrom;
15
16/// General information about a SQL connection.
17#[derive(Debug, Clone)]
18pub enum ConnectionInfo {
19    /// A PostgreSQL connection URL.
20    #[cfg(feature = "postgresql")]
21    #[cfg_attr(feature = "docs", doc(cfg(feature = "postgresql")))]
22    Postgres(PostgresUrl),
23    /// A MySQL connection URL.
24    #[cfg(feature = "mysql")]
25    #[cfg_attr(feature = "docs", doc(cfg(feature = "mysql")))]
26    Mysql(MysqlUrl),
27    /// A SQL Server connection URL.
28    #[cfg(feature = "mssql")]
29    #[cfg_attr(feature = "docs", doc(cfg(feature = "mssql")))]
30    Mssql(MssqlUrl),
31    /// A SQLite connection URL.
32    #[cfg(feature = "sqlite")]
33    #[cfg_attr(feature = "docs", doc(cfg(feature = "sqlite")))]
34    Sqlite {
35        /// The filesystem path of the SQLite database.
36        file_path: String,
37        /// The name the database is bound to - Always "main"
38        db_name: String,
39    },
40    #[cfg(feature = "sqlite")]
41    #[cfg_attr(feature = "docs", doc(cfg(feature = "sqlite")))]
42    InMemorySqlite { db_name: String },
43}
44
45impl ConnectionInfo {
46    /// Parse `ConnectionInfo` out from an SQL connection string.
47    ///
48    /// Will fail if URI is invalid or the scheme points to an unsupported
49    /// database.
50    pub fn from_url(url_str: &str) -> crate::Result<Self> {
51        let url_result: Result<Url, _> = url_str.parse();
52
53        // Non-URL database strings are interpreted as SQLite file paths.
54        match url_str {
55            #[cfg(feature = "sqlite")]
56            s if s.starts_with("file") => {
57                if url_result.is_err() {
58                    let params = SqliteParams::try_from(s)?;
59
60                    return Ok(ConnectionInfo::Sqlite { file_path: params.file_path, db_name: params.db_name });
61                }
62            }
63            #[cfg(feature = "mssql")]
64            s if s.starts_with("jdbc:sqlserver") || s.starts_with("sqlserver") => {
65                return Ok(ConnectionInfo::Mssql(MssqlUrl::new(url_str)?));
66            }
67            _ => (),
68        }
69
70        let url = url_result?;
71
72        let sql_family = SqlFamily::from_scheme(url.scheme()).ok_or_else(|| {
73            let kind =
74                ErrorKind::DatabaseUrlIsInvalid(format!("{} is not a supported database URL scheme.", url.scheme()));
75            Error::builder(kind).build()
76        })?;
77
78        match sql_family {
79            #[cfg(feature = "mysql")]
80            SqlFamily::Mysql => Ok(ConnectionInfo::Mysql(MysqlUrl::new(url)?)),
81            #[cfg(feature = "sqlite")]
82            SqlFamily::Sqlite => {
83                let params = SqliteParams::try_from(url_str)?;
84
85                Ok(ConnectionInfo::Sqlite { file_path: params.file_path, db_name: params.db_name })
86            }
87            #[cfg(feature = "postgresql")]
88            SqlFamily::Postgres => Ok(ConnectionInfo::Postgres(PostgresUrl::new(url)?)),
89            #[allow(unreachable_patterns)]
90            _ => unreachable!(),
91        }
92    }
93
94    /// The provided database name. This will be `None` on SQLite.
95    pub fn dbname(&self) -> Option<&str> {
96        match self {
97            #[cfg(feature = "postgresql")]
98            ConnectionInfo::Postgres(url) => Some(url.dbname()),
99            #[cfg(feature = "mysql")]
100            ConnectionInfo::Mysql(url) => Some(url.dbname()),
101            #[cfg(feature = "mssql")]
102            ConnectionInfo::Mssql(url) => Some(url.dbname()),
103            #[cfg(feature = "sqlite")]
104            ConnectionInfo::Sqlite { .. } | ConnectionInfo::InMemorySqlite { .. } => None,
105        }
106    }
107
108    /// This is what item names are prefixed with in queries.
109    ///
110    /// - In SQLite, this is the schema name that the database file was attached as.
111    /// - In Postgres, it is the selected schema inside the current database.
112    /// - In MySQL, it is the database name.
113    pub fn schema_name(&self) -> &str {
114        match self {
115            #[cfg(feature = "postgresql")]
116            ConnectionInfo::Postgres(url) => url.schema(),
117            #[cfg(feature = "mysql")]
118            ConnectionInfo::Mysql(url) => url.dbname(),
119            #[cfg(feature = "mssql")]
120            ConnectionInfo::Mssql(url) => url.schema(),
121            #[cfg(feature = "sqlite")]
122            ConnectionInfo::Sqlite { db_name, .. } => db_name,
123            #[cfg(feature = "sqlite")]
124            ConnectionInfo::InMemorySqlite { db_name } => db_name,
125        }
126    }
127
128    /// The provided database host. This will be `"localhost"` on SQLite.
129    pub fn host(&self) -> &str {
130        match self {
131            #[cfg(feature = "postgresql")]
132            ConnectionInfo::Postgres(url) => url.host(),
133            #[cfg(feature = "mysql")]
134            ConnectionInfo::Mysql(url) => url.host(),
135            #[cfg(feature = "mssql")]
136            ConnectionInfo::Mssql(url) => url.host(),
137            #[cfg(feature = "sqlite")]
138            ConnectionInfo::Sqlite { .. } | ConnectionInfo::InMemorySqlite { .. } => "localhost",
139        }
140    }
141
142    /// The provided database user name. This will be `None` on SQLite.
143    pub fn username(&self) -> Option<Cow<str>> {
144        match self {
145            #[cfg(feature = "postgresql")]
146            ConnectionInfo::Postgres(url) => Some(url.username()),
147            #[cfg(feature = "mysql")]
148            ConnectionInfo::Mysql(url) => Some(url.username()),
149            #[cfg(feature = "mssql")]
150            ConnectionInfo::Mssql(url) => url.username().map(Cow::from),
151            #[cfg(feature = "sqlite")]
152            ConnectionInfo::Sqlite { .. } | ConnectionInfo::InMemorySqlite { .. } => None,
153        }
154    }
155
156    /// The database file for SQLite, otherwise `None`.
157    pub fn file_path(&self) -> Option<&str> {
158        match self {
159            #[cfg(feature = "postgresql")]
160            ConnectionInfo::Postgres(_) => None,
161            #[cfg(feature = "mysql")]
162            ConnectionInfo::Mysql(_) => None,
163            #[cfg(feature = "mssql")]
164            ConnectionInfo::Mssql(_) => None,
165            #[cfg(feature = "sqlite")]
166            ConnectionInfo::Sqlite { file_path, .. } => Some(file_path),
167            #[cfg(feature = "sqlite")]
168            ConnectionInfo::InMemorySqlite { .. } => None,
169        }
170    }
171
172    /// The family of databases connected.
173    pub fn sql_family(&self) -> SqlFamily {
174        match self {
175            #[cfg(feature = "postgresql")]
176            ConnectionInfo::Postgres(_) => SqlFamily::Postgres,
177            #[cfg(feature = "mysql")]
178            ConnectionInfo::Mysql(_) => SqlFamily::Mysql,
179            #[cfg(feature = "mssql")]
180            ConnectionInfo::Mssql(_) => SqlFamily::Mssql,
181            #[cfg(feature = "sqlite")]
182            ConnectionInfo::Sqlite { .. } | ConnectionInfo::InMemorySqlite { .. } => SqlFamily::Sqlite,
183        }
184    }
185
186    /// The provided database port, if applicable.
187    pub fn port(&self) -> Option<u16> {
188        match self {
189            #[cfg(feature = "postgresql")]
190            ConnectionInfo::Postgres(url) => Some(url.port()),
191            #[cfg(feature = "mysql")]
192            ConnectionInfo::Mysql(url) => Some(url.port()),
193            #[cfg(feature = "mssql")]
194            ConnectionInfo::Mssql(url) => Some(url.port()),
195            #[cfg(feature = "sqlite")]
196            ConnectionInfo::Sqlite { .. } | ConnectionInfo::InMemorySqlite { .. } => None,
197        }
198    }
199
200    /// Whether the pgbouncer mode is enabled.
201    pub fn pg_bouncer(&self) -> bool {
202        #[allow(unreachable_patterns)]
203        match self {
204            #[cfg(feature = "postgresql")]
205            ConnectionInfo::Postgres(url) => url.pg_bouncer(),
206            _ => false,
207        }
208    }
209
210    /// A string describing the database location, meant for error messages. It will be the host
211    /// and port on MySQL/Postgres, and the file path on SQLite.
212    pub fn database_location(&self) -> String {
213        match self {
214            #[cfg(feature = "postgresql")]
215            ConnectionInfo::Postgres(url) => format!("{}:{}", url.host(), url.port()),
216            #[cfg(feature = "mysql")]
217            ConnectionInfo::Mysql(url) => format!("{}:{}", url.host(), url.port()),
218            #[cfg(feature = "mssql")]
219            ConnectionInfo::Mssql(url) => format!("{}:{}", url.host(), url.port()),
220            #[cfg(feature = "sqlite")]
221            ConnectionInfo::Sqlite { file_path, .. } => file_path.clone(),
222            #[cfg(feature = "sqlite")]
223            ConnectionInfo::InMemorySqlite { .. } => "in-memory".into(),
224        }
225    }
226}
227
228/// One of the supported SQL variants.
229#[derive(Debug, PartialEq, Eq, Clone, Copy)]
230pub enum SqlFamily {
231    #[cfg(feature = "postgresql")]
232    #[cfg_attr(feature = "docs", doc(cfg(feature = "postgresql")))]
233    Postgres,
234    #[cfg(feature = "mysql")]
235    #[cfg_attr(feature = "docs", doc(cfg(feature = "mysql")))]
236    Mysql,
237    #[cfg(feature = "sqlite")]
238    #[cfg_attr(feature = "docs", doc(cfg(feature = "sqlite")))]
239    Sqlite,
240    #[cfg(feature = "mssql")]
241    #[cfg_attr(feature = "docs", doc(cfg(feature = "mssql")))]
242    Mssql,
243}
244
245impl SqlFamily {
246    /// Get a string representation of the family.
247    pub fn as_str(self) -> &'static str {
248        match self {
249            #[cfg(feature = "postgresql")]
250            SqlFamily::Postgres => "postgresql",
251            #[cfg(feature = "mysql")]
252            SqlFamily::Mysql => "mysql",
253            #[cfg(feature = "sqlite")]
254            SqlFamily::Sqlite => "sqlite",
255            #[cfg(feature = "mssql")]
256            SqlFamily::Mssql => "mssql",
257        }
258    }
259
260    /// Convert url scheme to an SqlFamily.
261    pub fn from_scheme(url_scheme: &str) -> Option<Self> {
262        match url_scheme {
263            #[cfg(feature = "sqlite")]
264            "file" => Some(SqlFamily::Sqlite),
265            #[cfg(feature = "postgresql")]
266            "postgres" | "postgresql" => Some(SqlFamily::Postgres),
267            #[cfg(feature = "mysql")]
268            "mysql" => Some(SqlFamily::Mysql),
269            _ => None,
270        }
271    }
272
273    /// Check if a family exists for the given scheme.
274    pub fn scheme_is_supported(url_scheme: &str) -> bool {
275        Self::from_scheme(url_scheme).is_some()
276    }
277
278    /// True, if family is PostgreSQL.
279    #[cfg(feature = "postgresql")]
280    pub fn is_postgres(&self) -> bool {
281        matches!(self, SqlFamily::Postgres)
282    }
283
284    /// True, if family is PostgreSQL.
285    #[cfg(not(feature = "postgresql"))]
286    pub fn is_postgres(&self) -> bool {
287        false
288    }
289
290    /// True, if family is MySQL.
291    #[cfg(feature = "mysql")]
292    pub fn is_mysql(&self) -> bool {
293        matches!(self, SqlFamily::Mysql)
294    }
295
296    /// True, if family is MySQL.
297    #[cfg(not(feature = "mysql"))]
298    pub fn is_mysql(&self) -> bool {
299        false
300    }
301
302    /// True, if family is SQLite.
303    #[cfg(feature = "sqlite")]
304    pub fn is_sqlite(&self) -> bool {
305        matches!(self, SqlFamily::Sqlite)
306    }
307
308    /// True, if family is SQLite.
309    #[cfg(not(feature = "sqlite"))]
310    pub fn is_sqlite(&self) -> bool {
311        false
312    }
313
314    /// True, if family is SQL Server.
315    #[cfg(feature = "mssql")]
316    pub fn is_mssql(&self) -> bool {
317        matches!(self, SqlFamily::Mssql)
318    }
319
320    /// True, if family is SQL Server.
321    #[cfg(not(feature = "mssql"))]
322    pub fn is_mssql(&self) -> bool {
323        false
324    }
325}
326
327impl fmt::Display for SqlFamily {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        write!(f, "{}", self.as_str())
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    #[cfg(any(feature = "sqlite", feature = "mysql"))]
336    use super::*;
337
338    #[test]
339    #[cfg(feature = "sqlite")]
340    fn sqlite_connection_info_from_str_interprets_relative_path_correctly() {
341        let conn_info = ConnectionInfo::from_url("file:dev.db").unwrap();
342
343        #[allow(irrefutable_let_patterns)]
344        if let ConnectionInfo::Sqlite { file_path, db_name: _ } = conn_info {
345            assert_eq!(file_path, "dev.db");
346        } else {
347            panic!("Wrong type of connection info, should be Sqlite");
348        }
349    }
350
351    #[test]
352    #[cfg(feature = "mysql")]
353    fn mysql_connection_info_from_str() {
354        let conn_info = ConnectionInfo::from_url("mysql://myuser:my%23pass%23word@lclhst:5432/mydb").unwrap();
355
356        #[allow(irrefutable_let_patterns)]
357        if let ConnectionInfo::Mysql(url) = conn_info {
358            assert_eq!(url.password().unwrap(), "my#pass#word");
359            assert_eq!(url.host(), "lclhst");
360            assert_eq!(url.username(), "myuser");
361            assert_eq!(url.dbname(), "mydb");
362        } else {
363            panic!("Wrong type of connection info, should be Mysql");
364        }
365    }
366}