fix(sidebar): refresh local table search index on tree changes
This commit is contained in:
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);
|
||||
|
||||
+2
@@ -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),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user