Skip to content

Commit a09903a

Browse files
authored
fix(quaint): add url decode for non-ascii db names (#5750)
Description ## Summary Fix connection failure when using databases with non-ASCII names (e.g., Chinese characters like `测试库`). ## Problem When connecting to a PostgreSQL database with a non-ASCII name, the URL must be percent-encoded: postgresql://user:pass@localhost:5432/%E6%B5%8B%E8%AF%95%E5%BA%93 Previously, `dbname()` returned the encoded string `%E6%B5%8B%E8%AF%95%E5%BA%93` as-is, causing: Error: Database does not exist: %E6%B5%8B%E8%AF%95%E5%BA%93 for Example my database ![2026-01-18_15-23-24](https://github.com/user-attachments/assets/78b2a7f1-83e4-46d3-a47b-f581735eed7e) <img width="1765" height="452" alt="스크린샷 2026-01-25 20-24-00" src="https://github.com/user-attachments/assets/9b049e61-b223-49f8-9c6a-cd23e1cbddf3" /> ## Solution - Decode percent-encoded path segment in `PostgresNativeUrl::dbname()` using `percent_decode` - Change return type from `&str` to `Cow<'_, str>` to handle decoded strings - Update dependent code in connection_info, schema-engine, and qe-setup ## Test Plan - [x] Unit tests for percent-decoding logic (Chinese, spaces, special characters) - [x] Integration test connecting to real PostgreSQL with Chinese database name ## Before/After **Before (bug exists):** Connecting to: postgresql://.../%E6%B5%8B%E8%AF%95%E5%BA%93 ❌ Connection FAILED Error: Database does not exist: %E6%B5%8B%E8%AF%95%E5%BA%93 **After (fixed):** Connecting to: postgresql://.../%E6%B5%8B%E8%AF%95%E5%BA%93 ✅ Connection SUCCESS **Relation** prisma/prisma#26886
1 parent dd122f8 commit a09903a

File tree

6 files changed

+91
-13
lines changed

6 files changed

+91
-13
lines changed

quaint/src/connector/connection_info.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ impl ConnectionInfo {
111111
}
112112

113113
/// The provided database name. This will be `None` on SQLite.
114-
pub fn dbname(&self) -> Option<&str> {
114+
pub fn dbname(&self) -> Option<Cow<'_, str>> {
115115
match self {
116116
#[cfg(any(
117117
feature = "sqlite-native",
@@ -123,9 +123,9 @@ impl ConnectionInfo {
123123
#[cfg(feature = "postgresql-native")]
124124
NativeConnectionInfo::Postgres(url) => Some(url.dbname()),
125125
#[cfg(feature = "mysql-native")]
126-
NativeConnectionInfo::Mysql(url) => url.dbname(),
126+
NativeConnectionInfo::Mysql(url) => url.dbname().map(Cow::Borrowed),
127127
#[cfg(feature = "mssql-native")]
128-
NativeConnectionInfo::Mssql(url) => Some(url.dbname()),
128+
NativeConnectionInfo::Mssql(url) => Some(Cow::Borrowed(url.dbname())),
129129
#[cfg(feature = "sqlite-native")]
130130
NativeConnectionInfo::Sqlite { .. } | NativeConnectionInfo::InMemorySqlite { .. } => None,
131131
},

quaint/src/connector/postgres/native/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ impl PostgresNativeUrl {
207207
config.password(self.password().as_ref());
208208
config.host(self.host());
209209
config.port(self.port());
210-
config.dbname(self.dbname());
210+
config.dbname(self.dbname().as_ref());
211211
config.pgbouncer_mode(self.query_params.pg_bouncer);
212212

213213
if let Some(options) = self.options() {

quaint/src/connector/postgres/url.rs

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ impl PostgresUrl {
7878
Ok(Self::WebSocket(PostgresWebSocketUrl::new(url, api_key)))
7979
}
8080

81-
pub fn dbname(&self) -> &str {
81+
pub fn dbname(&self) -> Cow<'_, str> {
8282
match self {
8383
Self::Native(url) => url.dbname(),
84-
Self::WebSocket(url) => url.dbname(),
84+
Self::WebSocket(url) => Cow::Borrowed(url.dbname()),
8585
}
8686
}
8787

@@ -193,11 +193,20 @@ impl PostgresNativeUrl {
193193
is_url_localhost(&self.url)
194194
}
195195

196-
/// Name of the database connected. Defaults to `postgres`.
197-
pub fn dbname(&self) -> &str {
196+
/// decoded database name. Defaults to `postgres`.
197+
pub fn dbname(&self) -> Cow<'_, str> {
198198
match self.url.path_segments() {
199-
Some(mut segments) => segments.next().unwrap_or("postgres"),
200-
None => "postgres",
199+
Some(mut segments) => {
200+
let segment = segments.next().unwrap_or("postgres");
201+
match percent_decode(segment.as_bytes()).decode_utf8() {
202+
Ok(dbname) => dbname,
203+
Err(_) => {
204+
tracing::warn!("Couldn't decode dbname to UTF-8, using the non-decoded version.");
205+
segment.into()
206+
}
207+
}
208+
}
209+
None => Cow::Borrowed("postgres"),
201210
}
202211
}
203212

@@ -595,6 +604,31 @@ mod tests {
595604
assert_eq!("/var/run/postgresql", url.host());
596605
}
597606

607+
#[test]
608+
fn should_decode_percent_encoded_dbname() {
609+
// Chinese characters: 测试库 (test database)
610+
let url = PostgresNativeUrl::new(
611+
Url::parse("postgresql://user:pass@localhost:5432/%E6%B5%8B%E8%AF%95%E5%BA%93").unwrap(),
612+
)
613+
.unwrap();
614+
assert_eq!("测试库", url.dbname());
615+
}
616+
617+
#[test]
618+
fn should_decode_dbname_with_spaces() {
619+
let url =
620+
PostgresNativeUrl::new(Url::parse("postgresql://user:pass@localhost:5432/my%20database").unwrap()).unwrap();
621+
assert_eq!("my database", url.dbname());
622+
}
623+
624+
#[test]
625+
fn should_decode_dbname_with_special_characters() {
626+
// test-db_name
627+
let url = PostgresNativeUrl::new(Url::parse("postgresql://user:pass@localhost:5432/test%2Ddb%5Fname").unwrap())
628+
.unwrap();
629+
assert_eq!("test-db_name", url.dbname());
630+
}
631+
598632
#[test]
599633
fn should_allow_changing_of_cache_size() {
600634
let url =
@@ -815,4 +849,48 @@ mod tests {
815849
// CRDB does NOT support setting the search_path via a connection parameter if the identifier is unsafe.
816850
assert_eq!(config.get_search_path(), None);
817851
}
852+
853+
/// Tests that connecting to a database with a percent-encoded name works correctly.
854+
///
855+
/// This test verifies:
856+
/// 1. A database with Chinese characters (测试库) can be created
857+
/// 2. The percent-encoded URL correctly connects to the database
858+
/// 3. The `dbname()` function returns the decoded database name
859+
#[tokio::test]
860+
async fn should_connect_to_db_with_percent_encoded_name() {
861+
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
862+
863+
let base_url = Url::parse(&CONN_STR).unwrap();
864+
let conn = Quaint::new(base_url.as_str()).await.unwrap();
865+
866+
// Create a database with Chinese characters: 测试库 (meaning "test database")
867+
let test_db_name = "测试库";
868+
let _ = conn
869+
.raw_cmd(&format!(r#"DROP DATABASE IF EXISTS "{test_db_name}""#))
870+
.await;
871+
conn.raw_cmd(&format!(r#"CREATE DATABASE "{test_db_name}""#))
872+
.await
873+
.unwrap();
874+
875+
// Build URL with percent-encoded database name
876+
// 测试库 -> %E6%B5%8B%E8%AF%95%E5%BA%93
877+
let encoded_db_name = utf8_percent_encode(test_db_name, NON_ALPHANUMERIC).to_string();
878+
let mut test_url = base_url.clone();
879+
test_url.set_path(&format!("/{encoded_db_name}"));
880+
881+
// Connect using percent-encoded URL and verify dbname() returns decoded value
882+
let test_conn = Quaint::new(test_url.as_str()).await.unwrap();
883+
let pg_url = PostgresNativeUrl::new(test_url).unwrap();
884+
assert_eq!(test_db_name, pg_url.dbname());
885+
886+
// Verify the connection actually works by executing a simple query
887+
let result = test_conn.query_raw("SELECT 1 as test", &[]).await.unwrap();
888+
assert_eq!(1, result.len());
889+
890+
// Cleanup: drop the test database
891+
drop(test_conn);
892+
conn.raw_cmd(&format!(r#"DROP DATABASE IF EXISTS "{test_db_name}""#))
893+
.await
894+
.unwrap();
895+
}
818896
}

query-engine/connector-test-kit-rs/qe-setup/src/cockroachdb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub(crate) async fn cockroach_setup(url: String, prisma_schema: &str) -> Connect
2121

2222
conn.raw_cmd(&query).await.unwrap();
2323

24-
drop_db_when_thread_exits(parsed_url, db_name);
24+
drop_db_when_thread_exits(parsed_url, &db_name);
2525
let params = ConnectorParams::new(url, Default::default(), None);
2626
let mut connector = sql_schema_connector::SqlSchemaConnector::new_cockroach(params)?;
2727
crate::diff_and_apply(prisma_schema, &mut connector).await

schema-engine/connectors/sql-schema-connector/src/flavour/postgres.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ impl MigratePostgresUrl {
166166
Ok(Self(postgres_url))
167167
}
168168

169-
pub(super) fn dbname(&self) -> &str {
169+
pub(super) fn dbname(&self) -> Cow<'_, str> {
170170
self.0.dbname()
171171
}
172172

schema-engine/connectors/sql-schema-connector/src/flavour/postgres/connector/native/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ pub async fn create_database(state: &State) -> ConnectorResult<String> {
260260

261261
conn.close().await;
262262

263-
Ok(db_name.to_owned())
263+
Ok(db_name.into_owned())
264264
}
265265

266266
pub async fn drop_database(state: &State) -> ConnectorResult<()> {

0 commit comments

Comments
 (0)