diff --git a/apps/desktop/src/lib/database/databaseTree.ts b/apps/desktop/src/lib/database/databaseTree.ts index 937e0deca9..6e69b8c521 100644 --- a/apps/desktop/src/lib/database/databaseTree.ts +++ b/apps/desktop/src/lib/database/databaseTree.ts @@ -17,8 +17,8 @@ export function sortSidebarDatabases(databases: readonly DatabaseInfo[]): Databa export function buildDatabaseTreeNodes(connectionId: string, databases: DatabaseInfo[], options: { includeDefaultWhenEmpty?: boolean } = {}): TreeNode[] { const nodes = sortSidebarDatabases(databases).flatMap((db) => { - const name = db.name.trim(); - if (!name) return []; + const name = db.name; + if (!name.trim()) return []; return [ { id: `${connectionId}:${name}`, @@ -66,8 +66,8 @@ export function buildDuckDbConnectionTreeNodes(connectionId: string, databases: }); const attachedCatalogNodes = sortSidebarDatabases(databases).flatMap((db) => { - const name = db.name.trim(); - if (!name || name === "main") return []; + const name = db.name; + if (!name.trim() || name === "main") return []; return [ { id: `${connectionId}:${name}`, diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 0da8e5e958..cf8bdb2be3 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -2249,7 +2249,7 @@ export const useConnectionStore = defineStore("connection", () => { setChildren(node, children); await savePersistedConnectionTreeChildren(cacheKey, node.children || children); } else { - const cacheKey = schemaCacheKey(connectionId, "databases"); + const cacheKey = schemaCacheKey(connectionId, "databases-v2"); if (!options?.force) { const cached = await loadPersistedTreeChildren(node, cacheKey); if (cached.hit) { diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index 54b404794a..5d110e455f 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -3103,7 +3103,7 @@ fn base_pool_key_for( if is_single_connection_pool { connection_id.to_string() } else { - match database.map(str::trim).filter(|db| !db.is_empty()) { + match database.filter(|db| !db.trim().is_empty()) { Some(db) => format!("{connection_id}:{db}"), None => connection_id.to_string(), } @@ -4224,6 +4224,22 @@ mod tests { ); } + #[test] + fn database_scoped_pool_keys_preserve_identifier_whitespace() { + assert_eq!( + super::base_pool_key_for(Some(DatabaseType::Mysql), "mysql-conn", Some(" analytics"), false), + "mysql-conn: analytics" + ); + assert_eq!( + super::base_pool_key_for(Some(DatabaseType::Mysql), "mysql-conn", Some("analytics"), false), + "mysql-conn:analytics" + ); + assert_eq!( + super::base_pool_key_for(Some(DatabaseType::Postgres), "pg-conn", Some("analytics "), false), + "pg-conn:analytics " + ); + } + #[test] fn non_agent_single_connection_types_still_share_pool_keys() { assert_eq!( diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 7252319a65..b62585bfdf 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -4564,6 +4564,16 @@ UNIQUE KEY(`tenant_id`, `name``part`) assert_eq!(queries, vec!["USE `db/name`", "SET NAMES utf8mb4", "SET SESSION group_concat_max_len = 1048576"]); } + #[test] + fn mysql_setup_queries_preserve_database_identifier_whitespace() { + let queries = mysql_setup_queries("mysql://root:secret@localhost:3306/%20analytics%20?charset=utf8mb4", &[]); + + assert_eq!( + queries, + vec!["USE ` analytics `", "SET NAMES utf8mb4", "SET SESSION group_concat_max_len = 1048576"] + ); + } + #[test] fn mysql_setup_queries_can_select_database_without_url_path() { let queries = mysql_setup_queries_for_database( diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 806b2bd756..70bbb06120 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -755,11 +755,7 @@ impl ConnectionConfig { } pub fn effective_database(&self) -> Option<&str> { - self.database - .as_deref() - .map(str::trim) - .filter(|database| !database.is_empty()) - .or_else(|| self.default_database()) + self.database.as_deref().filter(|database| !database.trim().is_empty()).or_else(|| self.default_database()) } fn default_database(&self) -> Option<&'static str> { @@ -1956,6 +1952,30 @@ mod tests { } } + #[test] + fn database_identifier_whitespace_is_preserved_and_percent_encoded() { + let mut config = mysql_config("root", "secret", Some(" analytics ")); + + assert_eq!(config.effective_database(), Some(" analytics ")); + assert_eq!( + config.connection_url(), + "mysql://root:secret@10.1.2.3:2883/%20analytics%20?ssl-mode=disabled&charset=utf8mb4" + ); + + config.db_type = DatabaseType::Postgres; + assert_eq!(config.effective_database(), Some(" analytics ")); + assert_eq!(config.connection_url(), "postgres://root:secret@10.1.2.3:2883/%20analytics%20"); + } + + #[test] + fn whitespace_only_database_uses_database_type_default() { + let mut config = mysql_config("root", "secret", Some(" ")); + assert_eq!(config.effective_database(), None); + + config.db_type = DatabaseType::Postgres; + assert_eq!(config.effective_database(), Some("postgres")); + } + fn mongodb_config(username: &str, password: &str, database: Option<&str>) -> ConnectionConfig { let mut config = mysql_config(username, password, database); config.db_type = DatabaseType::MongoDb; diff --git a/packages/app-tests/databaseTree.test.ts b/packages/app-tests/databaseTree.test.ts index 01ded45b3a..ec7d6cca2d 100644 --- a/packages/app-tests/databaseTree.test.ts +++ b/packages/app-tests/databaseTree.test.ts @@ -15,3 +15,16 @@ test("数据库节点按自然名称排序", () => { test("sidebar name sorting uses numeric-aware ordering", () => { assert.deepEqual(sortSidebarNames(["db10", "db2", "db1"]), ["db1", "db2", "db10"]); }); + +test("数据库节点保留名称首尾空格并忽略纯空白名称", () => { + const nodes = buildDatabaseTreeNodes("conn-1", [{ name: " analytics" }, { name: "analytics" }, { name: "archive " }, { name: " " }]); + + assert.deepEqual( + nodes.map((node) => ({ id: node.id, label: node.label, database: node.database })), + [ + { id: "conn-1: analytics", label: " analytics", database: " analytics" }, + { id: "conn-1:analytics", label: "analytics", database: "analytics" }, + { id: "conn-1:archive ", label: "archive ", database: "archive " }, + ], + ); +});