fix(editor): refresh diagnostics after structure saves

This commit is contained in:
t8y2 committed 2026-08-02 19:46:44 +08:00
1 parent b75e7094c2
commit 2aeafa136c
8 files changed
+139 -1

No files matched your search

@@ -391,6 +391,78 @@ describe("connectionStore completion assistant", () => {
expect(quoted).toEqual([expect.objectContaining({ name: "MixedColumn", table: "MixedTable", schema: "MixedSchema" })]);
});
it("invalidates only the changed table completion metadata", async () => {
const getColumns = vi.fn(async (_connectionId: string, _database: string, _schema: string, table: string) => [
{
name: `${table}_column_${getColumns.mock.calls.length}`,
data_type: "integer",
is_nullable: false,
column_default: null,
is_primary_key: false,
extra: null,
},
]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch: vi.fn().mockRejectedValue(new Error("assistant unavailable")),
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [sqlServerConnection()];
store.connectedIds.add("sqlserver-1");
await store.listCompletionColumns("sqlserver-1", "app", "users", "dbo");
await store.listCompletionColumns("sqlserver-1", "app", "orders", "dbo");
await store.listCompletionColumns("sqlserver-1", "app", "users", "dbo");
await store.listCompletionColumns("sqlserver-1", "app", "orders", "dbo");
expect(getColumns.mock.calls.map((call) => call[3])).toEqual(["users", "orders"]);
expect(store.invalidateCompletionTableCache("sqlserver-1", "app", "users", "dbo")).toBeGreaterThan(0);
await store.listCompletionColumns("sqlserver-1", "app", "users", "dbo");
await store.listCompletionColumns("sqlserver-1", "app", "orders", "dbo");
expect(getColumns.mock.calls.map((call) => call[3])).toEqual(["users", "orders", "users"]);
});
it("keeps the same table cached in other catalogs", async () => {
const getColumns = vi.fn(async (_connectionId: string, _database: string, _schema: string, table: string, catalog?: string) => [
{
name: `${catalog}_${table}`,
data_type: "integer",
is_nullable: false,
column_default: null,
is_primary_key: false,
extra: null,
},
]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch: vi.fn(),
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [dorisConnection()];
store.connectedIds.add("doris-1");
await store.listCompletionColumns("doris-1", "sales", "users", undefined, undefined, "internal");
await store.listCompletionColumns("doris-1", "sales", "users", undefined, undefined, "hive");
expect(getColumns.mock.calls.map((call) => call[4])).toEqual(["internal", "hive"]);
expect(store.invalidateCompletionTableCache("doris-1", "sales", "users", undefined, "internal")).toBeGreaterThan(0);
await store.listCompletionColumns("doris-1", "sales", "users", undefined, undefined, "internal");
await store.listCompletionColumns("doris-1", "sales", "users", undefined, undefined, "hive");
expect(getColumns.mock.calls.map((call) => call[4])).toEqual(["internal", "hive", "internal"]);
});
it("keeps quoted and unquoted Oracle objects separate in the local column index", async () => {
const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null }) => ({
candidates: [
@@ -5293,6 +5293,46 @@ export const useConnectionStore = defineStore("connection", () => {
return `${completionScopeKey(connectionId, database, schema)}:${table.toLowerCase()}:fkeys`;
}
function completionTableCacheKeyMatches(key: string, connectionId: string, database: string, tableName: string, schema?: string, catalog?: string): boolean {
const normalizedKey = key.toLowerCase();
const prefix = `${connectionId}:${database}:`.toLowerCase();
if (!normalizedKey.startsWith(prefix)) return false;
const tableToken = `:${tableName.toLowerCase()}`;
const tableOffset = normalizedKey.lastIndexOf(tableToken);
if (tableOffset < prefix.length) return false;
const trailing = normalizedKey.slice(tableOffset + tableToken.length);
if (trailing && !trailing.startsWith(":")) return false;
const normalizedSchema = schema?.trim().toLowerCase();
const normalizedCatalog = catalog?.trim().toLowerCase();
const scope = normalizedKey.slice(prefix.length, tableOffset);
if (normalizedCatalog) {
const catalogScope = `${normalizedCatalog}:${normalizedSchema ?? ""}`;
return scope === catalogScope || (!!normalizedSchema && scope === normalizedSchema);
}
if (!normalizedSchema) return true;
return scope === normalizedSchema || scope.endsWith(`:${normalizedSchema}`);
}
function invalidateCompletionTableCache(connectionId: string, database: string, tableName: string, schema?: string, catalog?: string): number {
const matches = (key: string) => completionTableCacheKeyMatches(key, connectionId, database, tableName, schema, catalog);
let removed = 0;
for (const cache of [completionColumnsCache.value, completionForeignKeysCache.value]) {
for (const key of Object.keys(cache)) {
if (!matches(key)) continue;
delete cache[key];
removed++;
}
}
for (const cache of [completionColumnIndex, completionForeignKeyIndex, completionInFlight]) {
for (const key of cache.keys()) {
if (!matches(key)) continue;
cache.delete(key);
removed++;
}
}
return removed;
}
function touchCompletionIndex<T>(index: Map<string, { touched: number } & T>, key: string, value: T, max = COMPLETION_CACHE_MAX) {
index.set(key, { ...value, touched: Date.now() });
if (index.size <= max) return;
@@ -6854,6 +6894,7 @@ export const useConnectionStore = defineStore("connection", () => {
listMongoCompletionCollections,
listMongoCompletionFields,
invalidateCompletionCache,
invalidateCompletionTableCache,
invalidateMetadataCache,
exportConnectionsToFile,
readImportFile,
+4
View File
@@ -668,6 +668,10 @@ export const useQueryStore = defineStore("query", () => {
...tableStructureRefreshVersions.value,
[key]: (tableStructureRefreshVersions.value[key] ?? 0) + 1,
};
for (const tab of tabs.value) {
if (tab.mode !== "query" || tab.connectionId !== connectionId || tab.database !== database) continue;
tab.completionContextVersion = (tab.completionContextVersion ?? 0) + 1;
}
}
function tableStructureRefreshVersion(connectionId: string, database: string, schema: string | undefined, tableName: string): number {