Skip to content

Commit b29eab0

Browse files
feat: add to_url_lossy to connect options (#2902)
* feat: add get_url to connect options Add a get_url to connect options and implement it for all needed types; include get_filename for sqlite. These changes make it easier to test sqlx. * refactor: use expect with message * refactor: change method name to `to_url_lossy` * fix: remove unused imports
1 parent 34860b7 commit b29eab0

File tree

9 files changed

+253
-5
lines changed

9 files changed

+253
-5
lines changed

sqlx-core/src/any/options.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ impl ConnectOptions for AnyConnectOptions {
4343
})
4444
}
4545

46+
fn to_url_lossy(&self) -> Url {
47+
self.database_url.clone()
48+
}
49+
4650
#[inline]
4751
fn connect(&self) -> BoxFuture<'_, Result<AnyConnection, Error>> {
4852
AnyConnection::connect(self)

sqlx-core/src/connection.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,35 @@ pub trait ConnectOptions: 'static + Send + Sync + FromStr<Err = Error> + Debug +
189189
/// Parse the `ConnectOptions` from a URL.
190190
fn from_url(url: &Url) -> Result<Self, Error>;
191191

192+
/// Get a connection URL that may be used to connect to the same database as this `ConnectOptions`.
193+
///
194+
/// ### Note: Lossy
195+
/// Any flags or settings which do not have a representation in the URL format will be lost.
196+
/// They will fall back to their default settings when the URL is parsed.
197+
///
198+
/// The only settings guaranteed to be preserved are:
199+
/// * Username
200+
/// * Password
201+
/// * Hostname
202+
/// * Port
203+
/// * Database name
204+
/// * Unix socket or SQLite database file path
205+
/// * SSL mode (if applicable)
206+
/// * SSL CA certificate path
207+
/// * SSL client certificate path
208+
/// * SSL client key path
209+
///
210+
/// Additional settings are driver-specific. Refer to the source of a given implementation
211+
/// to see which options are preserved in the URL.
212+
///
213+
/// ### Panics
214+
/// This defaults to `unimplemented!()`.
215+
///
216+
/// Individual drivers should override this to implement the intended behavior.
217+
fn to_url_lossy(&self) -> Url {
218+
unimplemented!()
219+
}
220+
192221
/// Establish a new database connection with the options specified by `self`.
193222
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
194223
where

sqlx-mysql/src/options/connect.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ impl ConnectOptions for MySqlConnectOptions {
1414
Self::parse_from_url(url)
1515
}
1616

17+
fn to_url_lossy(&self) -> Url {
18+
self.build_url()
19+
}
20+
1721
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
1822
where
1923
Self::Connection: Sized,

sqlx-mysql/src/options/parse.rs

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::str::FromStr;
22

3-
use percent_encoding::percent_decode_str;
3+
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
44
use sqlx_core::Url;
55

6-
use crate::error::Error;
6+
use crate::{error::Error, MySqlSslMode};
77

88
use super::MySqlConnectOptions;
99

@@ -78,6 +78,65 @@ impl MySqlConnectOptions {
7878

7979
Ok(options)
8080
}
81+
82+
pub(crate) fn build_url(&self) -> Url {
83+
let mut url = Url::parse(&format!(
84+
"mysql://{}@{}:{}",
85+
self.username, self.host, self.port
86+
))
87+
.expect("BUG: generated un-parseable URL");
88+
89+
if let Some(password) = &self.password {
90+
let password = utf8_percent_encode(&password, NON_ALPHANUMERIC).to_string();
91+
let _ = url.set_password(Some(&password));
92+
}
93+
94+
if let Some(database) = &self.database {
95+
url.set_path(&database);
96+
}
97+
98+
let ssl_mode = match self.ssl_mode {
99+
MySqlSslMode::Disabled => "DISABLED",
100+
MySqlSslMode::Preferred => "PREFERRED",
101+
MySqlSslMode::Required => "REQUIRED",
102+
MySqlSslMode::VerifyCa => "VERIFY_CA",
103+
MySqlSslMode::VerifyIdentity => "VERIFY_IDENTITY",
104+
};
105+
url.query_pairs_mut().append_pair("ssl-mode", ssl_mode);
106+
107+
if let Some(ssl_ca) = &self.ssl_ca {
108+
url.query_pairs_mut()
109+
.append_pair("ssl-ca", &ssl_ca.to_string());
110+
}
111+
112+
url.query_pairs_mut().append_pair("charset", &self.charset);
113+
114+
if let Some(collation) = &self.collation {
115+
url.query_pairs_mut().append_pair("charset", &collation);
116+
}
117+
118+
if let Some(ssl_client_cert) = &self.ssl_client_cert {
119+
url.query_pairs_mut()
120+
.append_pair("ssl-cert", &ssl_client_cert.to_string());
121+
}
122+
123+
if let Some(ssl_client_key) = &self.ssl_client_key {
124+
url.query_pairs_mut()
125+
.append_pair("ssl-key", &ssl_client_key.to_string());
126+
}
127+
128+
url.query_pairs_mut().append_pair(
129+
"statement-cache-capacity",
130+
&self.statement_cache_capacity.to_string(),
131+
);
132+
133+
if let Some(socket) = &self.socket {
134+
url.query_pairs_mut()
135+
.append_pair("socket", &socket.to_string_lossy());
136+
}
137+
138+
url
139+
}
81140
}
82141

83142
impl FromStr for MySqlConnectOptions {
@@ -104,3 +163,16 @@ fn it_parses_password_with_non_ascii_chars_correctly() {
104163

105164
assert_eq!(Some("p@ssw0rd".into()), opts.password);
106165
}
166+
167+
#[test]
168+
fn it_returns_the_parsed_url() {
169+
let url = "mysql://username:p@ssw0rd@hostname:3306/database";
170+
let opts = MySqlConnectOptions::from_str(url).unwrap();
171+
172+
let mut expected_url = Url::parse(url).unwrap();
173+
// MySqlConnectOptions defaults
174+
let query_string = "ssl-mode=PREFERRED&charset=utf8mb4&statement-cache-capacity=100";
175+
expected_url.set_query(Some(query_string));
176+
177+
assert_eq!(expected_url, opts.build_url());
178+
}

sqlx-postgres/src/options/connect.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ impl ConnectOptions for PgConnectOptions {
1313
Self::parse_from_url(url)
1414
}
1515

16+
fn to_url_lossy(&self) -> Url {
17+
self.build_url()
18+
}
19+
1620
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
1721
where
1822
Self::Connection: Sized,

sqlx-postgres/src/options/parse.rs

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::error::Error;
2-
use crate::PgConnectOptions;
3-
use sqlx_core::percent_encoding::percent_decode_str;
2+
use crate::{PgConnectOptions, PgSslMode};
3+
use sqlx_core::percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
44
use sqlx_core::Url;
55
use std::net::IpAddr;
66
use std::str::FromStr;
@@ -108,6 +108,62 @@ impl PgConnectOptions {
108108

109109
Ok(options)
110110
}
111+
112+
pub(crate) fn build_url(&self) -> Url {
113+
let host = match &self.socket {
114+
Some(socket) => {
115+
utf8_percent_encode(&*socket.to_string_lossy(), NON_ALPHANUMERIC).to_string()
116+
}
117+
None => self.host.to_owned(),
118+
};
119+
120+
let mut url = Url::parse(&format!(
121+
"postgres://{}@{}:{}",
122+
self.username, host, self.port
123+
))
124+
.expect("BUG: generated un-parseable URL");
125+
126+
if let Some(password) = &self.password {
127+
let password = utf8_percent_encode(&password, NON_ALPHANUMERIC).to_string();
128+
let _ = url.set_password(Some(&password));
129+
}
130+
131+
if let Some(database) = &self.database {
132+
url.set_path(&database);
133+
}
134+
135+
let ssl_mode = match self.ssl_mode {
136+
PgSslMode::Allow => "ALLOW",
137+
PgSslMode::Disable => "DISABLED",
138+
PgSslMode::Prefer => "PREFERRED",
139+
PgSslMode::Require => "REQUIRED",
140+
PgSslMode::VerifyCa => "VERIFY_CA",
141+
PgSslMode::VerifyFull => "VERIFY_FULL",
142+
};
143+
url.query_pairs_mut().append_pair("ssl-mode", ssl_mode);
144+
145+
if let Some(ssl_root_cert) = &self.ssl_root_cert {
146+
url.query_pairs_mut()
147+
.append_pair("ssl-root-cert", &ssl_root_cert.to_string());
148+
}
149+
150+
if let Some(ssl_client_cert) = &self.ssl_client_cert {
151+
url.query_pairs_mut()
152+
.append_pair("ssl-cert", &ssl_client_cert.to_string());
153+
}
154+
155+
if let Some(ssl_client_key) = &self.ssl_client_key {
156+
url.query_pairs_mut()
157+
.append_pair("ssl-key", &ssl_client_key.to_string());
158+
}
159+
160+
url.query_pairs_mut().append_pair(
161+
"statement-cache-capacity",
162+
&self.statement_cache_capacity.to_string(),
163+
);
164+
165+
url
166+
}
111167
}
112168

113169
impl FromStr for PgConnectOptions {
@@ -242,3 +298,31 @@ fn it_parses_sqlx_options_correctly() {
242298
opts.options
243299
);
244300
}
301+
302+
#[test]
303+
fn it_returns_the_parsed_url_when_socket() {
304+
let url = "postgres://username@%2Fvar%2Flib%2Fpostgres/database";
305+
let opts = PgConnectOptions::from_str(url).unwrap();
306+
307+
let mut expected_url = Url::parse(url).unwrap();
308+
// PgConnectOptions defaults
309+
let query_string = "ssl-mode=PREFERRED&statement-cache-capacity=100";
310+
let port = 5432;
311+
expected_url.set_query(Some(query_string));
312+
let _ = expected_url.set_port(Some(port));
313+
314+
assert_eq!(expected_url, opts.build_url());
315+
}
316+
317+
#[test]
318+
fn it_returns_the_parsed_url_when_host() {
319+
let url = "postgres://username:p@ssw0rd@hostname:5432/database";
320+
let opts = PgConnectOptions::from_str(url).unwrap();
321+
322+
let mut expected_url = Url::parse(url).unwrap();
323+
// PgConnectOptions defaults
324+
let query_string = "ssl-mode=PREFERRED&statement-cache-capacity=100";
325+
expected_url.set_query(Some(query_string));
326+
327+
assert_eq!(expected_url, opts.build_url());
328+
}

sqlx-sqlite/src/options/connect.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ impl ConnectOptions for SqliteConnectOptions {
2424
Self::from_str(url.as_str())
2525
}
2626

27+
fn to_url_lossy(&self) -> Url {
28+
self.build_url()
29+
}
30+
2731
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
2832
where
2933
Self::Connection: Sized,

sqlx-sqlite/src/options/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@ impl SqliteConnectOptions {
211211
self
212212
}
213213

214+
/// Gets the current name of the database file.
215+
pub fn get_filename(self) -> Cow<'static, Path> {
216+
self.filename
217+
}
218+
214219
/// Set the enforcement of [foreign key constraints](https://www.sqlite.org/pragma.html#pragma_foreign_keys).
215220
///
216221
/// SQLx chooses to enable this by default so that foreign keys function as expected,

sqlx-sqlite/src/options/parse.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use crate::error::Error;
22
use crate::SqliteConnectOptions;
3-
use percent_encoding::percent_decode_str;
3+
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
44
use std::borrow::Cow;
55
use std::path::{Path, PathBuf};
66
use std::str::FromStr;
77
use std::sync::atomic::{AtomicUsize, Ordering};
8+
use url::Url;
89

910
// https://www.sqlite.org/uri.html
1011

@@ -111,6 +112,36 @@ impl SqliteConnectOptions {
111112

112113
Ok(options)
113114
}
115+
116+
pub(crate) fn build_url(&self) -> Url {
117+
let filename =
118+
utf8_percent_encode(&self.filename.to_string_lossy(), NON_ALPHANUMERIC).to_string();
119+
let mut url =
120+
Url::parse(&format!("sqlite://{}", filename)).expect("BUG: generated un-parseable URL");
121+
122+
let mode = match (self.in_memory, self.create_if_missing, self.read_only) {
123+
(true, _, _) => "memory",
124+
(false, true, _) => "rwc",
125+
(false, false, true) => "ro",
126+
(false, false, false) => "rw",
127+
};
128+
url.query_pairs_mut().append_pair("mode", mode);
129+
130+
let cache = match self.shared_cache {
131+
true => "shared",
132+
false => "private",
133+
};
134+
url.query_pairs_mut().append_pair("cache", cache);
135+
136+
url.query_pairs_mut()
137+
.append_pair("immutable", &self.immutable.to_string());
138+
139+
if let Some(vfs) = &self.vfs {
140+
url.query_pairs_mut().append_pair("vfs", &vfs);
141+
}
142+
143+
url
144+
}
114145
}
115146

116147
impl FromStr for SqliteConnectOptions {
@@ -169,3 +200,14 @@ fn test_parse_shared_in_memory() -> Result<(), Error> {
169200

170201
Ok(())
171202
}
203+
204+
#[test]
205+
fn it_returns_the_parsed_url() -> Result<(), Error> {
206+
let url = "sqlite://test.db?mode=rw&cache=shared";
207+
let options: SqliteConnectOptions = url.parse()?;
208+
209+
let expected_url = Url::parse(url).unwrap();
210+
assert_eq!(options.build_url(), expected_url);
211+
212+
Ok(())
213+
}

0 commit comments

Comments
 (0)