mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(completion): add contains fallback for first fuzzy input
This commit is contained in:
@@ -374,7 +374,8 @@ describe("connectionStore completion assistant", () => {
|
||||
|
||||
const [first, second] = await Promise.all([store.listCompletionTables("pg-1", "app", "acc", 20, "public"), store.listCompletionTables("pg-1", "app", "acc", 20, "public")]);
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledTimes(1);
|
||||
// Both callers share one in-flight prefix lookup plus its widened substring lookup.
|
||||
expect(completionAssistantSearch.mock.calls.map(([request]) => (request as { match_mode?: string }).match_mode)).toEqual(["prefix", "contains"]);
|
||||
expect(first).toEqual(second);
|
||||
expect(first[0]).toMatchObject({ name: "accounts", schema: "public", type: "table" });
|
||||
});
|
||||
@@ -426,11 +427,62 @@ describe("connectionStore completion assistant", () => {
|
||||
|
||||
const tables = await store.listCompletionTables("pg-1", "app", "accounts", 200, "reporting", false, "reporting", undefined, { verifySchemaMetadata: true });
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledOnce();
|
||||
expect(completionAssistantSearch.mock.calls.map(([request]) => (request as { match_mode?: string }).match_mode)).toEqual(["prefix", "contains"]);
|
||||
expect(listTables).toHaveBeenCalledWith("pg-1", "app", "reporting", "accounts", 200);
|
||||
expect(tables).toEqual([{ name: "accounts", schema: "reporting", type: "table", detail: "→ Customer accounts" }]);
|
||||
});
|
||||
|
||||
it("widens a sparse prefix search with substring matches", async () => {
|
||||
const completionAssistantSearch = vi.fn(async (request: { match_mode?: string }) =>
|
||||
request.match_mode === "contains" ? { candidates: [{ name: "YY_SFXXMK", kind: "view", schema: "public" }], incomplete: false, fallback_used: false } : { candidates: [{ name: "SFXXMK", kind: "table", schema: "public" }], incomplete: false, fallback_used: false },
|
||||
);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
listSchemas: vi.fn().mockResolvedValue(["public"]),
|
||||
listTables: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
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", "sfxxm", 200, "public");
|
||||
|
||||
expect(completionAssistantSearch.mock.calls.map(([request]) => (request as { match_mode?: string }).match_mode)).toEqual(["prefix", "contains"]);
|
||||
expect(tables.map((table) => table.name)).toEqual(["SFXXMK", "YY_SFXXMK"]);
|
||||
});
|
||||
|
||||
it("skips the substring lookup when the prefix search fills the result budget or the filter is short", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [{ name: "accounts", kind: "table", schema: "public" }],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
});
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
listSchemas: vi.fn().mockResolvedValue(["public"]),
|
||||
listTables: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [postgresConnection()];
|
||||
store.connectedIds.add("pg-1");
|
||||
|
||||
await store.listCompletionTables("pg-1", "app", "acc", 1, "public");
|
||||
expect(completionAssistantSearch).toHaveBeenCalledTimes(1);
|
||||
|
||||
await store.listCompletionTables("pg-1", "app", "ac", 200, "public");
|
||||
expect(completionAssistantSearch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
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) => {
|
||||
@@ -1386,8 +1438,11 @@ describe("connectionStore completion assistant", () => {
|
||||
await store.listCompletionObjects("pg-1", "app", "Order", 20, undefined, undefined, false, undefined, ["sequence"]);
|
||||
await store.listCompletionObjects("pg-1", "app", "order", 20, "App", undefined, false, undefined, ["sequence"], true);
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
// Each lookup issues a prefix search plus a widened substring search; the prefix
|
||||
// requests carry the scoping and case sensitivity under test here.
|
||||
const prefixRequests = completionAssistantSearch.mock.calls.map(([request]) => request as { match_mode?: string }).filter((request) => request.match_mode === "prefix");
|
||||
expect(prefixRequests).toHaveLength(3);
|
||||
expect(prefixRequests[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
object_kinds: ["sequence"],
|
||||
mask: "Order",
|
||||
@@ -1396,8 +1451,7 @@ describe("connectionStore completion assistant", () => {
|
||||
parent_schema: null,
|
||||
}),
|
||||
);
|
||||
expect(completionAssistantSearch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect(prefixRequests[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
object_kinds: ["sequence"],
|
||||
mask: "Order",
|
||||
@@ -1406,8 +1460,7 @@ describe("connectionStore completion assistant", () => {
|
||||
parent_schema: null,
|
||||
}),
|
||||
);
|
||||
expect(completionAssistantSearch).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect(prefixRequests[2]).toEqual(
|
||||
expect.objectContaining({
|
||||
object_kinds: ["sequence"],
|
||||
mask: "order",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DEFAULT_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
|
||||
import type {
|
||||
ColumnInfo,
|
||||
CompletionAssistantCandidate,
|
||||
CompletionAssistantMatchMode,
|
||||
CompletionAssistantObjectKind,
|
||||
CompletionAssistantRequest,
|
||||
ConnectionConfig,
|
||||
@@ -7625,7 +7626,29 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}));
|
||||
}
|
||||
|
||||
async function listCompletionAssistantTables(connectionId: string, database: string, filter: string, limit?: number, schema?: string, globalSearch = false, currentSchema?: string, requestRevision = completionCacheRevision(connectionId, database)): Promise<SqlCompletionTable[]> {
|
||||
/**
|
||||
* The completion assistant matches names by prefix only, while the warm local
|
||||
* index also matches substrings. Without widening, the very first fuzzy lookup
|
||||
* of a connection (empty local index) returns far fewer candidates than the
|
||||
* same lookup once the index is warm. Widening to a substring search keeps the
|
||||
* two paths consistent whenever the prefix search left room in the result list.
|
||||
*/
|
||||
function shouldWidenCompletionMatch(filter: string, resultCount: number, limit?: number): boolean {
|
||||
if (filter.trim().length < 3) return false;
|
||||
return limit === undefined || resultCount < limit;
|
||||
}
|
||||
|
||||
async function listCompletionAssistantTables(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
filter: string,
|
||||
limit?: number,
|
||||
schema?: string,
|
||||
globalSearch = false,
|
||||
currentSchema?: string,
|
||||
requestRevision = completionCacheRevision(connectionId, database),
|
||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||
): Promise<SqlCompletionTable[]> {
|
||||
const oracleAssistant = getConfig(connectionId)?.db_type === "oracle";
|
||||
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, globalSearch ? currentSchema : (schema ?? currentSchema)) : schema?.trim() || undefined;
|
||||
const objectKinds: CompletionAssistantObjectKind[] = ["table", "view"];
|
||||
@@ -7639,7 +7662,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
max_results: limit ?? 200,
|
||||
global_search: globalSearch,
|
||||
parent_schema: globalSearch ? null : (schema ?? null),
|
||||
match_mode: "prefix",
|
||||
match_mode: matchMode,
|
||||
},
|
||||
requestRevision,
|
||||
);
|
||||
@@ -7659,6 +7682,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
currentSchema: string | undefined,
|
||||
objectKinds: CompletionAssistantObjectKind[],
|
||||
caseSensitive: boolean,
|
||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||
): Promise<SqlCompletionObject[]> {
|
||||
const databaseType = getConfig(connectionId)?.db_type;
|
||||
const oracleAssistant = databaseType === "oracle";
|
||||
@@ -7676,7 +7700,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
global_search: globalSearch,
|
||||
parent_schema: globalSearch || sequenceOnly ? null : (schema ?? null),
|
||||
parent_name: parentName ?? null,
|
||||
match_mode: "prefix",
|
||||
match_mode: matchMode,
|
||||
});
|
||||
const objects = completionAssistantObjects(response.candidates, preferredSchema, oracleAssistant).map((object) => ({
|
||||
...object,
|
||||
@@ -8176,6 +8200,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
try {
|
||||
results = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema, globalSearch, currentSchema, requestRevision);
|
||||
assistantCompleted = true;
|
||||
if (shouldWidenCompletionMatch(trimmedFilter, results.length, limit)) {
|
||||
try {
|
||||
const widenedTables = await listCompletionAssistantTables(connectionId, database, trimmedFilter, limit, schema, globalSearch, currentSchema, requestRevision, "contains");
|
||||
results = dedupeCompletionTables([...results, ...widenedTables]);
|
||||
} catch {
|
||||
// Keep the prefix matches when the widened lookup is unavailable.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (schema) {
|
||||
const tables = await listCompletionTableMetadata(connectionId, database, schema, trimmedFilter, limit, catalog);
|
||||
@@ -8309,7 +8341,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await ensureConnected(connectionId);
|
||||
if (filteredRoutineAssistant) {
|
||||
try {
|
||||
completionObjectsCache.value[cacheKey] = dedupeCompletionObjects(await listCompletionAssistantObjects(connectionId, database, filter, limit, schema, parentName, globalSearch, currentSchema, objectKinds, caseSensitive));
|
||||
let assistantObjects = await listCompletionAssistantObjects(connectionId, database, filter, limit, schema, parentName, globalSearch, currentSchema, objectKinds, caseSensitive);
|
||||
if (shouldWidenCompletionMatch(filter, assistantObjects.length, limit)) {
|
||||
try {
|
||||
const widenedObjects = await listCompletionAssistantObjects(connectionId, database, filter, limit, schema, parentName, globalSearch, currentSchema, objectKinds, caseSensitive, "contains");
|
||||
assistantObjects = [...assistantObjects, ...widenedObjects];
|
||||
} catch {
|
||||
// Keep the prefix matches when the widened lookup is unavailable.
|
||||
}
|
||||
}
|
||||
completionObjectsCache.value[cacheKey] = dedupeCompletionObjects(assistantObjects);
|
||||
} catch {
|
||||
if (objectKinds.length === 1 && objectKinds[0] === "sequence") {
|
||||
completionObjectsCache.value[cacheKey] = [];
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
Reference in New Issue
Block a user