fix(editor): verify current-schema tables in semantic diagnosis

This commit is contained in:
zipg
2026-09-09 16:05:43 +08:00
committed by GitHub
parent 3b46109d36
commit fd44331876
5 changed files with 53 additions and 8 deletions
@@ -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;
}
@@ -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", () => {
@@ -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) => {
+13 -2
View File
@@ -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<SqlCompletionTable[]> {
async function listCompletionTables(connectionId: string, database: string, filter = "", limit?: number, schema?: string, globalSearch = false, currentSchema?: string, catalog?: string, options: { activateConnection?: boolean; verifySchemaMetadata?: boolean } = {}): Promise<SqlCompletionTable[]> {
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 {
+7 -3
View File
@@ -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) => {