diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index 5364e59f4f..d22549559a 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -59,6 +59,16 @@ function oracleConnection(): ConnectionConfig { } as ConnectionConfig; } +function oceanBaseOracleConnection(): ConnectionConfig { + return { + ...oracleConnection(), + id: "oceanbase-oracle-1", + name: "OceanBase Oracle", + db_type: "oceanbase-oracle", + database: "OBORCL", + } as ConnectionConfig; +} + function sapHanaConnection(): ConnectionConfig { return { ...postgresConnection(), @@ -678,6 +688,80 @@ describe("connectionStore completion assistant", () => { expect(completionAssistantSearch).not.toHaveBeenCalled(); }); + it.each(["empty", "error"] as const)("normalizes unquoted OceanBase Oracle identifiers after an %s assistant result", async (assistantResult) => { + const completionAssistantSearch = assistantResult === "empty" ? vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false }) : vi.fn().mockRejectedValue(new Error("assistant unavailable")); + const getColumns = vi.fn().mockResolvedValue([{ name: "ORDER_ID", data_type: "NUMBER", is_nullable: false, column_default: null, is_primary_key: true, extra: null, comment: null }]); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + getColumns, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [oceanBaseOracleConnection()]; + store.connectedIds.add("oceanbase-oracle-1"); + + const lower = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "orders_alias", "app", { tableQuoted: false, schemaQuoted: false }); + const upper = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "ORDERS_ALIAS", "APP", { tableQuoted: false, schemaQuoted: false }); + + expect(completionAssistantSearch).toHaveBeenCalledOnce(); + expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "APP", parent_schema: "APP", parent_name: "ORDERS_ALIAS" })); + expect(getColumns).toHaveBeenCalledOnce(); + expect(getColumns).toHaveBeenCalledWith("oceanbase-oracle-1", "OBORCL", "APP", "ORDERS_ALIAS", undefined, undefined); + expect(lower).toEqual([expect.objectContaining({ name: "ORDER_ID", table: "ORDERS_ALIAS", schema: "APP" })]); + expect(upper).toEqual(lower); + }); + + it("keeps quoted OceanBase Oracle identifiers exact and isolated from unquoted cache entries", async () => { + const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null; parent_schema?: string | null }) => ({ + candidates: [ + { + name: request.parent_name === "ORDERS_ALIAS" ? "UPPER_ID" : "MixedId", + kind: "column", + schema: request.parent_schema, + parent_schema: request.parent_schema, + parent_name: request.parent_name, + data_type: "NUMBER", + }, + ], + incomplete: false, + fallback_used: false, + })); + const getColumns = vi.fn(); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + getColumns, + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [oceanBaseOracleConnection()]; + store.connectedIds.add("oceanbase-oracle-1"); + + const unquoted = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "orders_alias", "app", { tableQuoted: false, schemaQuoted: false }); + const unquotedCached = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "ORDERS_ALIAS", "APP", { tableQuoted: false, schemaQuoted: false }); + const quoted = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "Orders_Alias", "AppSchema", { tableQuoted: true, schemaQuoted: true }); + const quotedCached = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", "Orders_Alias", "AppSchema", { tableQuoted: true, schemaQuoted: true }); + + expect(completionAssistantSearch.mock.calls.map(([request]) => ({ schema: request.schema, parent_schema: request.parent_schema, parent_name: request.parent_name }))).toEqual([ + { schema: "APP", parent_schema: "APP", parent_name: "ORDERS_ALIAS" }, + { schema: "AppSchema", parent_schema: "AppSchema", parent_name: "Orders_Alias" }, + ]); + expect(getColumns).not.toHaveBeenCalled(); + expect(unquoted).toEqual([expect.objectContaining({ name: "UPPER_ID", table: "ORDERS_ALIAS", schema: "APP" })]); + expect(unquotedCached).toEqual(unquoted); + expect(quoted).toEqual([expect.objectContaining({ name: "MixedId", table: "Orders_Alias", schema: "AppSchema" })]); + expect(quotedCached).toEqual(quoted); + expect(store.lookupLocalCompletionColumns("oceanbase-oracle-1", "OBORCL", "orders_alias", "app", undefined, { tableQuoted: false, schemaQuoted: false })).toEqual(unquoted); + expect(store.lookupLocalCompletionColumns("oceanbase-oracle-1", "OBORCL", "Orders_Alias", "AppSchema", undefined, { tableQuoted: true, schemaQuoted: true })).toEqual(quoted); + }); + it("normalizes unquoted SAP HANA table and schema names before column lookup", async () => { const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null; parent_schema?: string | null }) => ({ candidates: [ diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index c40938bf3d..786ad05c6b 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -6634,7 +6634,7 @@ export const useConnectionStore = defineStore("connection", () => { function completionColumnsKey(connectionId: string, database: string, table: string, schema?: string, catalog?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): string { const databaseType = getConfig(connectionId)?.db_type; - if (databaseType === "oracle") { + if (databaseType === "oracle" || databaseType === "oceanbase-oracle") { const normalizedTable = context?.tableQuoted === false ? table.toUpperCase() : table; const normalizedSchema = schema && context?.schemaQuoted === false ? schema.toUpperCase() : (schema ?? ""); return `${connectionId}:${database}:${catalog ?? ""}:${normalizedSchema}:${normalizedTable}`; @@ -7736,7 +7736,7 @@ export const useConnectionStore = defineStore("connection", () => { async function listCompletionColumns(connectionId: string, database: string, table: string, schema?: string, context?: { clientSessionId?: string; version?: number; tableQuoted?: boolean; schemaQuoted?: boolean }, catalog?: string): Promise { const config = getConfig(connectionId); - const oracleIdentifier = config?.db_type === "oracle"; + const oracleIdentifier = config?.db_type === "oracle" || config?.db_type === "oceanbase-oracle"; const uppercaseUnquotedIdentifier = oracleIdentifier || config?.db_type === "saphana"; const completionTable = uppercaseUnquotedIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table; const rawCompletionSchema = schema?.trim() || (config?.db_type === "dameng" ? config.username?.trim() || undefined : undefined);