fix(sidebar): preserve comment-matched tables during remote table list refresh

This commit is contained in:
zipg 2026-06-28 18:52:40 +08:00 committed by GitHub
parent 0fb39cb33f
commit 56fb49058f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 89 additions and 12 deletions

View File

@ -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;

View File

@ -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<_>>(), 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<_>>(), vec!["t_0001"]);
}
#[test]

View File

@ -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[] = [
{