feat(postgres): complete sequence names in regclass literals

Closes #4005
This commit is contained in:
t8y2 committed 2026-08-13 02:54:18 +00:00
1 parent a67bcb09db
commit 91e7463ee5
11 files changed
+580 -24

No files matched your search

@@ -883,6 +883,86 @@ describe("connectionStore completion assistant", () => {
]);
});
it("loads PostgreSQL sequences without falling back to routine metadata", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "OrderSequence", kind: "sequence", schema: "App" }],
incomplete: false,
fallback_used: false,
});
const listCompletionObjects = vi.fn().mockResolvedValue([{ name: "not_a_sequence", object_type: "FUNCTION" }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listCompletionObjects,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [postgresConnection()];
store.connectedIds.add("pg-1");
const objects = await store.listCompletionObjects("pg-1", "app", "Order", 20, "App", undefined, false, undefined, ["sequence"], true);
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,
expect.objectContaining({
object_kinds: ["sequence"],
mask: "Order",
case_sensitive: true,
schema: "App",
parent_schema: null,
}),
);
expect(completionAssistantSearch).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
object_kinds: ["sequence"],
mask: "Order",
case_sensitive: false,
schema: null,
parent_schema: null,
}),
);
expect(completionAssistantSearch).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
object_kinds: ["sequence"],
mask: "order",
case_sensitive: true,
schema: "App",
parent_schema: null,
}),
);
expect(listCompletionObjects).not.toHaveBeenCalled();
expect(objects).toEqual([expect.objectContaining({ name: "OrderSequence", schema: "App", type: "sequence" })]);
});
it.each([
["empty", vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false })],
["permission error", vi.fn().mockRejectedValue(new Error("permission denied for sequence"))],
])("returns an empty sequence fallback for %s metadata", async (_caseName, completionAssistantSearch) => {
const listCompletionObjects = vi.fn().mockResolvedValue([{ name: "not_a_sequence", object_type: "FUNCTION" }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
listCompletionObjects,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [postgresConnection()];
store.connectedIds.add("pg-1");
await expect(store.listCompletionObjects("pg-1", "app", "missing", 20, undefined, undefined, false, undefined, ["sequence"])).resolves.toEqual([]);
expect(listCompletionObjects).not.toHaveBeenCalled();
});
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: "app_user", data_type: "float" }],
+29 -8
View File
@@ -5921,7 +5921,7 @@ export const useConnectionStore = defineStore("connection", () => {
return candidates
.map((candidate): SqlCompletionObject | null => {
const candidateType = candidate.data_type?.toUpperCase();
const type = candidate.kind === "procedure" ? "procedure" : candidate.kind === "function" ? "function" : candidate.kind === "object" && candidateType === "PACKAGE" ? "package" : null;
const type = candidate.kind === "procedure" ? "procedure" : candidate.kind === "function" ? "function" : candidate.kind === "sequence" ? "sequence" : candidate.kind === "object" && candidateType === "PACKAGE" ? "package" : null;
if (!type) return null;
const dataType = candidate.data_type && !["FUNCTION", "PROCEDURE", "PACKAGE"].includes(candidateType ?? "") ? candidate.data_type : undefined;
return {
@@ -6000,20 +6000,23 @@ export const useConnectionStore = defineStore("connection", () => {
globalSearch: boolean,
currentSchema: string | undefined,
objectKinds: CompletionAssistantObjectKind[],
caseSensitive: boolean,
): Promise<SqlCompletionObject[]> {
const databaseType = getConfig(connectionId)?.db_type;
const oracleAssistant = databaseType === "oracle";
const requestedSchema = schema?.trim() || currentSchema?.trim() || undefined;
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
const sequenceOnly = objectKinds.length === 1 && objectKinds[0] === "sequence";
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (!sequenceOnly && databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
const response = await completionAssistantSearch({
connection_id: connectionId,
database,
schema: oracleAssistant ? (preferredSchema ?? null) : (requestedSchema ?? null),
object_kinds: objectKinds,
mask: filter.trim(),
case_sensitive: caseSensitive,
max_results: limit ?? 200,
global_search: globalSearch,
parent_schema: globalSearch ? null : (schema ?? null),
parent_schema: globalSearch || sequenceOnly ? null : (schema ?? null),
parent_name: parentName ?? null,
match_mode: "prefix",
});
@@ -6528,11 +6531,25 @@ export const useConnectionStore = defineStore("connection", () => {
return deduped;
}
async function listCompletionObjects(connectionId: string, database: string, filter = "", limit?: number, schema?: string, parentName?: string, globalSearch = false, currentSchema?: string, objectKinds: CompletionAssistantObjectKind[] = ["routine"]): Promise<SqlCompletionObject[]> {
async function listCompletionObjects(
connectionId: string,
database: string,
filter = "",
limit?: number,
schema?: string,
parentName?: string,
globalSearch = false,
currentSchema?: string,
objectKinds: CompletionAssistantObjectKind[] = ["routine"],
caseSensitive = false,
): Promise<SqlCompletionObject[]> {
const normalizedFilter = filter.trim().toLowerCase();
const cacheFilter = caseSensitive ? filter.trim() : normalizedFilter;
const databaseType = getConfig(connectionId)?.db_type;
const filteredRoutineAssistant = !!databaseType && FILTERED_ROUTINE_COMPLETION_DATABASES.has(databaseType) && (!!normalizedFilter || typeof limit === "number" || !!parentName || globalSearch);
const cacheKey = filteredRoutineAssistant ? `${connectionId}:${database}:${schema ?? ""}:${parentName ?? ""}:${normalizedFilter}:${limit ?? ""}:${globalSearch ? "global" : "scoped"}:${currentSchema ?? ""}:${[...objectKinds].sort().join(",")}` : `${connectionId}:${database}:${schema ?? ""}`;
const cacheKey = filteredRoutineAssistant
? `${connectionId}:${database}:${schema ?? ""}:${parentName ?? ""}:${cacheFilter}:${limit ?? ""}:${globalSearch ? "global" : "scoped"}:${currentSchema ?? ""}:${[...objectKinds].sort().join(",")}:${caseSensitive ? "case-sensitive" : "case-insensitive"}`
: `${connectionId}:${database}:${schema ?? ""}`;
if (!completionObjectsCache.value[cacheKey]) {
await withCompletionInFlight(
`${cacheKey}:objects`,
@@ -6540,10 +6557,14 @@ 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));
completionObjectsCache.value[cacheKey] = dedupeCompletionObjects(await listCompletionAssistantObjects(connectionId, database, filter, limit, schema, parentName, globalSearch, currentSchema, objectKinds, caseSensitive));
} catch {
const objects = isSchemaAwareDatabase(connectionId) ? await listSchemaAwareCompletionObjects(connectionId, database, schema) : await api.listCompletionObjects(connectionId, database, schema || database);
completionObjectsCache.value[cacheKey] = dedupeCompletionObjects(objects.map(toSqlCompletionObject).filter((object): object is SqlCompletionObject => object != null));
if (objectKinds.length === 1 && objectKinds[0] === "sequence") {
completionObjectsCache.value[cacheKey] = [];
} else {
const objects = isSchemaAwareDatabase(connectionId) ? await listSchemaAwareCompletionObjects(connectionId, database, schema) : await api.listCompletionObjects(connectionId, database, schema || database);
completionObjectsCache.value[cacheKey] = dedupeCompletionObjects(objects.map(toSqlCompletionObject).filter((object): object is SqlCompletionObject => object != null));
}
}
} else {
const objects = isSchemaAwareDatabase(connectionId) ? await listSchemaAwareCompletionObjects(connectionId, database, schema) : await api.listCompletionObjects(connectionId, database, schema || database);