fix(sqlserver): honor USE context in completions

This commit is contained in:
guoyongchang authored and GitHub committed 2026-07-31 21:22:31 +08:00
1 parent f64d363abe
commit 486a73ef09
16 files changed
+974 -114

No files matched your search

@@ -557,9 +557,9 @@ describe("connectionStore completion assistant", () => {
]);
});
it("searches default SQL Server schemas without treating the username as a schema", async () => {
it("searches the server-reported SQL Server default schema without treating the username as a schema", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "st_area", kind: "function", schema: "dbo", data_type: "float" }],
candidates: [{ name: "st_area", kind: "function", schema: "app_user", data_type: "float" }],
incomplete: false,
fallback_used: false,
});
@@ -576,11 +576,11 @@ describe("connectionStore completion assistant", () => {
store.connections = [sqlServerConnection()];
store.connectedIds.add("sqlserver-1");
const objects = await store.listCompletionObjects("sqlserver-1", "app", "st_", 20);
const objects = await store.listCompletionObjects("sqlserver-1", "app", "st_", 20, undefined, undefined, false, "app_user");
expect(completionAssistantSearch).toHaveBeenCalledWith(
expect.objectContaining({
schema: null,
schema: "app_user",
parent_schema: null,
mask: "st_",
}),
@@ -588,15 +588,100 @@ describe("connectionStore completion assistant", () => {
expect(objects).toEqual([
expect.objectContaining({
name: "st_area",
schema: "dbo",
schema: "app_user",
type: "function",
dataType: "float",
applyName: "dbo.st_area",
applyName: "app_user.st_area",
boost: 1000,
}),
]);
});
it("prefers an explicit SQL Server routine schema over the current default schema", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "calculate_tax", kind: "function", schema: "sales", data_type: "decimal" }],
incomplete: false,
fallback_used: false,
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listCompletionObjects: vi.fn().mockResolvedValue([]),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [sqlServerConnection()];
store.connectedIds.add("sqlserver-1");
await store.listCompletionObjects("sqlserver-1", "BarDB", "calculate_", 20, "sales", undefined, false, "app_user");
expect(completionAssistantSearch).toHaveBeenCalledWith(
expect.objectContaining({
database: "BarDB",
schema: "sales",
parent_schema: "sales",
mask: "calculate_",
}),
);
});
it("caches SQL Server completion context independently for each database", async () => {
const getSqlServerCompletionContext = vi.fn(async (_connectionId: string, database: string) => ({
default_schema: database === "BarDB" ? "bar_user" : "foo_user",
supports_session_database_switch: true,
}));
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
getSqlServerCompletionContext,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [sqlServerConnection()];
store.connectedIds.add("sqlserver-1");
const foo = await store.getSqlServerCompletionContext("sqlserver-1", "FooDB");
const fooCached = await store.getSqlServerCompletionContext("sqlserver-1", "FooDB");
const bar = await store.getSqlServerCompletionContext("sqlserver-1", "BarDB");
expect(foo).toMatchObject({ default_schema: "foo_user" });
expect(fooCached).toEqual(foo);
expect(bar).toMatchObject({ default_schema: "bar_user" });
expect(getSqlServerCompletionContext).toHaveBeenCalledTimes(2);
});
it("keeps SQL Server routine results isolated across databases", async () => {
const completionAssistantSearch = vi.fn(async (request: { database: string }) => ({
candidates: [{ name: request.database === "BarDB" ? "bar_proc" : "foo_proc", kind: "procedure", schema: "app_user" }],
incomplete: false,
fallback_used: false,
}));
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listCompletionObjects: vi.fn().mockResolvedValue([]),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [sqlServerConnection()];
store.connectedIds.add("sqlserver-1");
const foo = await store.listCompletionObjects("sqlserver-1", "FooDB", "", 20, undefined, undefined, false, "app_user");
const bar = await store.listCompletionObjects("sqlserver-1", "BarDB", "", 20, undefined, undefined, false, "app_user");
expect(foo.map((object) => object.name)).toEqual(["foo_proc"]);
expect(bar.map((object) => object.name)).toEqual(["bar_proc"]);
expect(completionAssistantSearch).toHaveBeenCalledTimes(2);
});
it("limits concurrent completion column metadata requests per connection database", async () => {
const gates = [deferred<any[]>(), deferred<any[]>(), deferred<any[]>(), deferred<any[]>()];
let activeColumns = 0;
+22 -2
View File
@@ -10,6 +10,7 @@ import type {
DatabaseType,
DatabaseConnectionInfo,
DatabaseStorageInfo,
SqlServerCompletionContext,
CatalogInfo,
ForeignKeyInfo,
ObjectInfo,
@@ -340,6 +341,7 @@ export const useConnectionStore = defineStore("connection", () => {
const completionColumnsCache = ref<Record<string, ColumnInfo[]>>({});
const completionForeignKeysCache = ref<Record<string, ForeignKeyInfo[]>>({});
const completionDatabasesCache = ref<Record<string, string[]>>({});
const sqlServerCompletionContextCache = ref<Record<string, SqlServerCompletionContext>>({});
const elasticsearchCompletionIndicesCache = ref<Record<string, string[]>>({});
const redisCompletionKeysCache = ref<Record<string, string[]>>({});
const mongoCompletionCollectionsCache = ref<Record<string, string[]>>({});
@@ -2310,6 +2312,9 @@ export const useConnectionStore = defineStore("connection", () => {
for (const key of Object.keys(schemaListCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete schemaListCache.value[key];
}
for (const key of Object.keys(sqlServerCompletionContextCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete sqlServerCompletionContextCache.value[key];
}
for (const key of Object.keys(elasticsearchCompletionIndicesCache.value)) {
if (key === exactCacheKey || key.startsWith(cachePrefix)) delete elasticsearchCompletionIndicesCache.value[key];
}
@@ -5417,8 +5422,8 @@ export const useConnectionStore = defineStore("connection", () => {
): Promise<SqlCompletionObject[]> {
const databaseType = getConfig(connectionId)?.db_type;
const oracleAssistant = databaseType === "oracle";
const requestedSchema = currentSchema?.trim() || schema?.trim() || undefined;
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (databaseType === "sqlserver" ? "dbo" : databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
const requestedSchema = schema?.trim() || currentSchema?.trim() || undefined;
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
const response = await completionAssistantSearch({
connection_id: connectionId,
database,
@@ -5666,6 +5671,20 @@ export const useConnectionStore = defineStore("connection", () => {
);
}
async function getSqlServerCompletionContext(connectionId: string, database: string): Promise<SqlServerCompletionContext> {
const cacheKey = `${connectionId}:${database}`;
if (sqlServerCompletionContextCache.value[cacheKey]) {
return sqlServerCompletionContextCache.value[cacheKey];
}
return withCompletionInFlight(`${cacheKey}:sqlserver-completion-context`, async () => {
await ensureConnected(connectionId);
const context = await api.getSqlServerCompletionContext(connectionId, database);
sqlServerCompletionContextCache.value[cacheKey] = context;
evictOldestCacheEntries(sqlServerCompletionContextCache.value, COMPLETION_CACHE_MAX);
return context;
});
}
async function listCompletionSchemas(connectionId: string, database: string): Promise<string[]> {
const cacheKey = `${connectionId}:${database}`;
if (schemaListCache.value[cacheKey]) {
@@ -6776,6 +6795,7 @@ export const useConnectionStore = defineStore("connection", () => {
listCompletionForeignKeys,
listCompletionSchemas,
listCompletionDatabases,
getSqlServerCompletionContext,
lookupLocalCompletionTables,
lookupLocalCompletionObjects,
lookupLocalCompletionColumns,