diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 0fc1bab97..fd5a64a7e 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -134,6 +134,7 @@ interface TreeClipboardTableStructure { interface LoadTreeOptions { force?: boolean; + expectedSidebarSearchQuery?: string; } interface PersistedTreeChildrenLoadResult { @@ -798,12 +799,18 @@ export const useConnectionStore = defineStore("connection", () => { } function refreshStaleTreeNode(node: TreeNode) { + const searchFilter = sidebarSearchQuery.value || ""; + if (searchFilter) return; if (staleTreeRefreshIds.has(node.id)) return; staleTreeRefreshIds.add(node.id); const expandedIds = collectExpandedNodeIds([node]); clearLoadedChildrenCache(node.id); - void loadTreeNodeChildren(node, { force: true }) - .then(() => restoreExpandedChildren(node, expandedIds, { force: true })) + const refreshOptions = { force: true, expectedSidebarSearchQuery: searchFilter }; + void loadTreeNodeChildren(node, refreshOptions) + .then(() => { + if ((sidebarSearchQuery.value || "") !== searchFilter) return; + return restoreExpandedChildren(node, expandedIds, refreshOptions); + }) .finally(() => staleTreeRefreshIds.delete(node.id)); } @@ -834,6 +841,10 @@ export const useConnectionStore = defineStore("connection", () => { return true; } + function isSidebarSearchQueryChanged(options?: LoadTreeOptions) { + return options?.expectedSidebarSearchQuery !== undefined && (sidebarSearchQuery.value || "") !== options.expectedSidebarSearchQuery; + } + function isTreeNodeChildrenLoaded(nodeId: string): boolean { return loadedTreeNodeChildrenIds.value.has(nodeId); } @@ -1332,6 +1343,7 @@ export const useConnectionStore = defineStore("connection", () => { } const [databases, schemas] = await Promise.all([withMetadataLoadTimeout(connectionId, api.listDatabases(connectionId), "databases"), withMetadataLoadTimeout(connectionId, api.listSchemas(connectionId, "main"), "schemas")]); const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node); + if (isSidebarSearchQueryChanged(options)) return; setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); } else if (config && connectionUsesVisibleSchemaFilter(config)) { @@ -1357,6 +1369,7 @@ export const useConnectionStore = defineStore("connection", () => { isExpanded: false, children: [], })); + if (isSidebarSearchQueryChanged(options)) return; setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node)); await savePersistedTreeChildren(cacheKey, schemaNodes); } else { @@ -1399,6 +1412,7 @@ export const useConnectionStore = defineStore("connection", () => { if (linkedServers.length > 0) loadedTreeNodeChildrenIds.value.add(sqlServerLinkedRootId(connectionId)); } const children = withSavedSqlRoot(connectionId, databaseNodes, node); + if (isSidebarSearchQueryChanged(options)) return; setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); } @@ -1793,6 +1807,7 @@ export const useConnectionStore = defineStore("connection", () => { children: [], }; }); + if (isSidebarSearchQueryChanged(options)) return; setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; @@ -1825,6 +1840,7 @@ export const useConnectionStore = defineStore("connection", () => { const config = getConfig(connectionId); const schemas = filterSchemaNamesForConnection(await api.listSchemas(connectionId, database), config, database); const children = buildSqlServerDatabaseTreeNodes(connectionId, database, schemas); + if (isSidebarSearchQueryChanged(options)) return; setChildren(node, children); await savePersistedTreeChildren(cacheKey, children); node.isExpanded = true; @@ -1956,7 +1972,8 @@ export const useConnectionStore = defineStore("connection", () => { if (useCachedChildren(node, options)) return; const simpleObjectDisplay = useSettingsStore().editorSettings.sidebarObjectDisplay === "simple"; const cacheKey = schemaCacheKey(connectionId, database, schema || "", simpleObjectDisplay ? "objects-simple-v3" : "objects-grouped-v3"); - if (!options?.force) { + const searchFilter = sidebarSearchQuery.value || ""; + if (!options?.force && !searchFilter) { const cached = await loadPersistedTreeChildren(node, cacheKey); if (cached.hit) { if (cached.isStale) refreshStaleTreeNode(node); @@ -1980,8 +1997,9 @@ export const useConnectionStore = defineStore("connection", () => { nonTableObjectTypes, offset: 0, pageSize, + searchFilter: searchFilter || undefined, }); - children = page.hasMore && !sidebarSearchQuery.value ? [...page.children, buildLoadMoreNode(node, page.nextOffset, pageSize)] : page.children; + children = page.hasMore && !searchFilter ? [...page.children, buildLoadMoreNode(node, page.nextOffset, pageSize)] : page.children; node.objectCount = page.objectCount; } else { children = buildObjectGroupPlaceholderNodes({ @@ -1992,8 +2010,11 @@ export const useConnectionStore = defineStore("connection", () => { objectTypes: supportedSidebarObjectTypes(config), }); } + if ((sidebarSearchQuery.value || "") !== searchFilter || isSidebarSearchQueryChanged(options)) return; setChildren(node, children); - await savePersistedTreeChildren(cacheKey, children); + if (!searchFilter) { + await savePersistedTreeChildren(cacheKey, children); + } node.isExpanded = true; } catch (e) { recordMetadataLoadError(connectionId, e); @@ -2017,7 +2038,8 @@ export const useConnectionStore = defineStore("connection", () => { const querySchema = connectionObjectTreeQuerySchema(config, node.database, node.schema); const effectiveSchema = connectionObjectTreeNodeSchema(config, node.database, node.schema); const cacheKey = objectGroupCacheKey(node); - if (!options?.force && !sidebarSearchQuery.value) { + const searchFilter = sidebarSearchQuery.value || ""; + if (!options?.force && !searchFilter) { const cached = await loadPersistedTreeChildren(node, cacheKey); if (cached.hit) { if (cached.isStale) refreshStaleTreeNode(node); @@ -2036,8 +2058,9 @@ export const useConnectionStore = defineStore("connection", () => { objectTypes, offset: 0, pageSize: sidebarObjectGroupPageSize(), + searchFilter: searchFilter || undefined, }); - children = page.hasMore && !sidebarSearchQuery.value ? [...page.children, buildLoadMoreNode(node, page.nextOffset, sidebarObjectGroupPageSize())] : page.children; + children = page.hasMore && !searchFilter ? [...page.children, buildLoadMoreNode(node, page.nextOffset, sidebarObjectGroupPageSize())] : page.children; node.objectCount = page.objectCount; } else { const objects = await api.listObjects(node.connectionId, node.database, querySchema, objectTypes); @@ -2050,8 +2073,9 @@ export const useConnectionStore = defineStore("connection", () => { }); node.objectCount = children.length; } + if ((sidebarSearchQuery.value || "") !== searchFilter || isSidebarSearchQueryChanged(options)) return; setChildren(node, children); - if (!sidebarSearchQuery.value) { + if (!searchFilter) { await savePersistedTreeChildren(cacheKey, children); } node.isExpanded = true; diff --git a/crates/dbx-core/src/db/mysql.rs b/crates/dbx-core/src/db/mysql.rs index 2711dd4ef..df31af82c 100644 --- a/crates/dbx-core/src/db/mysql.rs +++ b/crates/dbx-core/src/db/mysql.rs @@ -1154,7 +1154,10 @@ fn filter_list_tables_fallback( tables .into_iter() - .filter(|table| crate::sql::contains_or_fuzzy_match(&table.name, filter)) + .filter(|table| { + crate::sql::contains_or_fuzzy_match(&table.name, filter) + || table.comment.as_deref().is_some_and(|comment| crate::sql::contains_or_fuzzy_match(comment, filter)) + }) .filter(|table| if table.table_type.eq_ignore_ascii_case("VIEW") { wants_view } else { wants_table }) .skip(offset.unwrap_or(0)) .take(limit.unwrap_or(usize::MAX)) @@ -1196,12 +1199,18 @@ fn list_tables_sql( value.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_") }); sql.push_str(&format!( - " AND (LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\' OR LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\')", + " AND (LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\' OR LOWER(TABLE_COMMENT) LIKE {} ESCAPE '\\\\' OR LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\' OR LOWER(TABLE_COMMENT) LIKE {} ESCAPE '\\\\')", quote_value(&pattern), + quote_value(&pattern), + quote_value(&fuzzy_pattern), quote_value(&fuzzy_pattern) )); } else { - sql.push_str(&format!(" AND LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\'", quote_value(&pattern))); + sql.push_str(&format!( + " AND (LOWER(TABLE_NAME) LIKE {} ESCAPE '\\\\' OR LOWER(TABLE_COMMENT) LIKE {} ESCAPE '\\\\')", + quote_value(&pattern), + quote_value(&pattern) + )); } } sql.push_str(" ORDER BY TABLE_NAME"); @@ -3051,7 +3060,9 @@ mod tests { assert!(sql.contains("FROM information_schema.TABLES")); assert!(sql.contains("TABLE_SCHEMA = 'app'")); assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%user\\\\_\\\\%%' ESCAPE '\\\\'")); + assert!(sql.contains("LOWER(TABLE_COMMENT) LIKE '%user\\\\_\\\\%%' ESCAPE '\\\\'")); assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%u%s%e%r%\\\\_%\\\\%%' ESCAPE '\\\\'")); + assert!(sql.contains("LOWER(TABLE_COMMENT) LIKE '%u%s%e%r%\\\\_%\\\\%%' ESCAPE '\\\\'")); assert!(sql.contains("ORDER BY TABLE_NAME")); assert!(sql.contains("LIMIT 101")); assert!(sql.contains("OFFSET 200")); @@ -3062,7 +3073,9 @@ mod tests { let sql = list_tables_sql("app", Some("sysu"), Some(100), None, None); assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%sysu%' ESCAPE '\\\\'")); + assert!(sql.contains("LOWER(TABLE_COMMENT) LIKE '%sysu%' ESCAPE '\\\\'")); assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%s%y%s%u%' ESCAPE '\\\\'")); + assert!(sql.contains("LOWER(TABLE_COMMENT) LIKE '%s%y%s%u%' ESCAPE '\\\\'")); } #[test] @@ -3070,7 +3083,9 @@ mod tests { let sql = list_tables_sql("app", Some("u"), Some(100), None, None); assert!(sql.contains("LOWER(TABLE_NAME) LIKE '%u%' ESCAPE '\\\\'")); + assert!(sql.contains("LOWER(TABLE_COMMENT) LIKE '%u%' ESCAPE '\\\\'")); assert_eq!(sql.matches("LOWER(TABLE_NAME) LIKE").count(), 1); + assert_eq!(sql.matches("LOWER(TABLE_COMMENT) LIKE").count(), 1); assert!(!sql.contains(" OR LOWER(TABLE_NAME) LIKE")); } @@ -3116,7 +3131,7 @@ mod tests { TableInfo { name: "audit_2025".to_string(), table_type: "BASE TABLE".to_string(), - comment: None, + comment: Some("purchase order history".to_string()), parent_schema: None, parent_name: None, }, @@ -3124,6 +3139,17 @@ mod tests { let filtered = filter_list_tables_fallback(rows, Some("audit"), Some(1), Some(1), Some(&["TABLE".to_string()])); assert_eq!(filtered.iter().map(|table| table.name.as_str()).collect::>(), vec!["audit_2025"]); + + let rows = vec![TableInfo { + name: "t_0001".to_string(), + table_type: "BASE TABLE".to_string(), + comment: Some("food orders".to_string()), + parent_schema: None, + parent_name: None, + }]; + let filtered = filter_list_tables_fallback(rows, Some("ood"), None, None, Some(&["TABLE".to_string()])); + + assert_eq!(filtered.iter().map(|table| table.name.as_str()).collect::>(), vec!["t_0001"]); } #[test] diff --git a/packages/app-tests/sidebarSearchTree.test.ts b/packages/app-tests/sidebarSearchTree.test.ts index 97d451820..10b5eee5a 100644 --- a/packages/app-tests/sidebarSearchTree.test.ts +++ b/packages/app-tests/sidebarSearchTree.test.ts @@ -84,6 +84,33 @@ test("preserves loaded schema children when the database itself matches search", assert.equal(filtered[0]?.children?.[0]?.label, "public"); }); +test("matches table comments during sidebar search", () => { + const nodes: TreeNode[] = [ + { + id: "conn:db", + label: "app", + type: "database", + connectionId: "conn", + database: "app", + isExpanded: true, + children: [ + { + id: "conn:db:inventory", + label: "inventory", + type: "table", + connectionId: "conn", + database: "app", + comment: "purchase order history", + }, + ], + }, + ]; + + const filtered = filterSidebarTree(nodes, "purchase", new Set()); + + assert.equal(filtered[0]?.children?.[0]?.label, "inventory"); +}); + test("search scope excludes non-selected node self matches", () => { const nodes: TreeNode[] = [ {