From fd443318764cf1c219ca5ba32460a40ab3610872 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 9 Sep 2026 16:05:43 +0800 Subject: [PATCH] fix(editor): verify current-schema tables in semantic diagnosis --- .../src/components/editor/QueryEditor.vue | 4 ++- .../__tests__/database/jdbcDialect.spec.ts | 4 +-- .../connectionStore.completion.spec.ts | 28 +++++++++++++++++++ apps/desktop/src/stores/connectionStore.ts | 15 ++++++++-- packages/app-tests/queryStore.test.ts | 10 +++++-- 5 files changed, 53 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 8963e62ecb..9bdc1e73de 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -2624,7 +2624,9 @@ async function findExactSemanticDiagnosticTable(table: SqlTableReference, scope? return localExact; } - const remoteMatches = await connectionStore.listCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema, false, scope?.schema ?? props.schema, target.catalog); + const remoteMatches = await connectionStore.listCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema, false, scope?.schema ?? props.schema, target.catalog, { + verifySchemaMetadata: !!target.schema, + }); cachedTables = mergeCompletionTables(cachedTables, remoteMatches); return remoteMatches.find((item) => completionTablesMatch(item, table)) ?? null; } diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index de2c169b22..b7f5f3210b 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -364,8 +364,8 @@ describe("query execution schema", () => { expect(connectionQueryExecutionSchema({ db_type: dbType }, "ai_test", undefined, false)).toBe("ai_test"); }); - it("prefers an explicit schema for PostgreSQL", () => { - expect(connectionQueryExecutionSchema({ db_type: "postgres" }, "app", "reporting", false)).toBe("reporting"); + it.each(["postgres", "gaussdb", "opengauss"] as const)("prefers an explicit schema for %s", (dbType) => { + expect(connectionQueryExecutionSchema({ db_type: dbType }, "app", "reporting", false)).toBe("reporting"); }); it("prefers an explicit schema for Kingbase query execution", () => { diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index ec63e24a5f..43f8334aab 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -403,6 +403,34 @@ describe("connectionStore completion assistant", () => { expect(tables).toEqual([{ name: "accounts", schema: "public", type: "table", detail: "→ Customer accounts" }]); }); + it("verifies selected-schema tables when the completion index is stale", async () => { + const completionAssistantSearch = vi.fn().mockResolvedValue({ + candidates: [], + incomplete: false, + fallback_used: false, + }); + const listTables = vi.fn().mockResolvedValue([{ name: "accounts", table_type: "BASE TABLE", comment: "Customer accounts" }]); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + listSchemas: vi.fn().mockResolvedValue(["reporting"]), + listTables, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [postgresConnection()]; + store.connectedIds.add("pg-1"); + + const tables = await store.listCompletionTables("pg-1", "app", "accounts", 200, "reporting", false, "reporting", undefined, { verifySchemaMetadata: true }); + + expect(completionAssistantSearch).toHaveBeenCalledOnce(); + expect(listTables).toHaveBeenCalledWith("pg-1", "app", "reporting", "accounts", 200); + expect(tables).toEqual([{ name: "accounts", schema: "reporting", type: "table", detail: "→ Customer accounts" }]); + }); + it("keeps schema-qualified local table completion scoped to the selected schema", async () => { const completionAssistantSearch = vi.fn().mockRejectedValue(new Error("assistant unavailable")); const listTables = vi.fn(async (_connectionId: string, _database: string, schema: string, filter: string) => { diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 72d556545a..a1add2e27e 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -7793,7 +7793,7 @@ export const useConnectionStore = defineStore("connection", () => { return api.listTables(connectionId, database, schema, filter, limit); } - async function listCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string, globalSearch = false, currentSchema?: string, catalog?: string, options: { activateConnection?: boolean } = {}): Promise { + async function listCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string, globalSearch = false, currentSchema?: string, catalog?: string, options: { activateConnection?: boolean; verifySchemaMetadata?: boolean } = {}): Promise { const trimmedFilter = filter.trim(); const normalizedFilter = trimmedFilter.toLowerCase(); // Remote queries (Dameng/Oracle) are case-sensitive, so the cache key must @@ -7801,7 +7801,7 @@ export const useConnectionStore = defineStore("connection", () => { // second lookup returns the first's stale results. Local lookups below stay // case-insensitive because tableMatchScore normalizes internally. const relaxedFilter = relaxedCompletionTableFilter(trimmedFilter); - const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${trimmedFilter}:${limit ?? ""}:${schema ?? ""}:${globalSearch ? "global" : "scoped"}:${currentSchema ?? ""}`; + const cacheKey = `${connectionId}:${database}:${catalog ?? ""}:${trimmedFilter}:${limit ?? ""}:${schema ?? ""}:${globalSearch ? "global" : "scoped"}:${currentSchema ?? ""}:${options.verifySchemaMetadata ? "verified" : "indexed"}`; const cachedTables = completionTablesCache.value[cacheKey]; if (cachedTables) { const localTables = lookupLocalCompletionTables(connectionId, database, trimmedFilter, limit, schema, catalog); @@ -7821,8 +7821,10 @@ export const useConnectionStore = defineStore("connection", () => { if (isSchemaAwareDatabase(connectionId)) { if (normalizedFilter || limit) { let results: SqlCompletionTable[] = []; + let assistantCompleted = false; try { results = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema, globalSearch, currentSchema, requestRevision); + assistantCompleted = true; } catch { if (schema) { const tables = await listCompletionTableMetadata(connectionId, database, schema, trimmedFilter, limit, catalog); @@ -7831,6 +7833,15 @@ export const useConnectionStore = defineStore("connection", () => { results = lookupLocalCompletionTables(connectionId, database, normalizedFilter, limit, undefined, catalog); } } + if (assistantCompleted && schema && options.verifySchemaMetadata) { + try { + const tables = await listCompletionTableMetadata(connectionId, database, schema, trimmedFilter, limit, catalog); + results = dedupeCompletionTables([...tableInfosToCompletionTables(tables, schema, catalog), ...results]); + } catch { + // The completion index still provides a best-effort result when + // authoritative metadata is temporarily unavailable. + } + } if (results.length === 0 && relaxedFilter) { if (globalSearch) { try { diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index b5b50fbd32..908e0540dd 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -7563,15 +7563,19 @@ test("Spark query execution applies the selected database as schema context", as } }); -test("Kingbase query execution sends the selected schema context", async () => { +test.each([ + { label: "PostgreSQL", connection: conn("postgres-1"), connectionId: "postgres-1" }, + { label: "GaussDB", connection: clearableQuerySchemaConn("gaussdb-1", "gaussdb"), connectionId: "gaussdb-1" }, + { label: "Kingbase", connection: kingbaseConn("kingbase-1"), connectionId: "kingbase-1" }, +])("$label query execution sends the selected schema context", async ({ connection, connectionId }) => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); const connectionStore = useConnectionStore(); const store = useQueryStore(); const originalFetch = globalThis.fetch; - connectionStore.addEphemeralConnection(kingbaseConn("kingbase-1")); - const tabId = store.createTab("kingbase-1", "qinzhou", "Query", "query", "sdy_smartsite"); + connectionStore.addEphemeralConnection(connection); + const tabId = store.createTab(connectionId, "qinzhou", "Query", "query", "sdy_smartsite"); let executeBody: any; globalThis.fetch = withConnectionHealthMock(async (input, init) => {