fix(sidebar): refresh local table search index on tree changes

This commit is contained in:
zipg authored and GitHub committed 2026-08-24 18:37:13 +08:00
1 parent 4a478acf07
commit 4021216155
8 files changed
+495 -42

No files matched your search

@@ -13,11 +13,13 @@ import {
filterSidebarSearchRootsByConnectionState,
filterSidebarTree,
filterSidebarTreeToConnectedConnections,
findNodePathByIdentity,
mergeSidebarRegexIndexScopes,
resolveSidebarFilterGuards,
resolveSidebarObjectSearchFilter,
reuseLiveSidebarTreeNodes,
type SidebarRegexIndexScope,
type SidebarRegexScopeIdentity,
} from "@/lib/sidebar/sidebarSearchTree";
import { createSidebarLabelMatcher, matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
import { collectSidebarRegexIndexScopes, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode } from "@/lib/sidebar/sidebarRegexSearchIndex";
@@ -51,7 +53,7 @@ import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHostInstance } from "@/lib/sidebar/sidebarTreeRuntime";
import { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry";
import { insertSidebarTableSearchControls, isSidebarTableSearchControlNode } from "@/lib/sidebar/sidebarTableSearchControl";
import { createSidebarTableSearchDebouncer, loadOrBuildSidebarTableSearchIndex, scheduleExclusiveSidebarTableSearchDebounce } from "@/lib/sidebar/sidebarTableSearchIndex";
import { createSidebarTableSearchDebouncer, invalidateSidebarTableSearchBuild, loadOrBuildSidebarTableSearchIndex, scheduleExclusiveSidebarTableSearchDebounce } from "@/lib/sidebar/sidebarTableSearchIndex";
import TreeItem from "./TreeItem.vue";
import SidebarTreeRuntimeHost from "./SidebarTreeRuntimeHost.vue";
import SidebarTreeItemDialogs from "./SidebarTreeItemDialogs.vue";
@@ -579,6 +581,9 @@ function restoreTableSearchInput(focusRestore: TableSearchFocusRestore) {
const displayedTreeNodes = computed(() => sortConnectionListForDisplay(store.treeNodes, settingsStore.editorSettings.sidebarConnectionSortMode));
const localTableSearchResults = ref<Record<string, TableInfo[] | null>>({});
const localTableSearchRequestRevisions = new Map<string, number>();
type InvalidatedTableSearchScope = SidebarRegexScopeIdentity & { parentNodeId: string };
const pendingInvalidatedTableSearchScopes = new Map<string, InvalidatedTableSearchScope>();
const regexTableSearchScopes = shallowRef<SidebarRegexIndexScope[]>([]);
const localTableSearchParentTypes = new Set<TreeNodeType>(["database", "schema", "linked-server-schema", "group-tables"]);
@@ -636,7 +641,13 @@ function scheduleLocalSidebarTableSearchRefresh(parentNodeId: string, focusResto
});
}
async function loadLocalTableSearchResults(parentNodeId: string, refresh = false, focusRestore?: TableSearchFocusRestore) {
function invalidatedTableSearchScopeKey(scope: InvalidatedTableSearchScope): string {
return [scope.connectionId, scope.catalog ?? "", scope.database, scope.schema ?? "", scope.nodeType, scope.parentNodeId].join("\0");
}
async function loadLocalTableSearchResults(parentNodeId: string, refresh = false, focusRestore?: TableSearchFocusRestore, identity?: SidebarRegexScopeIdentity) {
const requestRevision = (localTableSearchRequestRevisions.get(parentNodeId) ?? 0) + 1;
localTableSearchRequestRevisions.set(parentNodeId, requestRevision);
try {
// A missing persisted index (null) means this scope was never indexed, so
// only the currently loaded first page of children is searchable. That
@@ -649,16 +660,49 @@ async function loadLocalTableSearchResults(parentNodeId: string, refresh = false
const entries = await loadOrBuildSidebarTableSearchIndex(
parentNodeId,
query,
() => store.loadSidebarTableSearchIndex(parentNodeId),
() => store.refreshSidebarTableSearchIndex(parentNodeId),
() => store.loadSidebarTableSearchIndex(parentNodeId, identity),
() => store.refreshSidebarTableSearchIndex(parentNodeId, identity),
refresh,
);
if (localTableSearchRequestRevisions.get(parentNodeId) !== requestRevision) return;
if (query || refresh) localTableSearchResults.value = { ...localTableSearchResults.value, [parentNodeId]: entries };
} finally {
if (focusRestore) restoreTableSearchInput(focusRestore);
}
}
function refreshPendingInvalidatedTableSearchScopes() {
for (const [scopeKey, scope] of pendingInvalidatedTableSearchScopes) {
const query = store.sidebarTableSearchQueries[scope.parentNodeId]?.trim() ?? "";
if (!settingsStore.editorSettings.sidebarTableSearchLocal || !query) {
pendingInvalidatedTableSearchScopes.delete(scopeKey);
continue;
}
if (!store.connectedIds.has(scope.connectionId)) continue;
if (!findNodePathByIdentity(store.treeNodes, scope.parentNodeId, scope)) continue;
pendingInvalidatedTableSearchScopes.delete(scopeKey);
void loadLocalTableSearchResults(scope.parentNodeId, true, undefined, scope).catch((error) => {
console.debug("[DBX][sidebar-table-search:index-rebuild-failed]", { scope, error });
});
}
}
watch(
() => store.sidebarTableSearchIndexInvalidation,
(invalidation) => {
if (!invalidation.scopes.length) return;
const nextResults = { ...localTableSearchResults.value };
for (const scope of invalidation.scopes) {
delete nextResults[scope.parentNodeId];
localTableSearchDebouncer.cancel(scope.parentNodeId);
invalidateSidebarTableSearchBuild(scope.parentNodeId);
pendingInvalidatedTableSearchScopes.set(invalidatedTableSearchScopeKey(scope), scope);
}
localTableSearchResults.value = nextResults;
refreshPendingInvalidatedTableSearchScopes();
},
);
const filteredNodes = computed(() => {
let nodes = displayedTreeNodes.value;
if (showConnectedConnectionsOnly.value) {
@@ -701,6 +745,8 @@ const flatNodes = computed<FlatTreeNode[]>(() =>
}),
);
watch(flatNodes, refreshPendingInvalidatedTableSearchScopes, { flush: "post" });
const sidebarCommentLabelWidths = shallowRef(new Map<string, number>());
let sidebarCommentMeasureFrame = 0;
const sidebarTreeContentWidth = ref(0);
@@ -2156,6 +2202,8 @@ onUnmounted(() => {
cancelPendingSidebarDataOpen();
remoteTableSearchDebouncer.cancelAll();
localTableSearchDebouncer.cancelAll();
localTableSearchRequestRevisions.clear();
pendingInvalidatedTableSearchScopes.clear();
tableSearchFocusRestoreTokens.clear();
latestTableSearchInteractionParentId = null;
latestTableSearchInteractionId = 0;
@@ -19,14 +19,26 @@ import type { TableInfo } from "@/types/database";
*/
const inFlightSidebarTableSearchBuilds = new Map<string, Promise<TableInfo[]>>();
/**
* Stop a later explicit refresh from reusing a build that started before the
* underlying table list was refreshed. The old promise may still settle, but
* callers can start a new build for the same scope immediately.
*/
export function invalidateSidebarTableSearchBuild(scopeKey: string): void {
inFlightSidebarTableSearchBuilds.delete(scopeKey);
}
function dedupeInFlightBuild(scopeKey: string, build: () => Promise<TableInfo[]>): Promise<TableInfo[]> {
const existing = inFlightSidebarTableSearchBuilds.get(scopeKey);
if (existing) return existing;
const pending = (async () => {
let pending!: Promise<TableInfo[]>;
pending = (async () => {
try {
return await build();
} finally {
inFlightSidebarTableSearchBuilds.delete(scopeKey);
if (inFlightSidebarTableSearchBuilds.get(scopeKey) === pending) {
inFlightSidebarTableSearchBuilds.delete(scopeKey);
}
}
})();
inFlightSidebarTableSearchBuilds.set(scopeKey, pending);
@@ -42,7 +42,9 @@ describe("connectionStore disconnect data-tab metadata freshness", () => {
checkConnectionHealth,
disconnectDb,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
@@ -175,7 +175,14 @@ describe("connectionStore metadata loading", () => {
fallback_used: false,
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), completionAssistantSearch, deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined), listInstalledAgents: vi.fn().mockResolvedValue([]) }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
listInstalledAgents: vi.fn().mockResolvedValue([]),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
@@ -314,6 +321,8 @@ describe("connectionStore metadata loading", () => {
completionAssistantSearch,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
getCustomTypeDetails,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
@@ -674,7 +674,9 @@ describe("connectionStore timeout recovery", () => {
connectDb,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
disconnectDb,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
@@ -736,7 +738,9 @@ describe("connectionStore timeout recovery", () => {
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
disconnectDb,
listInstalledAgents: vi.fn().mockResolvedValue([]),
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
@@ -776,7 +780,9 @@ describe("connectionStore timeout recovery", () => {
connectDb,
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
disconnectDb,
loadSchemaCache: vi.fn().mockResolvedValue(null),
saveConnections: vi.fn().mockResolvedValue(undefined),
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
}));
+167 -21
View File
@@ -461,6 +461,12 @@ export const useConnectionStore = defineStore("connection", () => {
const sidebarTableSearchIndexCache = new Map<string, TableInfo[] | null>();
const sidebarTableSearchIndexInFlight = new Map<string, Promise<TableInfo[] | null>>();
const sidebarTableSearchIndexConnectionGenerations = new Map<string, number>();
const sidebarTableSearchIndexScopeGenerations = new Map<string, number>();
const sidebarTableSearchIndexPersistenceQueues = new Map<string, Promise<void>>();
const sidebarTableSearchIndexInvalidation = ref<{
revision: number;
scopes: Array<SidebarRegexScopeIdentity & { parentNodeId: string }>;
}>({ revision: 0, scopes: [] });
let sidebarTableSearchIndexManifest: TableSearchIndexManifestEntry[] | null = null;
let sidebarTableSearchIndexManifestInFlight: Promise<TableSearchIndexManifestEntry[]> | null = null;
let sidebarTableSearchIndexManifestWriteQueue: Promise<void> = Promise.resolve();
@@ -2398,6 +2404,109 @@ export const useConnectionStore = defineStore("connection", () => {
return sidebarTableSearchIndexConnectionGenerations.get(connectionId) ?? 0;
}
function sidebarTableSearchIndexScopeGeneration(cacheKey: string): number {
return sidebarTableSearchIndexScopeGenerations.get(cacheKey) ?? 0;
}
function sidebarTableSearchIndexGenerationIsCurrent(connectionId: string, cacheKey: string, connectionGeneration: number, scopeGeneration: number): boolean {
return connectionGeneration === sidebarTableSearchIndexConnectionGeneration(connectionId) && scopeGeneration === sidebarTableSearchIndexScopeGeneration(cacheKey);
}
async function serializeSidebarTableSearchIndexPersistence(cacheKey: string, operation: () => Promise<void>): Promise<void> {
const previous = sidebarTableSearchIndexPersistenceQueues.get(cacheKey) ?? Promise.resolve();
const pending = previous.catch(() => undefined).then(operation);
sidebarTableSearchIndexPersistenceQueues.set(cacheKey, pending);
try {
await pending;
} finally {
if (sidebarTableSearchIndexPersistenceQueues.get(cacheKey) === pending) {
sidebarTableSearchIndexPersistenceQueues.delete(cacheKey);
}
}
}
type SidebarTableSearchIndexScope = SidebarRegexScopeIdentity & {
parentNodeId: string;
cacheKey: string;
};
function sidebarTableSearchIndexScopeForNode(parent: TreeNode): SidebarTableSearchIndexScope | null {
if (!parent.connectionId || !parent.database) return null;
const cacheKey = sidebarTableSearchIndexCacheKey(parent);
if (!cacheKey) return null;
return {
parentNodeId: parent.id,
connectionId: parent.connectionId,
database: parent.database,
schema: parent.schema,
catalog: parent.catalog,
nodeType: parent.type,
cacheKey,
};
}
function manifestTableSearchScopeMatchesTreeRefresh(scope: TableSearchIndexManifestEntry, node: TreeNode): boolean {
if (!node.connectionId || scope.connectionId !== node.connectionId) return false;
const pathMatch = scope.path?.some((pathNode) => pathNode.id === node.id && pathNode.type === node.type && pathNode.connectionId === node.connectionId);
if (pathMatch) return true;
if (node.type === "connection") return true;
if (node.type === "doris-catalog") return scope.catalog === node.catalog;
if (node.type === "database") return scope.database === node.database && scope.catalog === node.catalog;
if (node.type === "schema") return scope.database === node.database && scope.schema === node.schema && scope.catalog === node.catalog;
if (node.type === "linked-server-schema") return scope.parentNodeId === node.id && scope.nodeType === node.type;
if (node.type === "group-tables") return scope.parentNodeId === node.id && scope.nodeType === node.type;
return false;
}
function collectActiveTableSearchScopes(node: TreeNode): SidebarTableSearchIndexScope[] {
const scopes: SidebarTableSearchIndexScope[] = [];
const visit = (current: TreeNode) => {
if (sidebarTableSearchQueries.value[current.id]?.trim()) {
const scope = sidebarTableSearchIndexScopeForNode(current);
if (scope) scopes.push(scope);
}
for (const child of current.children ?? []) visit(child);
};
visit(node);
return scopes;
}
function publishSidebarTableSearchIndexInvalidation(scopes: SidebarTableSearchIndexScope[]) {
if (scopes.length === 0) return;
sidebarTableSearchIndexInvalidation.value = {
revision: sidebarTableSearchIndexInvalidation.value.revision + 1,
scopes: scopes.map(({ cacheKey: _cacheKey, ...scope }) => scope),
};
}
async function invalidateSidebarTableSearchIndexScopes(scopes: SidebarTableSearchIndexScope[], removeManifestScope: (scope: TableSearchIndexManifestEntry) => boolean): Promise<void> {
const uniqueScopes = [...new Map(scopes.map((scope) => [scope.cacheKey, scope])).values()];
if (uniqueScopes.length === 0) return;
const cacheKeys = new Set(uniqueScopes.map((scope) => scope.cacheKey));
for (const cacheKey of cacheKeys) {
sidebarTableSearchIndexScopeGenerations.set(cacheKey, sidebarTableSearchIndexScopeGeneration(cacheKey) + 1);
sidebarTableSearchIndexCache.delete(cacheKey);
sidebarTableSearchIndexInFlight.delete(cacheKey);
}
const deletePersisted = Promise.all(
[...cacheKeys].map((cacheKey) =>
serializeSidebarTableSearchIndexPersistence(cacheKey, async () => {
await api.deleteSchemaCachePrefix(cacheKey).catch(() => undefined);
}),
),
);
sidebarTableSearchIndexManifestWriteQueue = sidebarTableSearchIndexManifestWriteQueue.then(async () => {
const manifest = await loadSidebarTableSearchIndexManifest();
const nextManifest = manifest.filter((scope) => !cacheKeys.has(scope.cacheKey) && !removeManifestScope(scope));
if (nextManifest.length === manifest.length) return;
sidebarTableSearchIndexManifest = nextManifest;
await api.saveSchemaCache(sidebarTableSearchIndexManifestCacheKey, encodeTableSearchIndexManifest(nextManifest)).catch(() => undefined);
});
await Promise.all([deletePersisted, sidebarTableSearchIndexManifestWriteQueue]);
publishSidebarTableSearchIndexInvalidation(uniqueScopes);
}
async function invalidateSidebarTableSearchIndexesForConnection(connectionId: string): Promise<void> {
sidebarTableSearchIndexConnectionGenerations.set(connectionId, sidebarTableSearchIndexConnectionGeneration(connectionId) + 1);
const rawPrefix = `${connectionId}:`;
@@ -2409,14 +2518,38 @@ export const useConnectionStore = defineStore("connection", () => {
for (const cacheKey of sidebarTableSearchIndexInFlight.keys()) {
if (matchesConnectionCacheKey(cacheKey)) sidebarTableSearchIndexInFlight.delete(cacheKey);
}
sidebarTableSearchIndexManifestWriteQueue = sidebarTableSearchIndexManifestWriteQueue.then(async () => {
const manifest = await loadSidebarTableSearchIndexManifest();
const nextManifest = manifest.filter((scope) => scope.connectionId !== connectionId);
if (nextManifest.length === manifest.length) return;
sidebarTableSearchIndexManifest = nextManifest;
await api.saveSchemaCache(sidebarTableSearchIndexManifestCacheKey, encodeTableSearchIndexManifest(nextManifest)).catch(() => undefined);
});
await sidebarTableSearchIndexManifestWriteQueue;
const manifest = await loadSidebarTableSearchIndexManifest();
const manifestScopes = manifest
.filter((scope) => scope.connectionId === connectionId)
.map((scope) => ({
parentNodeId: scope.parentNodeId,
connectionId: scope.connectionId,
database: scope.database,
schema: scope.schema,
catalog: scope.catalog,
nodeType: scope.nodeType,
cacheKey: scope.cacheKey,
}));
const connectionNode = findConnectionNode(connectionId);
const activeScopes = connectionNode ? collectActiveTableSearchScopes(connectionNode) : [];
await invalidateSidebarTableSearchIndexScopes([...manifestScopes, ...activeScopes], (scope) => scope.connectionId === connectionId);
}
async function invalidateSidebarTableSearchIndexesForTreeRefresh(node: TreeNode): Promise<void> {
const manifest = await loadSidebarTableSearchIndexManifest();
const manifestScopes = manifest
.filter((scope) => manifestTableSearchScopeMatchesTreeRefresh(scope, node))
.map((scope) => ({
parentNodeId: scope.parentNodeId,
connectionId: scope.connectionId,
database: scope.database,
schema: scope.schema,
catalog: scope.catalog,
nodeType: scope.nodeType,
cacheKey: scope.cacheKey,
}));
const activeScopes = collectActiveTableSearchScopes(node);
await invalidateSidebarTableSearchIndexScopes([...manifestScopes, ...activeScopes], (scope) => manifestTableSearchScopeMatchesTreeRefresh(scope, node));
}
async function registerSidebarTableSearchIndexScope(parent: TreeNode, cacheKey: string): Promise<void> {
@@ -2439,12 +2572,13 @@ export const useConnectionStore = defineStore("connection", () => {
if (sidebarTableSearchIndexCache.has(cacheKey)) return sidebarTableSearchIndexCache.get(cacheKey) ?? null;
const pending = sidebarTableSearchIndexInFlight.get(cacheKey);
if (pending) return pending;
const generation = sidebarTableSearchIndexConnectionGeneration(connectionId);
const connectionGeneration = sidebarTableSearchIndexConnectionGeneration(connectionId);
const scopeGeneration = sidebarTableSearchIndexScopeGeneration(cacheKey);
const read = (async () => {
const decoded = decodeSchemaTreeCache<TreeNode[]>(await api.loadSchemaCache<unknown>(cacheKey).catch(() => null));
const index = decoded?.tableSearchIndex;
const entries = index ? index.entries.map((entry) => ({ name: entry.name, table_type: entry.tableType, ...(entry.comment !== undefined ? { comment: entry.comment } : {}) })) : null;
if (generation !== sidebarTableSearchIndexConnectionGeneration(connectionId)) return null;
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return null;
sidebarTableSearchIndexCache.set(cacheKey, entries);
return entries;
})();
@@ -2486,18 +2620,20 @@ export const useConnectionStore = defineStore("connection", () => {
async function refreshSidebarTableSearchIndex(parentNodeId: string, identity?: SidebarRegexScopeIdentity): Promise<TableInfo[]> {
const parent = identity ? findSidebarTreeNodeByIdentity(parentNodeId, identity) : findNode(treeNodes.value, parentNodeId);
if (!parent?.connectionId || !hasTreeNodeDatabaseContext(parent)) return [];
const connectionId = parent.connectionId;
const cacheKey = sidebarTableSearchIndexCacheKey(parent);
if (!cacheKey) return [];
const generation = sidebarTableSearchIndexConnectionGeneration(parent.connectionId);
await ensureConnected(parent.connectionId);
const config = getConfig(parent.connectionId);
const connectionGeneration = sidebarTableSearchIndexConnectionGeneration(connectionId);
const scopeGeneration = sidebarTableSearchIndexScopeGeneration(cacheKey);
await ensureConnected(connectionId);
const config = getConfig(connectionId);
const querySchema = connectionObjectTreeQuerySchema(config, parent.database, parent.schema);
const objectTypes = parent.type === "group-tables" ? (objectTypesForGroupNode(parent.type) ?? undefined) : undefined;
const pageSize = sidebarObjectGroupPageSize();
const entries: TableInfo[] = [];
for (let offset = 0; ; offset += pageSize) {
if (generation !== sidebarTableSearchIndexConnectionGeneration(parent.connectionId)) return [];
const page = await listTablesWithOptionalTableNameFilter(parent.connectionId, parent.database, querySchema, undefined, pageSize, offset, objectTypes, parent.catalog);
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return [];
const page = await listTablesWithOptionalTableNameFilter(connectionId, parent.database, querySchema, undefined, pageSize, offset, objectTypes, parent.catalog);
entries.push(...page);
if (page.length < pageSize) break;
}
@@ -2507,12 +2643,18 @@ export const useConnectionStore = defineStore("connection", () => {
indexedAt: new Date().toISOString(),
entries: deduped.map((entry) => ({ name: entry.name, tableType: entry.table_type, ...(entry.comment !== undefined ? { comment: entry.comment } : {}) })),
};
if (generation !== sidebarTableSearchIndexConnectionGeneration(parent.connectionId)) return [];
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache<TreeNode[]>([], Date.now(), tableSearchIndex));
if (generation !== sidebarTableSearchIndexConnectionGeneration(parent.connectionId)) {
await api.deleteSchemaCachePrefix(cacheKey).catch(() => undefined);
return [];
}
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return [];
let persisted = false;
await serializeSidebarTableSearchIndexPersistence(cacheKey, async () => {
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return;
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache<TreeNode[]>([], Date.now(), tableSearchIndex));
if (!sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) {
await api.deleteSchemaCachePrefix(cacheKey).catch(() => undefined);
return;
}
persisted = true;
});
if (!persisted || !sidebarTableSearchIndexGenerationIsCurrent(connectionId, cacheKey, connectionGeneration, scopeGeneration)) return [];
sidebarTableSearchIndexCache.set(cacheKey, deduped);
await registerSidebarTableSearchIndexScope(parent, cacheKey);
return deduped;
@@ -3541,6 +3683,7 @@ export const useConnectionStore = defineStore("connection", () => {
}
invalidateCompletionCache(connectionId);
invalidateObjectBrowserRowsCache({ connectionId });
await invalidateSidebarTableSearchIndexesForConnection(connectionId);
const { useQueryStore } = await import("@/stores/queryStore");
const queryStore = useQueryStore();
// 断开连接是明确的元数据新鲜度边界:数据标签页保留展示/编辑状态,但
@@ -6305,6 +6448,7 @@ export const useConnectionStore = defineStore("connection", () => {
if (objectTypesForGroupNode(node.type)) {
clearLoadedChildrenCache(node.id, { deletePersisted: false });
await loadObjectGroupChildren(node, { force: true });
await invalidateSidebarTableSearchIndexesForTreeRefresh(node);
return;
}
@@ -6339,6 +6483,7 @@ export const useConnectionStore = defineStore("connection", () => {
await loadTreeNodeChildren(node, { force: true });
if (isCurrentRefresh()) {
await restoreExpandedChildren(node, expandedIds, { force: true }, isCurrentRefresh);
if (isCurrentRefresh()) await invalidateSidebarTableSearchIndexesForTreeRefresh(node);
}
} catch (error) {
// A stale failure must never overwrite a newer successful (including empty) result.
@@ -8616,6 +8761,7 @@ export const useConnectionStore = defineStore("connection", () => {
databaseExportSource,
sidebarSearchQuery,
sidebarTableSearchQueries,
sidebarTableSearchIndexInvalidation,
sidebarTableNameFilters,
tableNameFilterScopeKey,
tableNameFilterForScope,