fix(sidebar): preserve comment-matched tables during remote table list refresh
This commit is contained in:
parent
0fb39cb33f
commit
56fb49058f
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue