pub(super) fn driver_for_url(database_url: &str) -> &'static str {
let scheme = database_url
.split_once(':')
.map_or("", |(scheme, _)| scheme);
if scheme.eq_ignore_ascii_case("postgres") || scheme.eq_ignore_ascii_case("postgresql") {
"postgres"
} else if scheme.eq_ignore_ascii_case("mysql") || scheme.eq_ignore_ascii_case("mariadb") {
"mysql"
} else {
"sqlite"
}
}
pub(super) fn has_placeholder_brackets(database_url: &str) -> bool {
let mut rest = database_url;
while let Some(open) = rest.find('[') {
let after_open = &rest[open + 1..];
let Some(close) = after_open.find(']') else {
return false;
};
let literal = &after_open[..close];
let address = literal
.split_once('%')
.map_or(literal, |(address, _)| address);
if address.parse::<std::net::Ipv6Addr>().is_err() {
return true;
}
rest = &after_open[close + 1..];
}
false
}
pub(super) fn ensure_configured_dsn(database_url: &str) -> Result<(), crate::Error> {
if has_placeholder_brackets(database_url) {
return Err(crate::Error::Internal(
"DATABASE_URL contains placeholder brackets like [your-database-id]. \
Update your .env file with a real connection string."
.to_string(),
));
}
Ok(())
}
fn sqlite_parts(database_url: &str) -> Option<(&str, &str)> {
if !database_url.starts_with("sqlite") {
return None;
}
let rest = database_url
.trim_start_matches("sqlite:")
.trim_start_matches("//")
.trim_start_matches("file:");
Some(rest.split_once('?').unwrap_or((rest, "")))
}
fn sqlite_mode(query: &str) -> Option<&str> {
query.split('&').find_map(|parameter| {
parameter
.split_once('=')
.filter(|(key, _)| key.eq_ignore_ascii_case("mode"))
.map(|(_, mode)| mode)
})
}
pub(super) fn is_sqlite_memory(database_url: &str) -> bool {
sqlite_parts(database_url).is_some_and(|(path, query)| {
path == ":memory:"
|| sqlite_mode(query).is_some_and(|mode| mode.eq_ignore_ascii_case("memory"))
})
}
pub(super) fn prepare_sqlite_file(database_url: &str) {
let Some((path_part, query)) = sqlite_parts(database_url) else {
return;
};
if path_part.is_empty()
|| is_sqlite_memory(database_url)
|| sqlite_mode(query).is_some_and(|mode| !mode.eq_ignore_ascii_case("rwc"))
{
return;
}
let Some(decoded) = percent_decode_utf8(path_part) else {
return;
};
let path = std::path::Path::new(&decoded);
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
let _ = std::fs::create_dir_all(parent);
}
if !path.exists() {
let _ = std::fs::File::create(path);
}
}
fn percent_decode_utf8(encoded: &str) -> Option<String> {
fn hex_value(digit: u8) -> Option<u8> {
char::from(digit)
.to_digit(16)
.and_then(|value| u8::try_from(value).ok())
}
let bytes = encoded.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while let Some(&byte) = bytes.get(index) {
let escaped = (byte == b'%')
.then(|| {
let high = hex_value(*bytes.get(index + 1)?)?;
let low = hex_value(*bytes.get(index + 2)?)?;
Some((high << 4) | low)
})
.flatten();
match escaped {
Some(value) => {
decoded.push(value);
index += 3;
}
None => {
decoded.push(byte);
index += 1;
}
}
}
String::from_utf8(decoded).ok()
}
#[cfg(test)]
mod tests {
use super::{driver_for_url, ensure_configured_dsn, percent_decode_utf8};
#[test]
fn dsn_paths_decode_like_sqlx() {
for (encoded, decoded) in [
("data/app.db", Some("data/app.db")),
(
"C:/Users/John%20Doe/app/data.db",
Some("C:/Users/John Doe/app/data.db"),
),
("data%3Fdir/a%23b.db", Some("data?dir/a#b.db")),
("caf%C3%A9.db", Some("café.db")),
("100%.db", Some("100%.db")),
("bad%zz.db", Some("bad%zz.db")),
("tail%2", Some("tail%2")),
("%25", Some("%")),
("invalid%FF.db", None),
] {
assert_eq!(
percent_decode_utf8(encoded).as_deref(),
decoded,
"{encoded}"
);
}
}
#[test]
fn driver_follows_the_dsn_scheme_and_its_aliases() {
for (dsn, driver) in [
("postgres://app:pw@db/app", "postgres"),
("postgresql://app:pw@db/app", "postgres"),
("POSTGRES://app:pw@db/app", "postgres"),
("mysql://app:pw@db/app", "mysql"),
("mariadb://app:pw@db/app", "mysql"),
("MariaDB://app:pw@db/app", "mysql"),
("sqlite::memory:", "sqlite"),
("sqlite://data/app.db", "sqlite"),
("data/app.db", "sqlite"),
] {
assert_eq!(driver_for_url(dsn), driver, "{dsn}");
}
}
#[test]
fn ipv6_literal_hosts_are_not_template_placeholders() {
for dsn in [
"postgres://app:pw@[2001:db8::10]:5432/app",
"mysql://app:pw@[::1]/app",
"postgres://app@[fe80::1%25eth0]:5432/app",
] {
assert!(ensure_configured_dsn(dsn).is_ok(), "{dsn}");
}
for dsn in [
"libsql://[your-database-id].turso.io",
"postgres://app:[YOUR-PASSWORD]@db.example.com/app",
"postgres://app@[::1]:5432/[database]",
] {
assert!(
matches!(ensure_configured_dsn(dsn), Err(crate::Error::Internal(_))),
"{dsn}"
);
}
}
}